Merge pull request #3599 from gradido/load_moderator_names

feat(admin): load moderator names for contribution list
This commit is contained in:
einhornimmond 2025-12-30 12:48:24 +01:00 committed by GitHub
commit a763f7005b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 155 additions and 50 deletions

View File

@ -6,7 +6,7 @@
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'" :icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
aria-label="Help" aria-label="Help"
></b-icon> ></b-icon>
{{ $t('hide_details') }} {{ row.item.user.firstName }} {{ row.item.user.lastName }} {{ $t('hide_details') }}
</b-button> </b-button>
</b-card> </b-card>
</template> </template>

View File

@ -34,11 +34,20 @@
</BButton> </BButton>
</div> </div>
</template> </template>
<template #cell(name)="row">
<span v-if="row.item.user">
{{ row.item.user.firstName }} {{ row.item.user.lastName }}
<small v-if="row.item.user.alias">
<hr />
{{ row.item.user.alias }}
</small>
</span>
</template>
<template #cell(memo)="row"> <template #cell(memo)="row">
{{ row.value }} {{ row.value }}
<small v-if="row.item.updatedBy > 0"> <small v-if="isAddCommentToMemo(row.item)" class="no-select">
<hr /> <hr />
{{ $t('moderator.memo-modified') }} {{ getMemoComment(row.item) }}
</small> </small>
</template> </template>
<template #cell(editCreation)="row"> <template #cell(editCreation)="row">
@ -140,6 +149,7 @@
import RowDetails from '../RowDetails' import RowDetails from '../RowDetails'
import EditCreationFormular from '../EditCreationFormular' import EditCreationFormular from '../EditCreationFormular'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList' import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList'
import { useDateFormatter } from '@/composables/useDateFormatter'
const iconMap = { const iconMap = {
IN_PROGRESS: 'question-square', IN_PROGRESS: 'question-square',
@ -195,6 +205,7 @@ export default {
this.removeClipboardListener() this.removeClipboardListener()
}, },
methods: { methods: {
...useDateFormatter(),
myself(item) { myself(item) {
return item.userId === this.$store.state.moderator.id return item.userId === this.$store.state.moderator.id
}, },
@ -235,6 +246,36 @@ export default {
this.creationUserData = row.item this.creationUserData = row.item
} }
}, },
isAddCommentToMemo(item) {
return item.closedBy > 0 || item.moderatorId > 0 || item.updatedBy > 0
},
getMemoComment(item) {
let comment = ''
if (item.closedBy > 0) {
if (item.contributionStatus === 'CONFIRMED') {
comment = this.$t('contribution.confirmedBy', { name: item.closedByUserName })
} else if (item.contributionStatus === 'DENIED') {
comment = this.$t('contribution.deniedBy', { name: item.closedByUserName })
} else if (item.contributionStatus === 'DELETED') {
comment = this.$t('contribution.deletedBy', { name: item.closedByUserName })
}
}
if (item.updatedBy > 0) {
if (comment.length) {
comment += ' | '
}
comment += this.$t('moderator.memo-modified', { name: item.updatedByUserName })
}
if (item.moderatorId > 0) {
if (comment.length) {
comment += ' | '
}
comment += this.$t('contribution.createdBy', { name: item.moderatorUserName })
}
return comment
},
addClipboardListener() { addClipboardListener() {
document.addEventListener('copy', this.handleCopy) document.addEventListener('copy', this.handleCopy)
}, },

View File

@ -1,10 +1,14 @@
export const useDateFormatter = () => { export const useDateFormatter = () => {
const formatDateFromDateTime = (datetimeString) => { const formatDateFromDateTime = (datetimeString) => {
if (!datetimeString || !datetimeString?.includes('T')) return datetimeString if (!datetimeString || !datetimeString?.includes('T')) {
return datetimeString
}
return datetimeString.split('T')[0] return datetimeString.split('T')[0]
} }
const formatDateOrDash = (value) => (value ? new Date(value).toLocaleDateString() : '—')
return { return {
formatDateFromDateTime, formatDateFromDateTime,
formatDateOrDash,
} }
} }

View File

@ -19,19 +19,18 @@ query adminListContributions(
} }
amount amount
memo memo
createdAt closedAt
closedBy
closedByUserName
contributionDate contributionDate
confirmedAt createdAt
confirmedBy
updatedAt updatedAt
updatedBy updatedBy
updatedByUserName
contributionStatus contributionStatus
messagesCount messagesCount
deniedAt
deniedBy
deletedAt
deletedBy
moderatorId moderatorId
moderatorUserName
userId userId
resubmissionAt resubmissionAt
} }

View File

@ -16,6 +16,12 @@
"back": "zurück", "back": "zurück",
"change_user_role": "Nutzerrolle ändern", "change_user_role": "Nutzerrolle ändern",
"close": "Schließen", "close": "Schließen",
"contribution": {
"confirmedBy": "Bestätigt von {name}.",
"createdBy": "Erstellt von {name}.",
"deletedBy": "Gelöscht von {name}.",
"deniedBy": "Abgelehnt von {name}."
},
"contributionLink": { "contributionLink": {
"amount": "Betrag", "amount": "Betrag",
"changeSaved": "Änderungen gespeichert", "changeSaved": "Änderungen gespeichert",
@ -49,6 +55,7 @@
}, },
"contributions": { "contributions": {
"all": "Alle", "all": "Alle",
"closed": "Geschlossen",
"confirms": "Bestätigt", "confirms": "Bestätigt",
"deleted": "Gelöscht", "deleted": "Gelöscht",
"denied": "Abgelehnt", "denied": "Abgelehnt",
@ -166,7 +173,7 @@
"moderator": { "moderator": {
"history": "Die Daten wurden geändert. Dies sind die alten Daten.", "history": "Die Daten wurden geändert. Dies sind die alten Daten.",
"memo": "Text ändern", "memo": "Text ändern",
"memo-modified": "Text vom Moderator bearbeitet.", "memo-modified": "Text von {name} bearbeitet.",
"memo-tooltip": "Den Beitragstext bearbeiten", "memo-tooltip": "Den Beitragstext bearbeiten",
"message": "Nachricht", "message": "Nachricht",
"message-tooltip": "Nachricht an Benutzer schreiben", "message-tooltip": "Nachricht an Benutzer schreiben",

View File

@ -16,6 +16,12 @@
"back": "back", "back": "back",
"change_user_role": "Change user role", "change_user_role": "Change user role",
"close": "Close", "close": "Close",
"contribution": {
"confirmedBy": "Confirmed by {name}.",
"createdBy": "Created by {name}.",
"deletedBy": "Deleted by {name}.",
"deniedBy": "Rejected by {name}."
},
"contributionLink": { "contributionLink": {
"amount": "Amount", "amount": "Amount",
"changeSaved": "Changes saved", "changeSaved": "Changes saved",
@ -49,6 +55,7 @@
}, },
"contributions": { "contributions": {
"all": "All", "all": "All",
"closed": "Closed",
"confirms": "Confirmed", "confirms": "Confirmed",
"deleted": "Deleted", "deleted": "Deleted",
"denied": "Rejected", "denied": "Rejected",
@ -166,7 +173,7 @@
"moderator": { "moderator": {
"history": "The data has been changed. This is the old data.", "history": "The data has been changed. This is the old data.",
"memo": "Edit text", "memo": "Edit text",
"memo-modified": "Text edited by moderator", "memo-modified": "Text edited by {name}",
"memo-tooltip": "Edit the text of the contribution", "memo-tooltip": "Edit the text of the contribution",
"message": "Message", "message": "Message",
"message-tooltip": "Write message to user", "message-tooltip": "Write message to user",

View File

@ -108,6 +108,7 @@ import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
import { getContribution } from '../graphql/getContribution' import { getContribution } from '../graphql/getContribution'
import { useAppToast } from '@/composables/useToast' import { useAppToast } from '@/composables/useToast'
import { useDateFormatter } from '@/composables/useDateFormatter'
import CONFIG from '@/config' import CONFIG from '@/config'
const FILTER_TAB_MAP = [ const FILTER_TAB_MAP = [
@ -134,9 +135,10 @@ const query = ref('')
const noHashtag = ref(null) const noHashtag = ref(null)
const hideResubmissionModel = ref(true) const hideResubmissionModel = ref(true)
const formatDateOrDash = (value) => (value ? new Date(value).toLocaleDateString() : '—') const { formatDateOrDash } = useDateFormatter()
const baseFields = { const baseFields = {
name: { key: 'name', label: t('name'), class: 'no-select' },
firstName: { key: 'user.firstName', label: t('firstname'), class: 'no-select' }, firstName: { key: 'user.firstName', label: t('firstname'), class: 'no-select' },
lastName: { key: 'user.lastName', label: t('lastname'), class: 'no-select' }, lastName: { key: 'user.lastName', label: t('lastname'), class: 'no-select' },
amount: { key: 'amount', label: t('creation'), formatter: (value) => value + ' GDD' }, amount: { key: 'amount', label: t('creation'), formatter: (value) => value + ' GDD' },
@ -153,13 +155,12 @@ const baseFields = {
class: 'no-select', class: 'no-select',
formatter: formatDateOrDash, formatter: formatDateOrDash,
}, },
confirmedAt: { closedAt: {
key: 'confirmedAt', key: 'closedAt',
label: t('contributions.confirms'), label: t('contributions.closed'),
class: 'no-select', class: 'no-select',
formatter: formatDateOrDash, formatter: formatDateOrDash,
}, },
confirmedBy: { key: 'confirmedBy', label: t('moderator.moderator'), class: 'no-select' },
} }
const fields = computed( const fields = computed(
@ -169,70 +170,52 @@ const fields = computed(
[ [
{ key: 'bookmark', label: t('delete') }, { key: 'bookmark', label: t('delete') },
{ key: 'deny', label: t('deny') }, { key: 'deny', label: t('deny') },
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
{ key: 'moderatorId', label: t('moderator.moderator'), class: 'no-select' },
{ key: 'editCreation', label: t('details') }, { key: 'editCreation', label: t('details') },
{ key: 'confirm', label: t('save') }, { key: 'confirm', label: t('save') },
], ],
// confirmed contributions // confirmed contributions
[ [
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
baseFields.confirmedAt, baseFields.closedAt,
baseFields.confirmedBy,
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// denied contributions // denied contributions
[ [
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
{ baseFields.closedAt,
key: 'deniedAt',
label: t('contributions.denied'),
formatter: formatDateOrDash,
},
{ key: 'deniedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// deleted contributions // deleted contributions
[ [
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
{ baseFields.closedAt,
key: 'deletedAt',
label: t('contributions.deleted'),
formatter: formatDateOrDash,
},
{ key: 'deletedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// all contributions // all contributions
[ [
{ key: 'contributionStatus', label: t('status') }, { key: 'contributionStatus', label: t('status') },
baseFields.firstName, baseFields.name,
baseFields.lastName,
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
baseFields.confirmedAt, baseFields.closedAt,
baseFields.confirmedBy,
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
][tabIndex.value], ][tabIndex.value],

View File

@ -1,6 +1,6 @@
import { ContributionStatus } from '@enum/ContributionStatus'
import { Contribution as DbContribution } from 'database' import { Contribution as DbContribution } from 'database'
import { Field, Int, ObjectType } from 'type-graphql' import { Field, Int, ObjectType } from 'type-graphql'
import { UnconfirmedContribution } from './UnconfirmedContribution' import { UnconfirmedContribution } from './UnconfirmedContribution'
@ObjectType() @ObjectType()
@ -8,6 +8,7 @@ export class Contribution extends UnconfirmedContribution {
constructor(dbContribution: DbContribution) { constructor(dbContribution: DbContribution) {
super(dbContribution) super(dbContribution)
this.createdAt = dbContribution.createdAt this.createdAt = dbContribution.createdAt
this.moderatorId = dbContribution.moderatorId
this.confirmedAt = dbContribution.confirmedAt this.confirmedAt = dbContribution.confirmedAt
this.confirmedBy = dbContribution.confirmedBy this.confirmedBy = dbContribution.confirmedBy
this.contributionDate = dbContribution.contributionDate this.contributionDate = dbContribution.contributionDate
@ -19,11 +20,36 @@ export class Contribution extends UnconfirmedContribution {
this.updatedAt = dbContribution.updatedAt this.updatedAt = dbContribution.updatedAt
this.updatedBy = dbContribution.updatedBy this.updatedBy = dbContribution.updatedBy
this.resubmissionAt = dbContribution.resubmissionAt this.resubmissionAt = dbContribution.resubmissionAt
if (ContributionStatus.CONFIRMED === dbContribution.contributionStatus) {
this.closedAt = dbContribution.confirmedAt
this.closedBy = dbContribution.confirmedBy
} else if (ContributionStatus.DELETED === dbContribution.contributionStatus) {
this.closedAt = dbContribution.deletedAt
this.closedBy = dbContribution.deletedBy
} else if (ContributionStatus.DENIED === dbContribution.contributionStatus) {
this.closedAt = dbContribution.deniedAt
this.closedBy = dbContribution.deniedBy
}
} }
@Field(() => Date, { nullable: true })
closedAt?: Date | null
@Field(() => Int, { nullable: true })
closedBy?: number | null
@Field(() => String, { nullable: true })
closedByUserName?: string | null
@Field(() => Date) @Field(() => Date)
createdAt: Date createdAt: Date
@Field(() => Int, { nullable: true })
moderatorId: number | null
@Field(() => String, { nullable: true })
moderatorUserName?: string | null
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
confirmedAt: Date | null confirmedAt: Date | null
@ -48,6 +74,9 @@ export class Contribution extends UnconfirmedContribution {
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
updatedBy: number | null updatedBy: number | null
@Field(() => String, { nullable: true })
updatedByUserName?: string | null
@Field(() => Date) @Field(() => Date)
contributionDate: Date contributionDate: Date

View File

@ -23,6 +23,7 @@ import {
Contribution as DbContribution, Contribution as DbContribution,
Transaction as DbTransaction, Transaction as DbTransaction,
User as DbUser, User as DbUser,
findUserNamesByIds,
getLastTransaction, getLastTransaction,
UserContact, UserContact,
} from 'database' } from 'database'
@ -348,6 +349,7 @@ export class ContributionResolver {
): Promise<ContributionListResult> { ): Promise<ContributionListResult> {
// Check if only count was requested (without contributionList) // Check if only count was requested (without contributionList)
const fields = Object.keys(extractGraphQLFields(info)) const fields = Object.keys(extractGraphQLFields(info))
// console.log(`fields: ${fields}`)
const countOnly: boolean = fields.includes('contributionCount') && fields.length === 1 const countOnly: boolean = fields.includes('contributionCount') && fields.length === 1
// check if related user was requested // check if related user was requested
const userRequested = const userRequested =
@ -370,8 +372,25 @@ export class ContributionResolver {
}, },
countOnly, countOnly,
) )
const result = new ContributionListResult(count, dbContributions)
return new ContributionListResult(count, dbContributions) const uniqueUserIds = new Set<number>()
const addIfExist = (userId?: number | null) => (userId ? uniqueUserIds.add(userId) : null)
for (const contribution of result.contributionList) {
addIfExist(contribution.updatedBy)
addIfExist(contribution.moderatorId)
addIfExist(contribution.closedBy)
}
const users = await findUserNamesByIds(Array.from(uniqueUserIds))
const getNameById = (userId?: number | null) => (userId ? (users.get(userId) ?? null) : null)
for (const contribution of result.contributionList) {
contribution.updatedByUserName = getNameById(contribution.updatedBy)
contribution.moderatorUserName = getNameById(contribution.moderatorId)
contribution.closedByUserName = getNameById(contribution.closedBy)
}
return result
} }
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION]) @Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])

View File

@ -66,6 +66,10 @@ export const findContributions = async (
if (relations?.user) { if (relations?.user) {
qb.orWhere('user.first_name LIKE :firstName', { firstName: queryString }) qb.orWhere('user.first_name LIKE :firstName', { firstName: queryString })
.orWhere('user.last_name LIKE :lastName', { lastName: queryString }) .orWhere('user.last_name LIKE :lastName', { lastName: queryString })
.orWhere('user.alias LIKE :alias', { alias: queryString })
.orWhere("LOWER(CONCAT(user.first_name, ' ', user.last_name)) LIKE LOWER(:fullName)", {
fullName: queryString.toLowerCase(),
})
.orWhere('emailContact.email LIKE :emailContact', { emailContact: queryString }) .orWhere('emailContact.email LIKE :emailContact', { emailContact: queryString })
.orWhere({ memo: Like(queryString) }) .orWhere({ memo: Like(queryString) })
} }

View File

@ -23,7 +23,7 @@ afterAll(async () => {
describe('openaiThreads query test', () => { describe('openaiThreads query test', () => {
it('should insert a new openai thread', async () => { it('should insert a new openai thread', async () => {
await Promise.resolve([dbInsertOpenaiThread('7', 1), dbInsertOpenaiThread('72', 6)]) await Promise.all([dbInsertOpenaiThread('7', 1), dbInsertOpenaiThread('72', 6)])
const result = await db.select().from(openaiThreadsTable) const result = await db.select().from(openaiThreadsTable)
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
expect(result).toMatchObject([ expect(result).toMatchObject([

View File

@ -1,6 +1,6 @@
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { aliasSchema, emailSchema, uuidv4Schema } from 'shared' import { aliasSchema, emailSchema, uuidv4Schema } from 'shared'
import { Raw } from 'typeorm' import { In, Raw } from 'typeorm'
import { User as DbUser, UserContact as DbUserContact } from '../entity' import { User as DbUser, UserContact as DbUserContact } from '../entity'
import { findWithCommunityIdentifier, LOG4JS_QUERIES_CATEGORY_NAME } from './index' import { findWithCommunityIdentifier, LOG4JS_QUERIES_CATEGORY_NAME } from './index'
@ -81,3 +81,15 @@ export async function findForeignUserByUuids(
where: { foreign: true, communityUuid, gradidoID }, where: { foreign: true, communityUuid, gradidoID },
}) })
} }
export async function findUserNamesByIds(userIds: number[]): Promise<Map<number, string>> {
const users = await DbUser.find({
select: { id: true, firstName: true, lastName: true, alias: true },
where: { id: In(userIds) },
})
return new Map(
users.map((user) => {
return [user.id, `${user.firstName} ${user.lastName}`]
}),
)
}