Skip to content

Commit 2b9aee2

Browse files
committed
feat(degree-participations): add bulk actions for certificate management
- Add checkboxes and bulk action support to DegreeParticipationsTab - Implement 'Generate Achievement Certificates' bulk action - Implement 'Delete Achievement Certificates' bulk action - Create REMOVE_ACHIEVEMENT_CERTIFICATES GraphQL mutation - Migrate column sizing from legacy meta.width to modern size properties - Add user feedback via NotificationSnackbar for success/error states - Add translations for bulk actions (DE/EN) Breaking changes: None Closes: N/A
1 parent e533fb8 commit 2b9aee2

File tree

7 files changed

+189
-31
lines changed

7 files changed

+189
-31
lines changed

frontend-nx/apps/edu-hub/components/pages/ManageCourseContent/DegreeParticipationsTab/index.tsx

Lines changed: 122 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useMemo, useState } from 'react';
1+
import { FC, useCallback, useMemo, useState } from 'react';
22
import useTranslation from 'next-translate/useTranslation';
33
import { ColumnDef } from '@tanstack/react-table';
44
import { ManagedCourse_Course_by_pk } from '../../../../queries/__generated__/ManagedCourse';
@@ -8,6 +8,11 @@ import { DegreeParticipantsWithDegreeEnrollments_Course_by_pk_CourseEnrollments
88
import { DEGREE_PARTICIPANTS_WITH_DEGREE_ENROLLMENTS } from '../../../../queries/courseDegree';
99
import { CertificateDownload } from '../../../common/CertificateDownload';
1010
import { useTableGrid } from '../../../common/TableGrid/hooks';
11+
import { useRoleMutation } from '../../../../hooks/authedMutation';
12+
import { CREATE_CERTIFICATES } from '../../../../queries/actions';
13+
import { REMOVE_ACHIEVEMENT_CERTIFICATES } from '../../../../queries/courseEnrollment';
14+
import { BulkAction } from '../../../common/TableGrid/types';
15+
import NotificationSnackbar from '../../../common/dialogs/NotificationSnackbar';
1116

1217
interface DegreeParticipationsTabIProps {
1318
course: ManagedCourse_Course_by_pk;
@@ -25,12 +30,17 @@ export const DegreeParticipationsTab: FC<DegreeParticipationsTabIProps> = ({ cou
2530
const { t, lang } = useTranslation('manageCourse');
2631

2732
const [pageSize, setPageSize] = useState(20);
33+
const [snackbarOpen, setSnackbarOpen] = useState(false);
34+
const [snackbarMessage, setSnackbarMessage] = useState('');
2835

2936
const handlePageSizeChange = (newPageSize: number) => {
3037
setPageSize(newPageSize);
3138
setPageIndex(0);
3239
};
3340

41+
const [createCertificates] = useRoleMutation(CREATE_CERTIFICATES);
42+
const [removeAchievementCertificates] = useRoleMutation(REMOVE_ACHIEVEMENT_CERTIFICATES);
43+
3444
const { data, loading, error, pageIndex, setPageIndex, searchFilter, setSearchFilter } = useTableGrid({
3545
queryHook: useRoleQuery,
3646
query: DEGREE_PARTICIPANTS_WITH_DEGREE_ENROLLMENTS,
@@ -155,67 +165,141 @@ export const DegreeParticipationsTab: FC<DegreeParticipationsTabIProps> = ({ cou
155165
};
156166
});
157167

168+
// Bulk actions for certificate management
169+
const bulkActions: BulkAction[] = useMemo(
170+
() => [
171+
{
172+
value: 'generate-achievement-certificates',
173+
label: t('generate_achievement_certificates'),
174+
},
175+
{
176+
value: 'delete-achievement-certificates',
177+
label: t('delete_achievement_certificates'),
178+
},
179+
],
180+
[t]
181+
);
182+
183+
// Handle bulk actions
184+
const handleBulkAction = useCallback(
185+
async (action: string, selectedRows: ExtendedDegreeParticipantsEnrollment[]) => {
186+
if (action === 'generate-achievement-certificates') {
187+
try {
188+
const userIds = selectedRows.map((row) => row.User.id);
189+
190+
const response = await createCertificates({
191+
variables: {
192+
courseId: course.id,
193+
userIds,
194+
certificateType: 'achievement',
195+
},
196+
});
197+
198+
const result = response.data.createCertificates;
199+
200+
if (!result.success) {
201+
throw new Error(result.error || t(`errors:${result.messageKey}`));
202+
}
203+
204+
const certCount = result.count;
205+
const successTranslationKey =
206+
certCount <= 1
207+
? `course-page:${certCount === 0 ? 'no-' : '1-'}certificate-generated`
208+
: 'course-page:certificates-generated';
209+
210+
setSnackbarMessage(t(successTranslationKey, { number: certCount }));
211+
setSnackbarOpen(true);
212+
213+
// Refetch data to update the table
214+
setPageIndex(pageIndex); // This triggers a refetch via useTableGrid
215+
} catch (err) {
216+
console.error('Certificate generation error:', err);
217+
setSnackbarMessage(err.message || t('errors:certificate_generation_failed'));
218+
setSnackbarOpen(true);
219+
}
220+
} else if (action === 'delete-achievement-certificates') {
221+
try {
222+
const enrollmentIds = selectedRows.map((row) => row.id);
223+
224+
const response = await removeAchievementCertificates({
225+
variables: {
226+
enrollmentIds,
227+
},
228+
});
229+
230+
const affectedRows = response.data?.update_CourseEnrollment?.affected_rows || 0;
231+
232+
const successTranslationKey =
233+
affectedRows <= 1
234+
? affectedRows === 0
235+
? 'manageCourse:no_certificates_deleted'
236+
: 'manageCourse:certificate_deleted_singular'
237+
: 'manageCourse:certificates_deleted_plural';
238+
239+
setSnackbarMessage(t(successTranslationKey, { count: affectedRows }));
240+
setSnackbarOpen(true);
241+
242+
// Refetch data to update the table
243+
setPageIndex(pageIndex); // This triggers a refetch via useTableGrid
244+
} catch (err) {
245+
console.error('Certificate deletion error:', err);
246+
setSnackbarMessage(err.message || t('common:error_handling.certificate_deletion_failed'));
247+
setSnackbarOpen(true);
248+
}
249+
}
250+
},
251+
[course.id, createCertificates, removeAchievementCertificates, t, setPageIndex, pageIndex]
252+
);
253+
158254
const columns = useMemo<ColumnDef<ExtendedDegreeParticipantsEnrollment>[]>(
159255
() => [
160256
{
161257
header: t('name'),
162258
accessorKey: 'name',
163259
enableSorting: true,
164-
className: '',
165-
meta: {
166-
width: 3,
167-
},
260+
size: 200,
261+
minSize: 150,
168262
cell: ({ getValue }) => <div className="uppercase">{getValue<string>()}</div>,
169263
},
170264
{
171265
header: t('participations'),
172-
accessorKey: 'participations', // Use the flattened summary string
173-
meta: {
174-
width: 4,
175-
},
176-
cell: ({ getValue }) => <div style={{ whiteSpace: 'pre-line' }}>{getValue<string>()}</div>, // Display the summary string with multiline support
266+
accessorKey: 'participations',
267+
size: 400,
268+
minSize: 300,
269+
maxSize: 600,
270+
cell: ({ getValue }) => <div style={{ whiteSpace: 'pre-line' }}>{getValue<string>()}</div>,
177271
},
178272
{
179273
header: t('lastApplication'),
180274
accessorKey: 'lastApplication',
181-
meta: {
182-
className: 'text-center',
183-
width: 1,
184-
},
275+
size: 150,
276+
minSize: 120,
185277
},
186278
{
187279
header: t('status'),
188280
accessorKey: 'status',
189-
meta: {
190-
className: 'text-center',
191-
width: 1,
192-
},
281+
size: 120,
282+
minSize: 100,
193283
},
194284
{
195285
header: t('ectsTotal'),
196286
accessorKey: 'ectsTotal',
197-
meta: {
198-
className: 'text-center',
199-
width: 1,
200-
},
287+
size: 120,
288+
minSize: 100,
201289
enableSorting: true,
202290
},
203291
{
204292
header: t('attendedEvents'),
205293
accessorKey: 'attendedEvents',
206-
meta: {
207-
className: 'text-center',
208-
width: 1,
209-
},
294+
size: 150,
295+
minSize: 120,
210296
},
211297
{
212298
header: t('certificate'),
213299
accessorKey: 'certificate',
214300
accessorFn: (row) => row,
215-
meta: {
216-
className: 'text-center',
217-
width: 1,
218-
},
301+
size: 150,
302+
minSize: 120,
219303
cell: ({ getValue }) => (
220304
<div>
221305
<CertificateDownload courseEnrollment={getValue<ExtendedDegreeParticipantsEnrollment>()} manageView />
@@ -240,9 +324,16 @@ export const DegreeParticipationsTab: FC<DegreeParticipationsTabIProps> = ({ cou
240324
onSearchFilterChange={setSearchFilter}
241325
error={error}
242326
loading={loading}
243-
showCheckbox={false}
327+
showCheckbox={true}
328+
bulkActions={bulkActions}
329+
onBulkAction={handleBulkAction}
244330
refetchQueries={['DegreeParticipantsWithDegreeEnrollments']}
245331
/>
332+
<NotificationSnackbar
333+
open={snackbarOpen}
334+
onClose={() => setSnackbarOpen(false)}
335+
message={snackbarMessage}
336+
/>
246337
</>
247338
);
248339
};

frontend-nx/apps/edu-hub/locales/de/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
"delete_restricted_by_relationship": "{{parent}} können nicht gelöscht werden, wenn sie mit {{child}} verknüpft sind. Bitte lösche diese zuerst.",
196196
"delete_restricted_by_relationships": "{{table}} können nicht gelöscht werden, wenn sie mit anderen Einträgen verknüpft sind. Bitte lösche zuerst die anderen Einträge.",
197197
"generic_error": "Ein Fehler ist aufgetreten. Bitte versuche es erneut.",
198+
"certificate_deletion_failed": "Fehler beim Löschen der Leistungsnachweise. Bitte versuche es erneut.",
198199
"entities": {
199200
"course": "Kurse",
200201
"courseenrollment": "Anmeldungen",

frontend-nx/apps/edu-hub/locales/de/manageCourse.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
"certificate": "Zertifikat",
2828
"projectTitle": "Projekttitel",
2929
"lastRecordUpload": "Upload Datum",
30+
"generate_achievement_certificates": "Leistungsnachweise generieren",
31+
"delete_achievement_certificates": "Leistungsnachweise löschen",
32+
"no_certificates_deleted": "Es wurden keine Zertifikate gelöscht",
33+
"certificate_deleted_singular": "{{count}} Leistungsnachweis wurde gelöscht",
34+
"certificates_deleted_plural": "{{count}} Leistungsnachweise wurden gelöscht",
3035
"max_participants": {
3136
"label": "Max. Teilnehmendenanzahl",
3237
"placeholder": "Max. Teilnehmendenanzahl",

frontend-nx/apps/edu-hub/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
"delete_restricted_by_relationship": "{{parent}} cannot be deleted when they are still associated with {{child}}. Please remove all {{child}} first.",
196196
"delete_restricted_by_relationships": "{{table}} cannot be deleted when they are associated with other records. Please remove those records first.",
197197
"generic_error": "An error occurred. Please try again later.",
198+
"certificate_deletion_failed": "Failed to delete achievement certificates. Please try again.",
198199
"entities": {
199200
"course": "Courses",
200201
"courseenrollment": "Enrollments",

frontend-nx/apps/edu-hub/locales/en/manageCourse.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
"certificate": "Certificate",
2828
"projectTitle": "Project title",
2929
"lastRecordUpload": "Upload Date",
30+
"generate_achievement_certificates": "Generate Achievement Certificates",
31+
"delete_achievement_certificates": "Delete Achievement Certificates",
32+
"no_certificates_deleted": "No certificates were deleted",
33+
"certificate_deleted_singular": "{{count}} achievement certificate was deleted",
34+
"certificates_deleted_plural": "{{count}} achievement certificates were deleted",
3035
"max_participants": {
3136
"label": "Max. Teilnehmendenanzahl",
3237
"placeholder": "Max. Teilnehmendenanzahl",

frontend-nx/apps/edu-hub/queries/__generated__/RemoveAchievementCertificates.ts

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend-nx/apps/edu-hub/queries/courseEnrollment.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ export const UPDATE_ATTENDANCE = gql`
2222
}
2323
`;
2424

25+
export const REMOVE_ACHIEVEMENT_CERTIFICATES = gql`
26+
mutation RemoveAchievementCertificates($enrollmentIds: [Int!]!) {
27+
update_CourseEnrollment(
28+
where: { id: { _in: $enrollmentIds } }
29+
_set: { achievementCertificateURL: null }
30+
) {
31+
affected_rows
32+
returning {
33+
id
34+
achievementCertificateURL
35+
}
36+
}
37+
}
38+
`;
39+
2540
export const COURSE_ENROLLMENTS = gql`
2641
${COURSE_INSTRUCTOR_FRAGMENT}
2742
query CourseEnrollmentQuery(

0 commit comments

Comments
 (0)