@vicacha-devs/strapi-plugin-certificates 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +17 -2
  3. package/dist/admin/CertificatesPage-CFDIEBN_.js +783 -0
  4. package/dist/admin/CertificatesPage-DeGmoX7h.cjs +783 -0
  5. package/dist/admin/SettingsPage-CsWFO1XB.js +447 -0
  6. package/dist/admin/SettingsPage-D7Ob1gV0.cjs +447 -0
  7. package/dist/admin/ar-Bv3T0_7C.cjs +258 -0
  8. package/dist/admin/ar-CDFHNnFd.js +258 -0
  9. package/dist/admin/de-BjMxn-Lt.js +258 -0
  10. package/dist/admin/de-DB5eW-yI.cjs +258 -0
  11. package/dist/admin/en-BI5Z3lPW.js +258 -0
  12. package/dist/admin/en-CsYV0VDf.cjs +258 -0
  13. package/dist/admin/es-B7uy9a-2.js +258 -0
  14. package/dist/admin/es-DEp3j3kD.cjs +258 -0
  15. package/dist/admin/fr-D7y038Pm.cjs +258 -0
  16. package/dist/admin/fr-DxbtdEIA.js +258 -0
  17. package/dist/admin/id-DfW4ARVm.js +258 -0
  18. package/dist/admin/id-Wuw9jl-Y.cjs +258 -0
  19. package/dist/admin/index-BQdS3gc3.cjs +707 -0
  20. package/dist/admin/index-BYO34da4.js +708 -0
  21. package/dist/admin/index.cjs +5 -0
  22. package/dist/admin/index.js +5 -0
  23. package/dist/admin/index.mjs +5 -0
  24. package/dist/admin/pt-C0QaikKS.js +258 -0
  25. package/dist/admin/pt-Dj2oc3Ga.cjs +258 -0
  26. package/dist/admin/src/components/IssueCertificateModal.d.ts +9 -0
  27. package/dist/admin/src/components/SettingsTabHelpers.d.ts +13 -0
  28. package/dist/admin/src/index.d.ts +11 -0
  29. package/dist/admin/src/pages/CertificatesPage.d.ts +3 -0
  30. package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
  31. package/dist/admin/src/translations/cmLabels.d.ts +1 -0
  32. package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
  33. package/dist/admin/tr-BbeagXRY.js +258 -0
  34. package/dist/admin/tr-DlGl_XMF.cjs +258 -0
  35. package/dist/admin/ur-Bwn4TrjF.js +258 -0
  36. package/dist/admin/ur-qIlUtgqb.cjs +258 -0
  37. package/dist/admin/zh-Cmg5mEvP.js +258 -0
  38. package/dist/admin/zh-D-3QDsuQ.cjs +258 -0
  39. package/dist/package.json +3 -0
  40. package/dist/server/index.cjs +2282 -0
  41. package/dist/server/index.js +2282 -0
  42. package/dist/server/index.mjs +2282 -0
  43. package/dist/server/src/bootstrap.d.ts +5 -0
  44. package/dist/server/src/content-types/certificate/schema.json.d.ts +165 -0
  45. package/dist/server/src/content-types/index.d.ts +277 -0
  46. package/dist/server/src/content-types/template/schema.json.d.ts +118 -0
  47. package/dist/server/src/controllers/admin.d.ts +31 -0
  48. package/dist/server/src/controllers/content-api.d.ts +28 -0
  49. package/dist/server/src/controllers/index.d.ts +30 -0
  50. package/dist/server/src/index.d.ts +413 -0
  51. package/dist/server/src/register.d.ts +5 -0
  52. package/dist/server/src/routes/admin.d.ts +12 -0
  53. package/dist/server/src/routes/content-api.d.ts +12 -0
  54. package/dist/server/src/routes/index.d.ts +25 -0
  55. package/dist/server/src/services/docusign.d.ts +76 -0
  56. package/dist/server/src/services/index.d.ts +76 -0
  57. package/dist/server/src/services/issuance.d.ts +54 -0
  58. package/dist/server/src/services/issuanceHelpers.d.ts +47 -0
  59. package/dist/server/src/services/settings.d.ts +17 -0
  60. package/dist/server/src/services/urlHelpers.d.ts +2 -0
  61. package/dist/shared/calendarSystems.d.ts +3 -0
  62. package/dist/shared/cryptoEngine.d.ts +27 -0
  63. package/dist/shared/documentRenderer.d.ts +12 -0
  64. package/dist/shared/index.cjs +636 -0
  65. package/dist/shared/index.d.ts +5 -0
  66. package/dist/shared/index.js +613 -0
  67. package/dist/shared/index.mjs +613 -0
  68. package/dist/shared/pluginId.d.ts +2 -0
  69. package/dist/shared/types.d.ts +194 -0
  70. package/package.json +112 -3
@@ -0,0 +1,783 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { Box, Flex, Typography, Button, Field, SingleSelect, SingleSelectOption, TextInput, Textarea, Checkbox, Badge } from "@strapi/design-system";
3
+ import { useFetchClient, useNotification, Layouts } from "@strapi/strapi/admin";
4
+ import { useState, useCallback, useEffect } from "react";
5
+ import { useIntl } from "react-intl";
6
+ import { P as PLUGIN_ID } from "./index-BYO34da4.js";
7
+ function trimOrUndefined(value) {
8
+ const trimmed = value.trim();
9
+ return trimmed.length > 0 ? trimmed : void 0;
10
+ }
11
+ function buildWitnessEntry(name, docId, email, role) {
12
+ const fullName = trimOrUndefined(name);
13
+ if (!fullName) return null;
14
+ return {
15
+ fullName,
16
+ documentIdRaw: trimOrUndefined(docId),
17
+ email: trimOrUndefined(email),
18
+ routingOrder: 1,
19
+ role
20
+ };
21
+ }
22
+ const IssueCertificateModal = ({
23
+ templates,
24
+ onClose,
25
+ onSubmit
26
+ }) => {
27
+ const { formatMessage } = useIntl();
28
+ const t = (id, defaultMessage, values) => formatMessage({ id: `${"certificates"}.${id}`, defaultMessage }, values);
29
+ const [templateSlug, setTemplateSlug] = useState(
30
+ templates[0]?.slug || ""
31
+ );
32
+ const [recipientFullName, setRecipientFullName] = useState("");
33
+ const [recipientChosenName, setRecipientChosenName] = useState("");
34
+ const [recipientEmail, setRecipientEmail] = useState("");
35
+ const [recipientPhone, _setRecipientPhone] = useState("");
36
+ const [recipientNationality, setRecipientNationality] = useState("");
37
+ const [nationalIdRaw, setNationalIdRaw] = useState("");
38
+ const [witness1Name, setWitness1Name] = useState("");
39
+ const [witness1Id, setWitness1Id] = useState("");
40
+ const [witness1Email, setWitness1Email] = useState("");
41
+ const [witness2Name, setWitness2Name] = useState("");
42
+ const [witness2Id, setWitness2Id] = useState("");
43
+ const [witness2Email, setWitness2Email] = useState("");
44
+ const [linkedBookingCode, setLinkedBookingCode] = useState("");
45
+ const [notes, setNotes] = useState("");
46
+ const [autoApprove, setAutoApprove] = useState(true);
47
+ const [sendViaESign, setSendViaESign] = useState(false);
48
+ const [submitting, setSubmitting] = useState(false);
49
+ const selectedTemplate = templates.find((tpl) => tpl.slug === templateSlug);
50
+ const handleCreate = async () => {
51
+ const cleanName = trimOrUndefined(recipientFullName);
52
+ if (!cleanName || !templateSlug) return;
53
+ setSubmitting(true);
54
+ try {
55
+ const w1 = buildWitnessEntry(
56
+ witness1Name,
57
+ witness1Id,
58
+ witness1Email,
59
+ "Official Witness 1"
60
+ );
61
+ const w2 = buildWitnessEntry(
62
+ witness2Name,
63
+ witness2Id,
64
+ witness2Email,
65
+ "Official Witness 2"
66
+ );
67
+ const witnesses = [w1, w2].filter(
68
+ (w) => w !== null
69
+ );
70
+ await onSubmit({
71
+ templateSlug,
72
+ recipientFullName: cleanName,
73
+ recipientChosenName: trimOrUndefined(recipientChosenName),
74
+ recipientEmail: trimOrUndefined(recipientEmail),
75
+ recipientPhone: trimOrUndefined(recipientPhone),
76
+ recipientNationality: trimOrUndefined(recipientNationality),
77
+ nationalIdRaw: trimOrUndefined(nationalIdRaw),
78
+ witnesses,
79
+ linkedBookingCode: trimOrUndefined(linkedBookingCode),
80
+ notes: trimOrUndefined(notes),
81
+ autoApprove: !sendViaESign && autoApprove,
82
+ sendViaESign
83
+ });
84
+ } finally {
85
+ setSubmitting(false);
86
+ }
87
+ };
88
+ return /* @__PURE__ */ jsxs(
89
+ Box,
90
+ {
91
+ padding: 6,
92
+ background: "neutral0",
93
+ hasRadius: true,
94
+ shadow: "filterShadow",
95
+ borderColor: "neutral200",
96
+ marginBottom: 6,
97
+ children: [
98
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, children: [
99
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", fontWeight: "bold", children: t(
100
+ "modal.title_issue_official_signed_certificate",
101
+ "Issue Official Signed Certificate"
102
+ ) }),
103
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: onClose, children: t("modal.close", "Close") })
104
+ ] }),
105
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, alignItems: "stretch", children: [
106
+ /* @__PURE__ */ jsxs(Field.Root, { required: true, children: [
107
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.certificate_template", "Certificate Template") }),
108
+ /* @__PURE__ */ jsx(
109
+ SingleSelect,
110
+ {
111
+ placeholder: t(
112
+ "placeholder.select_certificate_template",
113
+ "Select certificate template"
114
+ ),
115
+ value: templateSlug,
116
+ onChange: (val) => setTemplateSlug(String(val)),
117
+ children: templates.map((tpl) => /* @__PURE__ */ jsxs(SingleSelectOption, { value: tpl.slug, children: [
118
+ tpl.name,
119
+ " (",
120
+ tpl.serialPrefix,
121
+ ")"
122
+ ] }, tpl.slug))
123
+ }
124
+ )
125
+ ] }),
126
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", children: [
127
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 240 }, children: /* @__PURE__ */ jsxs(Field.Root, { required: true, children: [
128
+ /* @__PURE__ */ jsx(Field.Label, { children: t(
129
+ "field.recipient_full_legal_name",
130
+ "Recipient Full Legal Name"
131
+ ) }),
132
+ /* @__PURE__ */ jsx(
133
+ TextInput,
134
+ {
135
+ value: recipientFullName,
136
+ onChange: (e) => setRecipientFullName(e.target.value),
137
+ placeholder: t(
138
+ "placeholder.e_g_carlos_andres_perez",
139
+ "e.g. Carlos Andres Perez"
140
+ )
141
+ }
142
+ )
143
+ ] }) }),
144
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 240 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
145
+ /* @__PURE__ */ jsx(Field.Label, { children: t(
146
+ "field.chosen_name_honorific_title_optional",
147
+ "Chosen Name / Honorific / Title (Optional)"
148
+ ) }),
149
+ /* @__PURE__ */ jsx(
150
+ TextInput,
151
+ {
152
+ value: recipientChosenName,
153
+ onChange: (e) => setRecipientChosenName(e.target.value),
154
+ placeholder: t(
155
+ "placeholder.e_g_ibrahim_chosen_muslim_name",
156
+ "e.g. Ibrahim / Chosen Muslim Name"
157
+ )
158
+ }
159
+ )
160
+ ] }) })
161
+ ] }),
162
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", children: [
163
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
164
+ /* @__PURE__ */ jsx(Field.Label, { children: t(
165
+ "field.national_id_passport_hashed_sha_256_at_rest",
166
+ "National ID / Passport (Hashed SHA-256 at rest)"
167
+ ) }),
168
+ /* @__PURE__ */ jsx(
169
+ TextInput,
170
+ {
171
+ value: nationalIdRaw,
172
+ onChange: (e) => setNationalIdRaw(e.target.value),
173
+ placeholder: t(
174
+ "placeholder.e_g_cc_1020304050",
175
+ "e.g. CC 1020304050"
176
+ )
177
+ }
178
+ )
179
+ ] }) }),
180
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
181
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.nationality", "Nationality") }),
182
+ /* @__PURE__ */ jsx(
183
+ TextInput,
184
+ {
185
+ value: recipientNationality,
186
+ onChange: (e) => setRecipientNationality(e.target.value),
187
+ placeholder: t("placeholder.e_g_colombian", "e.g. Colombian")
188
+ }
189
+ )
190
+ ] }) }),
191
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 200 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
192
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.recipient_email", "Recipient Email") }),
193
+ /* @__PURE__ */ jsx(
194
+ TextInput,
195
+ {
196
+ value: recipientEmail,
197
+ onChange: (e) => setRecipientEmail(e.target.value),
198
+ placeholder: t(
199
+ "placeholder.recipient_example_org",
200
+ "recipient@example.org"
201
+ )
202
+ }
203
+ )
204
+ ] }) }),
205
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
206
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.linked_booking_code", "Linked Booking Code") }),
207
+ /* @__PURE__ */ jsx(
208
+ TextInput,
209
+ {
210
+ value: linkedBookingCode,
211
+ onChange: (e) => setLinkedBookingCode(e.target.value),
212
+ placeholder: t(
213
+ "placeholder.e_g_bk_a1b2c3d4",
214
+ "e.g. BK-A1B2C3D4"
215
+ )
216
+ }
217
+ )
218
+ ] }) })
219
+ ] }),
220
+ /* @__PURE__ */ jsxs(
221
+ Box,
222
+ {
223
+ padding: 4,
224
+ background: "neutral100",
225
+ hasRadius: true,
226
+ borderColor: "neutral200",
227
+ style: { border: "1px solid rgba(128, 128, 128, 0.18)" },
228
+ children: [
229
+ /* @__PURE__ */ jsxs(Typography, { variant: "omega", fontWeight: "bold", children: [
230
+ t(
231
+ "modal.official_witnesses_attestation",
232
+ "Official Witnesses Attestation"
233
+ ),
234
+ " ",
235
+ selectedTemplate?.requiresWitnesses ? t(
236
+ "modal.minimum_witnesses_required",
237
+ "(Minimum {count} required)",
238
+ {
239
+ count: selectedTemplate.minWitnesses
240
+ }
241
+ ) : t("modal.optional_suffix", "(Optional)")
242
+ ] }),
243
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", marginTop: 3, children: [
244
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
245
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.witness_1_full_name", "Witness 1 Full Name") }),
246
+ /* @__PURE__ */ jsx(
247
+ TextInput,
248
+ {
249
+ value: witness1Name,
250
+ onChange: (e) => setWitness1Name(e.target.value),
251
+ placeholder: t(
252
+ "placeholder.e_g_ahmad_al_mansour",
253
+ "e.g., Ahmad Al-Mansour"
254
+ )
255
+ }
256
+ )
257
+ ] }) }),
258
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 150 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
259
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.witness_1_id_document", "Witness 1 ID Document") }),
260
+ /* @__PURE__ */ jsx(
261
+ TextInput,
262
+ {
263
+ value: witness1Id,
264
+ onChange: (e) => setWitness1Id(e.target.value),
265
+ placeholder: t(
266
+ "placeholder.e_g_cc_79123456_masked",
267
+ "e.g., CC 79123456 (masked)"
268
+ )
269
+ }
270
+ )
271
+ ] }) }),
272
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
273
+ /* @__PURE__ */ jsx(Field.Label, { children: t(
274
+ "field.witness_1_email_docusign",
275
+ "Witness 1 Email (DocuSign)"
276
+ ) }),
277
+ /* @__PURE__ */ jsx(
278
+ TextInput,
279
+ {
280
+ value: witness1Email,
281
+ onChange: (e) => setWitness1Email(e.target.value),
282
+ placeholder: t(
283
+ "placeholder.e_g_witness1_example_org",
284
+ "e.g., witness1@example.org"
285
+ )
286
+ }
287
+ )
288
+ ] }) })
289
+ ] }),
290
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, wrap: "wrap", marginTop: 3, children: [
291
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
292
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.witness_2_full_name", "Witness 2 Full Name") }),
293
+ /* @__PURE__ */ jsx(
294
+ TextInput,
295
+ {
296
+ value: witness2Name,
297
+ onChange: (e) => setWitness2Name(e.target.value),
298
+ placeholder: t(
299
+ "placeholder.e_g_yusuf_benavides",
300
+ "e.g., Yusuf Benavides"
301
+ )
302
+ }
303
+ )
304
+ ] }) }),
305
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 150 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
306
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.witness_2_id_document", "Witness 2 ID Document") }),
307
+ /* @__PURE__ */ jsx(
308
+ TextInput,
309
+ {
310
+ value: witness2Id,
311
+ onChange: (e) => setWitness2Id(e.target.value),
312
+ placeholder: t(
313
+ "placeholder.e_g_cc_80234567_masked",
314
+ "e.g., CC 80234567 (masked)"
315
+ )
316
+ }
317
+ )
318
+ ] }) }),
319
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1, minWidth: 180 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
320
+ /* @__PURE__ */ jsx(Field.Label, { children: t(
321
+ "field.witness_2_email_docusign",
322
+ "Witness 2 Email (DocuSign)"
323
+ ) }),
324
+ /* @__PURE__ */ jsx(
325
+ TextInput,
326
+ {
327
+ value: witness2Email,
328
+ onChange: (e) => setWitness2Email(e.target.value),
329
+ placeholder: t(
330
+ "placeholder.e_g_witness2_example_org",
331
+ "e.g., witness2@example.org"
332
+ )
333
+ }
334
+ )
335
+ ] }) })
336
+ ] })
337
+ ]
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsxs(Field.Root, { children: [
341
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.internal_registry_notes", "Internal Registry Notes") }),
342
+ /* @__PURE__ */ jsx(
343
+ Textarea,
344
+ {
345
+ placeholder: t(
346
+ "placeholder.attestation_context_officiating_imam_or_ceremony",
347
+ "Attestation context, officiating Imam, or ceremony notes..."
348
+ ),
349
+ value: notes,
350
+ onChange: (e) => setNotes(e.target.value)
351
+ }
352
+ )
353
+ ] }),
354
+ /* @__PURE__ */ jsx(
355
+ Checkbox,
356
+ {
357
+ checked: autoApprove && !sendViaESign,
358
+ disabled: sendViaESign,
359
+ onCheckedChange: (val) => setAutoApprove(Boolean(val)),
360
+ children: t(
361
+ "checkbox.immediately_sign_with_ed25519",
362
+ "Immediately sign with Ed25519 private key and mark as Issued"
363
+ )
364
+ }
365
+ ),
366
+ /* @__PURE__ */ jsx(
367
+ Checkbox,
368
+ {
369
+ checked: sendViaESign,
370
+ onCheckedChange: (val) => setSendViaESign(Boolean(val)),
371
+ children: t(
372
+ "checkbox.dispatch_docusign_esignature_envelope",
373
+ "Dispatch DocuSign eSignature Envelope to Witnesses & Signatories first (auto-approves on completion webhook)"
374
+ )
375
+ }
376
+ ),
377
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "flex-end", gap: 3, children: [
378
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: onClose, children: t("modal.cancel", "Cancel") }),
379
+ /* @__PURE__ */ jsx(
380
+ Button,
381
+ {
382
+ onClick: handleCreate,
383
+ loading: submitting,
384
+ disabled: !recipientFullName.trim() || !templateSlug,
385
+ children: sendViaESign ? t(
386
+ "modal.create_and_send_docusign",
387
+ "Create & Send via DocuSign"
388
+ ) : t(
389
+ "modal.sign_and_issue_certificate",
390
+ "Sign & Issue Certificate"
391
+ )
392
+ }
393
+ )
394
+ ] })
395
+ ] })
396
+ ]
397
+ }
398
+ );
399
+ };
400
+ const CertificatesPage = () => {
401
+ const { formatMessage } = useIntl();
402
+ const t = (id, defaultMessage, values) => formatMessage({ id: `${PLUGIN_ID}.${id}`, defaultMessage }, values);
403
+ const { get, post, put } = useFetchClient();
404
+ const { toggleNotification } = useNotification();
405
+ const [records, setRecords] = useState([]);
406
+ const [templates, setTemplates] = useState([]);
407
+ const [stats, setStats] = useState({
408
+ total: 0,
409
+ issued: 0,
410
+ pending_approval: 0,
411
+ revoked: 0,
412
+ expired: 0
413
+ });
414
+ const [statusFilter, setStatusFilter] = useState("all");
415
+ const [searchQuery, setSearchQuery] = useState("");
416
+ const [showIssueModal, setShowIssueModal] = useState(false);
417
+ const [loading, setLoading] = useState(true);
418
+ const loadData = useCallback(async () => {
419
+ setLoading(true);
420
+ try {
421
+ const queryParams = new URLSearchParams();
422
+ if (statusFilter && statusFilter !== "all") {
423
+ queryParams.set("status", statusFilter);
424
+ }
425
+ if (searchQuery.trim()) {
426
+ queryParams.set("search", searchQuery.trim());
427
+ }
428
+ const [certRes, tplRes] = await Promise.all([
429
+ get(
430
+ `/${PLUGIN_ID}/certificates?${queryParams.toString()}`
431
+ ),
432
+ get(`/${PLUGIN_ID}/templates`)
433
+ ]);
434
+ setRecords(certRes.data?.data || []);
435
+ if (certRes.data?.meta?.stats) {
436
+ setStats(certRes.data.meta.stats);
437
+ }
438
+ setTemplates(tplRes.data?.data || []);
439
+ } catch {
440
+ toggleNotification({
441
+ type: "danger",
442
+ message: t(
443
+ "page.ui.msg_failed_to_load_certificates_registry",
444
+ "Failed to load certificates registry"
445
+ )
446
+ });
447
+ } finally {
448
+ setLoading(false);
449
+ }
450
+ }, [get, searchQuery, statusFilter, toggleNotification]);
451
+ useEffect(() => {
452
+ void loadData();
453
+ }, [loadData]);
454
+ const handleIssue = async (payload) => {
455
+ try {
456
+ const res = await post(
457
+ `/${PLUGIN_ID}/certificates/issue`,
458
+ payload
459
+ );
460
+ toggleNotification({
461
+ type: "success",
462
+ message: `Certificate ${res.data?.data?.serialNumber} created`
463
+ });
464
+ setShowIssueModal(false);
465
+ await loadData();
466
+ } catch (err) {
467
+ toggleNotification({
468
+ type: "danger",
469
+ message: err.message || "Failed to issue certificate"
470
+ });
471
+ }
472
+ };
473
+ const handleSendDocuSign = async (documentId) => {
474
+ try {
475
+ const res = await post(
476
+ `/${PLUGIN_ID}/certificates/${documentId}/send-docusign`,
477
+ {}
478
+ );
479
+ toggleNotification({
480
+ type: "success",
481
+ message: `Dispatched DocuSign envelope ${res.data?.data?.envelopeId}`
482
+ });
483
+ await loadData();
484
+ } catch (err) {
485
+ toggleNotification({
486
+ type: "danger",
487
+ message: err.message || "Failed to send DocuSign eSignature envelope"
488
+ });
489
+ }
490
+ };
491
+ const handleApprove = async (documentId) => {
492
+ try {
493
+ await put(`/${PLUGIN_ID}/certificates/${documentId}/approve`, {});
494
+ toggleNotification({
495
+ type: "success",
496
+ message: t(
497
+ "page.ui.msg_certificate_approved_and_signed_with_ed25519",
498
+ "Certificate approved and signed with Ed25519"
499
+ )
500
+ });
501
+ await loadData();
502
+ } catch (err) {
503
+ toggleNotification({
504
+ type: "danger",
505
+ message: err.message || "Failed to approve certificate"
506
+ });
507
+ }
508
+ };
509
+ const handleRevoke = async (documentId) => {
510
+ try {
511
+ await put(`/${PLUGIN_ID}/certificates/${documentId}/revoke`, {
512
+ reason: "Revoked via Admin Registry"
513
+ });
514
+ toggleNotification({
515
+ type: "warning",
516
+ message: t("page.ui.msg_certificate_revoked", "Certificate revoked")
517
+ });
518
+ await loadData();
519
+ } catch {
520
+ toggleNotification({
521
+ type: "danger",
522
+ message: t(
523
+ "page.ui.msg_failed_to_revoke_certificate",
524
+ "Failed to revoke certificate"
525
+ )
526
+ });
527
+ }
528
+ };
529
+ const handleOpenPrintable = async (documentId) => {
530
+ try {
531
+ const res = await get(
532
+ `/${PLUGIN_ID}/certificates/${documentId}/print`
533
+ );
534
+ const htmlContent = typeof res.data === "string" ? res.data : String(res.data || "");
535
+ const blob = new Blob([htmlContent], { type: "text/html;charset=utf-8" });
536
+ const blobUrl = URL.createObjectURL(blob);
537
+ window.open(blobUrl, "_blank", "noopener,noreferrer");
538
+ } catch {
539
+ toggleNotification({
540
+ type: "danger",
541
+ message: t(
542
+ "page.ui.msg_unable_to_render_printable_certificate_document",
543
+ "Unable to render printable certificate document"
544
+ )
545
+ });
546
+ }
547
+ };
548
+ return /* @__PURE__ */ jsxs(Layouts.Root, { children: [
549
+ /* @__PURE__ */ jsx(
550
+ Layouts.Header,
551
+ {
552
+ title: t(
553
+ "page.ui.title_official_certificates_cryptographic_registry",
554
+ "Official Certificates & Cryptographic Registry"
555
+ ),
556
+ subtitle: t(
557
+ "page.ui.subtitle_ed25519_digitally_signed_certificates_optional_d",
558
+ "Ed25519 digitally signed certificates, optional DocuSign eSignature envelopes, witness attestation, and QR verification"
559
+ ),
560
+ primaryAction: /* @__PURE__ */ jsx(Button, { onClick: () => setShowIssueModal(true), children: t(
561
+ "page.ui.issue_signed_certificate",
562
+ "+ Issue Signed Certificate"
563
+ ) })
564
+ }
565
+ ),
566
+ /* @__PURE__ */ jsxs(Layouts.Content, { children: [
567
+ /* @__PURE__ */ jsxs(Flex, { gap: 4, marginBottom: 6, wrap: "wrap", children: [
568
+ /* @__PURE__ */ jsxs(
569
+ Box,
570
+ {
571
+ padding: 4,
572
+ background: "neutral0",
573
+ hasRadius: true,
574
+ shadow: "filterShadow",
575
+ style: { flex: 1, minWidth: 160 },
576
+ children: [
577
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: t("page.ui.total_certificates", "Total Certificates") }),
578
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "alpha", fontWeight: "bold", children: stats.total }) })
579
+ ]
580
+ }
581
+ ),
582
+ /* @__PURE__ */ jsxs(
583
+ Box,
584
+ {
585
+ padding: 4,
586
+ background: "neutral0",
587
+ hasRadius: true,
588
+ shadow: "filterShadow",
589
+ style: { flex: 1, minWidth: 160 },
590
+ children: [
591
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "success600", children: t("page.ui.issued_signed", "Issued & Signed") }),
592
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "alpha", fontWeight: "bold", children: stats.issued }) })
593
+ ]
594
+ }
595
+ ),
596
+ /* @__PURE__ */ jsxs(
597
+ Box,
598
+ {
599
+ padding: 4,
600
+ background: "neutral0",
601
+ hasRadius: true,
602
+ shadow: "filterShadow",
603
+ style: { flex: 1, minWidth: 160 },
604
+ children: [
605
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "warning600", children: t("page.ui.pending_approval", "Pending Approval") }),
606
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "alpha", fontWeight: "bold", children: stats.pending_approval }) })
607
+ ]
608
+ }
609
+ ),
610
+ /* @__PURE__ */ jsxs(
611
+ Box,
612
+ {
613
+ padding: 4,
614
+ background: "neutral0",
615
+ hasRadius: true,
616
+ shadow: "filterShadow",
617
+ style: { flex: 1, minWidth: 160 },
618
+ children: [
619
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "danger600", children: t("page.ui.revoked_expired", "Revoked / Expired") }),
620
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Typography, { variant: "alpha", fontWeight: "bold", children: stats.revoked + stats.expired }) })
621
+ ]
622
+ }
623
+ )
624
+ ] }),
625
+ showIssueModal && /* @__PURE__ */ jsx(
626
+ IssueCertificateModal,
627
+ {
628
+ templates,
629
+ onClose: () => setShowIssueModal(false),
630
+ onSubmit: handleIssue
631
+ }
632
+ ),
633
+ /* @__PURE__ */ jsx(
634
+ Box,
635
+ {
636
+ padding: 4,
637
+ background: "neutral0",
638
+ hasRadius: true,
639
+ shadow: "filterShadow",
640
+ marginBottom: 4,
641
+ children: /* @__PURE__ */ jsxs(Flex, { gap: 4, alignItems: "flex-end", wrap: "wrap", children: [
642
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 260, flex: 1 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
643
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.search_certificates", "Search Certificates") }),
644
+ /* @__PURE__ */ jsx(
645
+ TextInput,
646
+ {
647
+ placeholder: t(
648
+ "placeholder.search_by_serial_number_recipient_name_chosen_na",
649
+ "Search by serial number, recipient name, chosen name, or email..."
650
+ ),
651
+ value: searchQuery,
652
+ onChange: (e) => setSearchQuery(e.target.value)
653
+ }
654
+ )
655
+ ] }) }),
656
+ /* @__PURE__ */ jsx(Box, { style: { minWidth: 200 }, children: /* @__PURE__ */ jsxs(Field.Root, { children: [
657
+ /* @__PURE__ */ jsx(Field.Label, { children: t("field.certificate_status", "Certificate Status") }),
658
+ /* @__PURE__ */ jsxs(
659
+ SingleSelect,
660
+ {
661
+ placeholder: t("placeholder.all_statuses", "All Statuses"),
662
+ value: statusFilter,
663
+ onChange: (val) => setStatusFilter(String(val)),
664
+ children: [
665
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "all", children: t("page.ui.all_statuses", "All Statuses") }),
666
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "issued", children: t("page.ui.issued", "Issued") }),
667
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "pending_approval", children: t("page.ui.pending_approval", "Pending Approval") }),
668
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "revoked", children: t("page.ui.revoked", "Revoked") }),
669
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "expired", children: t("page.ui.expired", "Expired") })
670
+ ]
671
+ }
672
+ )
673
+ ] }) }),
674
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => void loadData(), children: t("page.ui.refresh", "Refresh") })
675
+ ] })
676
+ }
677
+ ),
678
+ /* @__PURE__ */ jsx(Box, { padding: 4, background: "neutral0", hasRadius: true, shadow: "filterShadow", children: loading ? /* @__PURE__ */ jsx(Typography, { children: t(
679
+ "page.ui.loading_official_certificates_registry",
680
+ "Loading official certificates registry..."
681
+ ) }) : records.length === 0 ? /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: t(
682
+ "page.ui.no_certificates_found_matching_the_current_filte",
683
+ "No certificates found matching the current filters."
684
+ ) }) : /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 3, alignItems: "stretch", children: records.map((rec) => {
685
+ const secondaryText = Object.values(rec.secondaryDates || {}).map((d) => `${d.label}: ${d.compact || d.formatted}`).join(" • ");
686
+ return /* @__PURE__ */ jsx(
687
+ Box,
688
+ {
689
+ padding: 4,
690
+ background: "neutral100",
691
+ hasRadius: true,
692
+ borderColor: "neutral200",
693
+ children: /* @__PURE__ */ jsxs(
694
+ Flex,
695
+ {
696
+ justifyContent: "space-between",
697
+ alignItems: "center",
698
+ wrap: "wrap",
699
+ gap: 3,
700
+ children: [
701
+ /* @__PURE__ */ jsxs(Box, { children: [
702
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", wrap: "wrap", children: [
703
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", variant: "omega", children: rec.serialNumber }),
704
+ /* @__PURE__ */ jsx(Badge, { children: rec.status.toUpperCase() }),
705
+ rec.esignEnvelopeId && /* @__PURE__ */ jsx(Badge, { children: t(
706
+ "page.ui.docusign_badge",
707
+ "DOCUSIGN: {status}",
708
+ {
709
+ status: (rec.esignEnvelopeStatus || "sent").toUpperCase()
710
+ }
711
+ ) }),
712
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: rec.templateName })
713
+ ] }),
714
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Typography, { fontWeight: "semiBold", children: [
715
+ rec.recipientFullName,
716
+ rec.recipientChosenName ? ` (${rec.recipientChosenName})` : ""
717
+ ] }) }),
718
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Typography, { variant: "pi", textColor: "neutral600", children: [
719
+ t("page.ui.issued_date", "Issued: {date}", {
720
+ date: rec.issuedAt.slice(0, 10)
721
+ }),
722
+ secondaryText ? ` • ${secondaryText}` : "",
723
+ rec.nationalIdMasked ? ` • ${t("page.ui.id_masked", "ID: {id}", {
724
+ id: rec.nationalIdMasked
725
+ })}` : "",
726
+ " • ",
727
+ t(
728
+ "page.ui.witnesses_count",
729
+ "Witnesses: {count}",
730
+ {
731
+ count: (rec.witnesses || []).length
732
+ }
733
+ )
734
+ ] }) })
735
+ ] }),
736
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
737
+ /* @__PURE__ */ jsx(
738
+ Button,
739
+ {
740
+ variant: "secondary",
741
+ onClick: () => void handleOpenPrintable(rec.documentId),
742
+ children: t("page.ui.print_pdf", "Print / PDF")
743
+ }
744
+ ),
745
+ rec.status === "pending_approval" && /* @__PURE__ */ jsxs(Fragment, { children: [
746
+ /* @__PURE__ */ jsx(
747
+ Button,
748
+ {
749
+ variant: "tertiary",
750
+ onClick: () => void handleSendDocuSign(rec.documentId),
751
+ children: t("page.ui.send_docusign", "Send DocuSign")
752
+ }
753
+ ),
754
+ /* @__PURE__ */ jsx(
755
+ Button,
756
+ {
757
+ onClick: () => void handleApprove(rec.documentId),
758
+ children: t("page.ui.approve_sign", "Approve & Sign")
759
+ }
760
+ )
761
+ ] }),
762
+ rec.status === "issued" && /* @__PURE__ */ jsx(
763
+ Button,
764
+ {
765
+ variant: "danger-light",
766
+ onClick: () => void handleRevoke(rec.documentId),
767
+ children: t("page.ui.revoke", "Revoke")
768
+ }
769
+ )
770
+ ] })
771
+ ]
772
+ }
773
+ )
774
+ },
775
+ rec.documentId
776
+ );
777
+ }) }) })
778
+ ] })
779
+ ] });
780
+ };
781
+ export {
782
+ CertificatesPage
783
+ };