request the name of contribution changer (deniedBy, deletedBy, confirmedBy, updatedBy) and show them in admin frontend

This commit is contained in:
einhornimmond 2025-12-17 13:44:33 +01:00
parent f64b37b273
commit c3acd21a67
11 changed files with 124 additions and 17 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

@ -30,9 +30,9 @@
</template> </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)">
<hr /> <hr />
{{ $t('moderator.memo-modified') }} {{ getMemoComment(row.item) }}
</small> </small>
</template> </template>
<template #cell(editCreation)="row"> <template #cell(editCreation)="row">
@ -229,6 +229,27 @@ export default {
this.creationUserData = row.item this.creationUserData = row.item
} }
}, },
isAddCommentToMemo(item) {
return item.updatedBy > 0 || item.confirmedBy > 0 || item.deletedBy > 0 || item.deniedBy > 0
},
getMemoComment(item) {
let comment = ''
if (item.confirmedBy > 0) {
comment = this.$t('contribution.confirmedBy', { name: item.confirmedByUserName })
} else if (item.deletedBy > 0) {
comment = this.$t('contribution.deletedBy', { name: item.deletedByUserName })
} else if (item.deniedBy > 0) {
comment = this.$t('contribution.deniedBy', { name: item.deniedByUserName })
}
if (item.updatedBy > 0) {
if (comment.length) {
comment += ' '
}
comment += this.$t('moderator.memo-modified', { name: item.updatedByUserName })
}
return comment
},
addClipboardListener() { addClipboardListener() {
document.addEventListener('copy', this.handleCopy) document.addEventListener('copy', this.handleCopy)
}, },

View File

@ -23,15 +23,20 @@ query adminListContributions(
contributionDate contributionDate
confirmedAt confirmedAt
confirmedBy confirmedBy
confirmedByUserName
updatedAt updatedAt
updatedBy updatedBy
updatedByUserName
contributionStatus contributionStatus
messagesCount messagesCount
deniedAt deniedAt
deniedBy deniedBy
deniedByUserName
deletedAt deletedAt
deletedBy deletedBy
deletedByUserName
moderatorId moderatorId
moderatorUserName
userId userId
resubmissionAt resubmissionAt
} }

View File

@ -16,6 +16,11 @@
"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}.",
"deletedBy": "Gelöscht von {name}.",
"deniedBy": "Abgelehnt von {name}."
},
"contributionLink": { "contributionLink": {
"amount": "Betrag", "amount": "Betrag",
"changeSaved": "Änderungen gespeichert", "changeSaved": "Änderungen gespeichert",
@ -171,10 +176,11 @@
"notice-tooltip": "Die Notiz ist nur für Moderatoren sichtbar", "notice-tooltip": "Die Notiz ist nur für Moderatoren sichtbar",
"memo": "Text ändern", "memo": "Text ändern",
"memo-tooltip": "Den Beitragstext bearbeiten", "memo-tooltip": "Den Beitragstext bearbeiten",
"memo-modified": "Text vom Moderator bearbeitet.", "memo-modified": "Text von {name} bearbeitet.",
"message": "Nachricht", "message": "Nachricht",
"message-tooltip": "Nachricht an Benutzer schreiben", "message-tooltip": "Nachricht an Benutzer schreiben",
"request": "Diese Nachricht ist nur für die Moderatoren sichtbar!" "request": "Diese Nachricht ist nur für die Moderatoren sichtbar!",
"who": "Wer?"
}, },
"name": "Name", "name": "Name",
"navbar": { "navbar": {

View File

@ -16,6 +16,11 @@
"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}.",
"deletedBy": "Deleted by {name}.",
"deniedBy": "Denied by {name}."
},
"contributionLink": { "contributionLink": {
"amount": "Amount", "amount": "Amount",
"changeSaved": "Changes saved", "changeSaved": "Changes saved",
@ -171,10 +176,11 @@
"notice-tooltip": "The note is only visible to moderators", "notice-tooltip": "The note is only visible to moderators",
"memo": "Edit text", "memo": "Edit text",
"memo-tooltip": "Edit the text of the contribution", "memo-tooltip": "Edit the text of the contribution",
"memo-modified": "Text edited by moderator", "memo-modified": "Text edited by {name}",
"message": "Message", "message": "Message",
"message-tooltip": "Write message to user", "message-tooltip": "Write message to user",
"request": "This message is only visible to the moderators!" "request": "This message is only visible to the moderators!",
"who": "Who?"
}, },
"name": "Name", "name": "Name",
"navbar": { "navbar": {

View File

@ -159,7 +159,11 @@ const baseFields = {
class: 'no-select', class: 'no-select',
formatter: formatDateOrDash, formatter: formatDateOrDash,
}, },
confirmedBy: { key: 'confirmedBy', label: t('moderator.moderator'), class: 'no-select' }, confirmedByUserName: {
key: 'confirmedByUserName',
label: t('moderator.who'),
class: 'no-select',
},
} }
const fields = computed( const fields = computed(
@ -174,7 +178,7 @@ const fields = computed(
baseFields.amount, baseFields.amount,
baseFields.memo, baseFields.memo,
baseFields.contributionDate, baseFields.contributionDate,
{ key: 'moderatorId', label: t('moderator.moderator'), class: 'no-select' }, { key: 'moderatorUserName', label: t('moderator.who'), class: 'no-select' },
{ key: 'editCreation', label: t('details') }, { key: 'editCreation', label: t('details') },
{ key: 'confirm', label: t('save') }, { key: 'confirm', label: t('save') },
], ],
@ -187,7 +191,7 @@ const fields = computed(
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
baseFields.confirmedAt, baseFields.confirmedAt,
baseFields.confirmedBy, baseFields.confirmedByUserName,
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// denied contributions // denied contributions
@ -203,7 +207,7 @@ const fields = computed(
label: t('contributions.denied'), label: t('contributions.denied'),
formatter: formatDateOrDash, formatter: formatDateOrDash,
}, },
{ key: 'deniedBy', label: t('moderator.moderator') }, { key: 'deniedByUserName', label: t('moderator.who') },
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// deleted contributions // deleted contributions
@ -219,7 +223,7 @@ const fields = computed(
label: t('contributions.deleted'), label: t('contributions.deleted'),
formatter: formatDateOrDash, formatter: formatDateOrDash,
}, },
{ key: 'deletedBy', label: t('moderator.moderator') }, { key: 'deletedByUserName', label: t('moderator.who') },
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
// all contributions // all contributions
@ -232,7 +236,7 @@ const fields = computed(
baseFields.contributionDate, baseFields.contributionDate,
baseFields.createdAt, baseFields.createdAt,
baseFields.confirmedAt, baseFields.confirmedAt,
baseFields.confirmedBy, baseFields.confirmedByUserName,
{ key: 'chatCreation', label: t('details') }, { key: 'chatCreation', label: t('details') },
], ],
][tabIndex.value], ][tabIndex.value],

View File

@ -60,6 +60,7 @@
"core": "*", "core": "*",
"cors": "^2.8.5", "cors": "^2.8.5",
"database": "*", "database": "*",
"dataloader": "^2.2.3",
"decimal.js-light": "^2.5.1", "decimal.js-light": "^2.5.1",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"esbuild": "^0.25.2", "esbuild": "^0.25.2",

View File

@ -1,6 +1,5 @@
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 +7,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
@ -24,30 +24,48 @@ export class Contribution extends UnconfirmedContribution {
@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
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
confirmedBy: number | null confirmedBy: number | null
@Field(() => String, { nullable: true })
confirmedByUserName?: string | null
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
deniedAt: Date | null deniedAt: Date | null
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
deniedBy: number | null deniedBy: number | null
@Field(() => String, { nullable: true })
deniedByUserName?: string | null
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
deletedAt: Date | null deletedAt: Date | null
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
deletedBy: number | null deletedBy: number | null
@Field(() => String, { nullable: true })
deletedByUserName?: string | null
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
updatedAt: Date | null updatedAt: Date | null
@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,9 +23,11 @@ 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'
import DataLoader from 'dataloader'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { GraphQLResolveInfo } from 'graphql' import { GraphQLResolveInfo } from 'graphql'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
@ -348,6 +350,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 +373,32 @@ export class ContributionResolver {
}, },
countOnly, countOnly,
) )
const result = new ContributionListResult(count, dbContributions)
return new ContributionListResult(count, dbContributions) const dataLoader = new DataLoader(async (userIds: readonly number[]) => {
const uniqueUserIds = new Set<number>()
userIds.forEach((userId) => uniqueUserIds.add(userId))
const users = await findUserNamesByIds(Array.from(uniqueUserIds))
return userIds.map((userId) => users.get(userId))
})
for (const contribution of result.contributionList) {
if (contribution.confirmedBy) {
contribution.confirmedByUserName = await dataLoader.load(contribution.confirmedBy)
}
if (contribution.updatedBy) {
contribution.updatedByUserName = await dataLoader.load(contribution.updatedBy)
}
if (contribution.moderatorId) {
contribution.moderatorUserName = await dataLoader.load(contribution.moderatorId)
}
if (contribution.deletedBy) {
contribution.deletedByUserName = await dataLoader.load(contribution.deletedBy)
}
if (contribution.deniedBy) {
contribution.deniedByUserName = await dataLoader.load(contribution.deniedBy)
}
}
return result
} }
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION]) @Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])

View File

@ -122,6 +122,7 @@
"core": "*", "core": "*",
"cors": "^2.8.5", "cors": "^2.8.5",
"database": "*", "database": "*",
"dataloader": "^2.2.3",
"decimal.js-light": "^2.5.1", "decimal.js-light": "^2.5.1",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"esbuild": "^0.25.2", "esbuild": "^0.25.2",
@ -1808,6 +1809,8 @@
"database": ["database@workspace:database"], "database": ["database@workspace:database"],
"dataloader": ["dataloader@2.2.3", "", {}, "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA=="],
"date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], "date-fns": ["date-fns@2.30.0", "", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="],
"date-format": ["date-format@4.0.14", "", {}, "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg=="], "date-format": ["date-format@4.0.14", "", {}, "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg=="],

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,19 @@ 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) => {
let name = `${user.firstName} ${user.lastName}`
if (user.alias && user.alias.length > 2) {
name = user.alias
}
return [user.id, name]
}),
)
}