@7365admin1/layer-common 4.96.0 → 4.97.1-staging.500

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 (65) hide show
  1. package/CHANGELOG.md +10 -4
  2. package/assets/css/primitives.css +57 -0
  3. package/assets/css/screens.css +8 -4
  4. package/components/AccessCardDetailsDialog.vue +59 -6
  5. package/components/AccessCardPreviewDialog.vue +371 -53
  6. package/components/AppSelect.vue +143 -4
  7. package/components/BuildingUnitFormEdit.vue +6 -0
  8. package/components/BuildingUnitManagement.vue +298 -0
  9. package/components/Dialog/UpdateMoreAction.vue +11 -1
  10. package/components/DocumentForm.vue +407 -112
  11. package/components/DocumentManagement.vue +213 -12
  12. package/components/Facility/BookingSetup.vue +63 -0
  13. package/components/HidAccessLogDashboard.vue +74 -17
  14. package/components/HidAccessPermissions.vue +424 -0
  15. package/components/HidIntercomManagement.vue +183 -14
  16. package/components/HidProfileQrCode.vue +332 -0
  17. package/components/HidQrCodeConfiguration.vue +17 -195
  18. package/components/HidReaderManagement.vue +398 -0
  19. package/components/HidReaderUserRoster.vue +44 -3
  20. package/components/HidUserEnrollment.vue +2239 -276
  21. package/components/InvitationClientForm.vue +19 -1
  22. package/components/Nfc/NFCPatrolRouteForm.vue +109 -58
  23. package/components/Nfc/NFCPatrolRouteMain.vue +3 -18
  24. package/components/Nfc/PatrolReport/PatrolActivityTable.vue +0 -8
  25. package/components/Nfc/PatrolReport/PatrolRouteSummary.vue +1 -1
  26. package/components/ServiceProviderMain.vue +48 -0
  27. package/components/SiteProviderChange.vue +371 -0
  28. package/components/TableMain.vue +22 -7
  29. package/components/VehicleQrStickerDialog.vue +64 -4
  30. package/components/VisitorForm.vue +12 -0
  31. package/components/VisitorManagement.vue +267 -5
  32. package/composables/useAccessManagement.ts +17 -1
  33. package/composables/useCustomerSite.ts +29 -0
  34. package/composables/useDocument.ts +7 -0
  35. package/composables/useFacility.ts +11 -0
  36. package/composables/useHidAmico.ts +165 -0
  37. package/composables/useHidNavigation.ts +25 -1
  38. package/composables/useHidReaderSelection.ts +50 -0
  39. package/composables/useMember.ts +12 -0
  40. package/composables/useNFCPatrolReport.ts +36 -32
  41. package/composables/useNFCPatrolReportFilters.ts +21 -3
  42. package/composables/useNFCPatrolRoute.ts +10 -3
  43. package/composables/usePeople.ts +19 -0
  44. package/composables/useServiceProvider.ts +45 -2
  45. package/composables/useSiteCategory.ts +45 -0
  46. package/package.json +1 -1
  47. package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
  48. package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
  49. package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +23 -0
  50. package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
  51. package/types/document.d.ts +14 -0
  52. package/types/facility.d.ts +4 -0
  53. package/types/member.d.ts +4 -0
  54. package/types/nfc-patrol-report.ts +0 -2
  55. package/types/people.d.ts +28 -1
  56. package/types/service-provider.d.ts +5 -0
  57. package/types/site.d.ts +7 -1
  58. package/utils/hid-enrolment-subject.ts +344 -0
  59. package/utils/hid-permission-assignments.ts +198 -0
  60. package/utils/hid-reader-selection.ts +52 -0
  61. package/utils/nfc-patrol-time.ts +99 -0
  62. package/utils/occupancy-role.ts +109 -0
  63. package/utils/service-type.ts +30 -0
  64. package/utils/vehicle-qr-design.ts +304 -20
  65. package/utils/vehicle-qr-sticker.ts +126 -20
@@ -1,45 +1,177 @@
1
1
  <template>
2
2
  <v-card width="100%" class="screen-modal">
3
- <v-card-title class="text-capitalize">{{ prop.mode }} Document</v-card-title>
3
+ <v-card-title class="screen-modal__title">{{ heading }}</v-card-title>
4
4
 
5
5
  <v-card-text class="screen-modal__body pa-0">
6
- <v-form v-model="validForm" :disabled="disable">
7
- <v-col cols="12" class="px-6">
8
- <InputLabel class="text-capitalize" title="Document Attachment" />
9
- <InputFileV2
10
- v-model="document.attachment"
11
- :multiple="false"
12
- :max-length="10"
13
- title="Upload Files"
14
- accept=".pdf,.csv,.doc,.docx"
15
- />
6
+ <!--
7
+ EDIT: metadata only. The design deliberately shows the attachment as a
8
+ read-only chip here - swapping the file is what Update- Document is for,
9
+ and expiry is only set on upload or renewal, so neither control appears.
10
+ -->
11
+ <template v-if="isEdit">
12
+ <v-col cols="12" class="px-6 pt-4">
13
+ <InputLabel title="Document Attachment" />
14
+ <div class="doc-form__chip">
15
+ <v-icon size="20" class="mr-2">mdi-file-document-outline</v-icon>
16
+ <span class="text-body-2">{{ existingFileName || "—" }}</span>
17
+ </div>
16
18
  </v-col>
17
19
 
18
20
  <v-col cols="12" class="px-6">
19
- <InputLabel class="text-capitalize" title="Document Name" />
21
+ <InputLabel title="Document Title" required />
20
22
  <v-text-field
21
- v-model.trim="document.name"
23
+ v-model.trim="editTitle"
22
24
  class="filter-field"
23
25
  density="comfortable"
24
- ></v-text-field>
26
+ placeholder="Company Policy"
27
+ hide-details="auto"
28
+ />
25
29
  </v-col>
26
30
 
27
- <v-col cols="12">
28
- <v-row no-gutters>
29
- <v-col cols="12" class="text-center">
30
- <span
31
- class="text-none text-subtitle-2 font-weight-medium text-error"
32
- >
33
- {{ message }}
34
- </span>
35
- </v-col>
36
- </v-row>
31
+ <v-col cols="12" class="px-6 pb-2">
32
+ <InputLabel title="Remarks (Optional)" />
33
+ <v-textarea
34
+ v-model.trim="editRemarks"
35
+ class="filter-field"
36
+ density="comfortable"
37
+ rows="3"
38
+ placeholder="Enter remarks"
39
+ hide-details="auto"
40
+ />
37
41
  </v-col>
38
- </v-form>
42
+ </template>
43
+
44
+ <!-- ADD and UPDATE: a drop zone, then one configurable row per file. -->
45
+ <template v-else>
46
+ <v-col cols="12" class="px-6 pt-4">
47
+ <InputLabel title="Document Attachment" />
48
+
49
+ <!--
50
+ Hand-rolled rather than `InputFileV2`, which renders its own chip
51
+ list from internal state and only ever pushes ids outward. The
52
+ design puts that list in the table below with a per-row delete, and
53
+ a delete there could not reach back into the uploader to drop the
54
+ chip - the two lists would disagree on what is about to be saved.
55
+ -->
56
+ <div
57
+ class="doc-form__drop"
58
+ :class="{ 'doc-form__drop--over': dragging }"
59
+ role="button"
60
+ tabindex="0"
61
+ @click="pickFiles"
62
+ @keydown.enter.prevent="pickFiles"
63
+ @keydown.space.prevent="pickFiles"
64
+ @dragover.prevent="dragging = true"
65
+ @dragleave.prevent="dragging = false"
66
+ @drop.prevent="onDrop"
67
+ >
68
+ <v-progress-circular v-if="uploading" indeterminate size="24" />
69
+ <template v-else>
70
+ <v-icon size="24" class="mr-2">mdi-cloud-upload-outline</v-icon>
71
+ <span class="text-body-2">{{ dropLabel }}</span>
72
+ </template>
73
+ </div>
74
+
75
+ <input
76
+ ref="fileInput"
77
+ type="file"
78
+ class="d-none"
79
+ :accept="ACCEPT"
80
+ :multiple="isAdd"
81
+ @change="onPick"
82
+ />
83
+ </v-col>
84
+
85
+ <v-col v-if="rows.length" cols="12" class="px-6 pb-2">
86
+ <div class="d-flex align-center justify-space-between">
87
+ <span class="text-body-2">{{ selectionSummary }}</span>
88
+ <v-btn
89
+ v-if="isAdd"
90
+ variant="flat"
91
+ class="text-none screen-btn-primary"
92
+ prepend-icon="mdi-plus"
93
+ :disabled="uploading"
94
+ @click="pickFiles"
95
+ >
96
+ Add more files
97
+ </v-btn>
98
+ </div>
99
+ </v-col>
100
+
101
+ <v-col v-if="rows.length" cols="12" class="pa-0">
102
+ <v-table class="doc-form__table" density="comfortable">
103
+ <thead>
104
+ <tr>
105
+ <th>Original File Name</th>
106
+ <th>Custom Name</th>
107
+ <th>Remarks</th>
108
+ <th>{{ isAdd ? "Expiry Date (optional)" : "Expiry Date" }}</th>
109
+ <th class="text-center">Action</th>
110
+ </tr>
111
+ </thead>
112
+ <tbody>
113
+ <tr v-for="row in rows" :key="row.id">
114
+ <td>
115
+ <div class="d-flex align-center ga-2">
116
+ <v-icon size="20">mdi-file-document-outline</v-icon>
117
+ <span>{{ row.originalName }}</span>
118
+ </div>
119
+ </td>
120
+ <td>
121
+ <v-text-field
122
+ v-model.trim="row.customName"
123
+ class="filter-field"
124
+ density="compact"
125
+ placeholder="Company Policy"
126
+ hide-details
127
+ />
128
+ </td>
129
+ <td>
130
+ <v-text-field
131
+ v-model.trim="row.remarks"
132
+ class="filter-field"
133
+ density="compact"
134
+ placeholder="Enter remarks"
135
+ hide-details
136
+ />
137
+ </td>
138
+ <td>
139
+ <!--
140
+ No `min`. A past date is legitimate on both paths: an estate
141
+ may file a contract that already lapsed, and a renewal that
142
+ does not move the date forward is defined to leave the
143
+ document on the Expired tab rather than be rejected.
144
+ -->
145
+ <InputDatePicker
146
+ v-model="row.expiryDate"
147
+ density="compact"
148
+ clearable
149
+ hide-details
150
+ />
151
+ </td>
152
+ <td class="text-center">
153
+ <v-btn
154
+ icon="mdi-trash-can-outline"
155
+ variant="text"
156
+ size="small"
157
+ density="comfortable"
158
+ aria-label="Remove file"
159
+ @click="removeRow(row.id)"
160
+ />
161
+ </td>
162
+ </tr>
163
+ </tbody>
164
+ </v-table>
165
+ </v-col>
166
+ </template>
167
+
168
+ <v-col cols="12" class="pt-0">
169
+ <p v-if="message" class="text-error text-center text-body-2 mb-0">
170
+ {{ message }}
171
+ </p>
172
+ </v-col>
39
173
  </v-card-text>
40
174
 
41
- <!-- Was a `v-toolbar`: a grey slab with square (`tile`) buttons and a
42
- hardcoded `color="black"` primary. The gate on Submit is unchanged. -->
43
175
  <v-card-actions class="screen-modal__footer">
44
176
  <v-row no-gutters>
45
177
  <v-col cols="6" class="pr-2">
@@ -47,8 +179,8 @@
47
179
  block
48
180
  variant="outlined"
49
181
  class="text-none screen-btn-ghost"
182
+ :disabled="saving"
50
183
  @click="cancel"
51
- :disabled="disable"
52
184
  >
53
185
  Cancel
54
186
  </v-btn>
@@ -59,24 +191,38 @@
59
191
  block
60
192
  variant="flat"
61
193
  class="text-none screen-btn-primary"
62
- :disabled="
63
- !validForm ||
64
- disable ||
65
- document.attachment.length == 0 ||
66
- document.name == ''
67
- "
194
+ :disabled="!canSubmit || saving"
195
+ :loading="saving"
68
196
  @click="submit"
69
- :loading="disable"
70
197
  >
71
- Submit
198
+ {{ submitLabel }}
72
199
  </v-btn>
73
200
  </v-col>
74
201
  </v-row>
75
202
  </v-card-actions>
76
203
  </v-card>
77
204
  </template>
205
+
78
206
  <script setup lang="ts">
207
+ /** The upload filter. Kept beside `deriveTypeFromFilename`, which must agree. */
208
+ const ACCEPT = ".pdf,.csv,.doc,.docx";
209
+
210
+ type DocumentRow = {
211
+ /** Stored file id, returned by `addFile`. Also the row key. */
212
+ id: string;
213
+ originalName: string;
214
+ customName: string;
215
+ remarks: string;
216
+ /** `YYYY-MM-DD` from `InputDatePicker`, or null for "never expires". */
217
+ expiryDate: string | null;
218
+ };
219
+
79
220
  const prop = defineProps({
221
+ /**
222
+ * "add" uploads any number of new documents, "update" replaces the file on
223
+ * one existing document and re-dates it, "edit" changes title and remarks
224
+ * only. They are three dialogs in the design and three payloads here.
225
+ */
80
226
  mode: {
81
227
  type: String,
82
228
  default: "add",
@@ -98,26 +244,55 @@ const prop = defineProps({
98
244
  const emit = defineEmits(["cancel", "success"]);
99
245
 
100
246
  const { add, updateById } = useDocument();
101
- const { getFileById } = useFile();
247
+ const { addFile, getFileById } = useFile();
102
248
 
103
- const validForm = ref(false);
104
- const disable = ref(false);
105
- const message = ref("");
249
+ const isAdd = computed(() => prop.mode === "add");
250
+ const isEdit = computed(() => prop.mode === "edit");
251
+ const isUpdate = computed(() => prop.mode === "update");
106
252
 
107
- const document = ref<Record<string, any>>({
108
- name: "",
109
- attachment: [],
110
- type: "",
253
+ const heading = computed(() => {
254
+ if (isEdit.value) return "Edit Document";
255
+ if (isUpdate.value) return "Update- Document";
256
+ return "Add Document";
111
257
  });
112
258
 
259
+ const submitLabel = computed(() => (isEdit.value ? "Apply" : "Submit"));
260
+ const dropLabel = computed(() => (isAdd.value ? "Select Files" : "Select File"));
261
+
262
+ const rows = ref<DocumentRow[]>([]);
263
+ const fileInput = ref<HTMLInputElement | null>(null);
264
+ const dragging = ref(false);
265
+ const uploading = ref(false);
266
+ const saving = ref(false);
267
+ const message = ref("");
268
+
269
+ const editTitle = ref("");
270
+ const editRemarks = ref("");
271
+ const existingFileName = ref("");
272
+
113
273
  const route = useRoute();
114
274
  const siteId = route.params.site as string;
115
275
  const orgId = route.params.org as string;
116
276
  const routeParentId = computed(() =>
117
- typeof route.query.parentId === "string" ? route.query.parentId : ""
277
+ typeof route.query.parentId === "string" ? route.query.parentId : "",
118
278
  );
119
279
  const effectiveParentId = computed(() => prop.parentId || routeParentId.value);
120
280
 
281
+ const selectionSummary = computed(() =>
282
+ rows.value.length === 1 ? "1 file selected" : `${rows.value.length} files selected`,
283
+ );
284
+
285
+ /**
286
+ * Every row needs a name, because `name` is what the list draws and what the
287
+ * server requires. Blank-but-present rows are the realistic mistake, so the
288
+ * gate is on trimmed emptiness rather than on the form's validity flag.
289
+ */
290
+ const canSubmit = computed(() => {
291
+ if (uploading.value) return false;
292
+ if (isEdit.value) return editTitle.value.trim().length > 0;
293
+ return rows.value.length > 0 && rows.value.every((r) => r.customName.trim());
294
+ });
295
+
121
296
  function getAttachmentId(attachment: any): string {
122
297
  if (!attachment) return "";
123
298
  if (Array.isArray(attachment)) {
@@ -130,102 +305,222 @@ function getAttachmentId(attachment: any): string {
130
305
  return String(attachment?._id || attachment?.id || "");
131
306
  }
132
307
 
133
- if (prop.mode === "edit") {
134
- document.value.name = prop.document.name;
135
-
136
- const extractedId = getAttachmentId(prop.document.attachment);
137
- document.value.attachment = extractedId ? [extractedId] : [];
138
-
139
- document.value.type = prop.document.type;
308
+ function deriveTypeFromFilename(name = "") {
309
+ const n = String(name).toLowerCase();
310
+ if (n.endsWith(".pdf")) return "pdf";
311
+ if (n.endsWith(".csv")) return "csv";
312
+ if (n.endsWith(".doc") || n.endsWith(".docx")) return "doc";
313
+ return "";
314
+ }
315
+
316
+ /** `Contract.doc` -> `Contract`, so the name field starts somewhere sensible. */
317
+ function stripExtension(name: string) {
318
+ const dot = name.lastIndexOf(".");
319
+ return dot > 0 ? name.slice(0, dot) : name;
320
+ }
321
+
322
+ /** `2026-12-13` -> a date-only ISO instant the server's `Joi.date()` accepts. */
323
+ function toIsoOrNull(value: string | null) {
324
+ return value ? new Date(`${value}T00:00:00.000Z`).toISOString() : null;
325
+ }
326
+
327
+ /** An ISO instant from the server -> the `YYYY-MM-DD` the picker binds to. */
328
+ function toPickerDate(value: unknown) {
329
+ if (!value) return null;
330
+ const d = new Date(value as string);
331
+ return Number.isNaN(d.getTime()) ? null : d.toISOString().slice(0, 10);
332
+ }
333
+
334
+ const existingAttachmentId = computed(() =>
335
+ getAttachmentId(prop.document?.attachment),
336
+ );
337
+
338
+ onMounted(async () => {
339
+ if (!isEdit.value && !isUpdate.value) return;
340
+
341
+ editTitle.value = prop.document?.name ?? "";
342
+ editRemarks.value = String((prop.document as any)?.remarks ?? "");
343
+
344
+ const id = existingAttachmentId.value;
345
+ if (!id) return;
346
+
347
+ try {
348
+ const file = await getFileById(id);
349
+ existingFileName.value = file?.data?.name ?? id;
350
+ } catch {
351
+ // The name is decoration on a read-only chip; the id still saves.
352
+ existingFileName.value = id;
353
+ }
354
+ });
355
+
356
+ function pickFiles() {
357
+ if (uploading.value) return;
358
+ fileInput.value?.click();
359
+ }
360
+
361
+ function onDrop(event: DragEvent) {
362
+ dragging.value = false;
363
+ const files = Array.from(event.dataTransfer?.files ?? []);
364
+ if (files.length) void uploadFiles(files);
140
365
  }
141
366
 
142
- watch(
143
- () => document.value.attachment,
144
- async (newVal, oldVal) => {
145
- const newId = Array.isArray(newVal) ? newVal[0] : newVal;
146
- const oldId = Array.isArray(oldVal) ? oldVal[0] : oldVal;
367
+ function onPick(event: Event) {
368
+ const input = event.target as HTMLInputElement;
369
+ const files = Array.from(input.files ?? []);
370
+ // Cleared so re-picking the same file fires `change` again.
371
+ input.value = "";
372
+ if (files.length) void uploadFiles(files);
373
+ }
147
374
 
148
- if (newId && newId !== oldId) {
149
- const fileId = newId;
150
- const fileData = await getFileById(fileId);
151
- if (fileData) {
152
- document.value.name = fileData.data.name;
375
+ async function uploadFiles(files: File[]) {
376
+ message.value = "";
377
+ uploading.value = true;
378
+
379
+ try {
380
+ // Update replaces one file, so a second pick supersedes the first rather
381
+ // than queueing behind it.
382
+ const queue = isAdd.value ? files : files.slice(0, 1);
383
+
384
+ for (const file of queue) {
385
+ if (!deriveTypeFromFilename(file.name)) {
386
+ message.value = "Unsupported file type. Allowed: pdf, csv, doc, docx.";
387
+ continue;
388
+ }
389
+
390
+ const stored = await addFile(file);
391
+ if (!stored?.id) {
392
+ message.value = "Upload failed. Please try again.";
393
+ continue;
394
+ }
395
+
396
+ const row: DocumentRow = {
397
+ id: stored.id,
398
+ originalName: file.name,
399
+ customName: stripExtension(file.name),
400
+ remarks: "",
401
+ expiryDate: null,
402
+ };
403
+
404
+ if (isAdd.value) {
405
+ rows.value = [...rows.value, row];
406
+ } else {
407
+ // Carry the existing document's title and expiry onto the replacement,
408
+ // so a renewal that only swaps the file keeps its identity.
409
+ row.customName = editTitle.value || row.customName;
410
+ row.remarks = editRemarks.value;
411
+ row.expiryDate = toPickerDate((prop.document as any)?.expiryDate);
412
+ rows.value = [row];
153
413
  }
154
414
  }
415
+ } catch (error: any) {
416
+ message.value = error?.response?._data?.message || "Upload failed.";
417
+ } finally {
418
+ uploading.value = false;
155
419
  }
156
- );
420
+ }
157
421
 
158
- function deriveTypeFromFilename(name = "") {
159
- const n = String(name).toLowerCase();
160
- if (n.endsWith(".pdf")) return "pdf";
161
- if (n.endsWith(".csv")) return "csv";
162
- if (n.endsWith(".doc") || n.endsWith(".docx")) return "doc";
163
- return "";
422
+ function removeRow(id: string) {
423
+ rows.value = rows.value.filter((row) => row.id !== id);
164
424
  }
165
425
 
166
426
  function cancel() {
167
- // createMore.value = false;
168
427
  message.value = "";
169
428
  emit("cancel");
170
429
  }
171
430
 
172
431
  async function submit() {
173
- disable.value = true;
174
- try {
175
- if (prop.mode === "add") {
176
- const fileId = Array.isArray(document.value.attachment)
177
- ? document.value.attachment[0]
178
- : document.value.attachment;
179
-
180
- const fileData = await getFileById(fileId);
181
- const fileName = fileData?.data?.name || String(fileId || "");
182
- const derivedType = deriveTypeFromFilename(fileName);
183
-
184
- if (!derivedType) {
185
- throw new Error("Unsupported file type. Allowed: pdf, csv, doc.");
186
- }
432
+ saving.value = true;
433
+ message.value = "";
187
434
 
188
- const payload = {
189
- name: document.value.name,
190
- attachment: document.value.attachment,
191
- type: derivedType,
192
- size: fileData?.data?.size ?? fileData?.data?.file_size ?? 0,
435
+ try {
436
+ if (isAdd.value) {
437
+ await add({
438
+ // `name` is required on the envelope even when every item names
439
+ // itself; the first row stands in for it.
440
+ name: rows.value[0].customName,
193
441
  status: "active",
194
442
  org: orgId,
195
443
  site: siteId,
196
444
  parentId: effectiveParentId.value,
197
- };
198
-
199
- await add(payload);
445
+ items: rows.value.map((row) => ({
446
+ name: row.customName,
447
+ attachment: [row.id],
448
+ type: deriveTypeFromFilename(row.originalName),
449
+ remarks: row.remarks,
450
+ expiryDate: toIsoOrNull(row.expiryDate),
451
+ })),
452
+ });
200
453
  }
201
454
 
202
- if (prop.mode === "edit") {
203
- const fileId = Array.isArray(document.value.attachment)
204
- ? document.value.attachment[0]
205
- : document.value.attachment;
206
-
207
- let payload: Record<string, any> = {
208
- name: document.value.name,
209
- attachment: document.value.attachment,
210
- };
211
-
212
- if (fileId) {
213
- const fileData = await getFileById(fileId);
214
- const fileName = fileData?.data?.name || String(fileId || "");
215
- const derivedType = deriveTypeFromFilename(fileName);
216
- if (derivedType) payload.type = derivedType;
217
- payload.size = fileData?.data?.size ?? fileData?.data?.file_size ?? 0;
218
- }
455
+ if (isUpdate.value) {
456
+ const row = rows.value[0];
457
+ await updateById(prop.document._id ?? "", {
458
+ name: row.customName,
459
+ attachment: [row.id],
460
+ type: deriveTypeFromFilename(row.originalName),
461
+ remarks: row.remarks,
462
+ expiryDate: toIsoOrNull(row.expiryDate),
463
+ });
464
+ }
219
465
 
220
- await updateById(prop.document._id ?? "", payload);
466
+ if (isEdit.value) {
467
+ // The attachment is resent unchanged on purpose. The update service
468
+ // treats a missing `attachment` as "no files", blanking the document's
469
+ // file and zeroing its size - so a title-only edit would destroy the
470
+ // document it was meant to rename.
471
+ await updateById(prop.document._id ?? "", {
472
+ name: editTitle.value,
473
+ remarks: editRemarks.value,
474
+ attachment: existingAttachmentId.value
475
+ ? [existingAttachmentId.value]
476
+ : [],
477
+ });
221
478
  }
222
479
 
223
480
  emit("success");
224
481
  } catch (error: any) {
225
482
  message.value =
226
- error.response?._data?.message || "Failed to create document";
483
+ error?.response?._data?.message || "Failed to save document.";
227
484
  } finally {
228
- disable.value = false;
485
+ saving.value = false;
229
486
  }
230
487
  }
231
488
  </script>
489
+
490
+ <style scoped>
491
+ .doc-form__drop {
492
+ display: flex;
493
+ align-items: center;
494
+ justify-content: center;
495
+ min-height: 120px;
496
+ border: 1px dashed var(--border);
497
+ border-radius: var(--r-inner);
498
+ cursor: pointer;
499
+ transition: background-color 0.15s ease, border-color 0.15s ease;
500
+ }
501
+
502
+ .doc-form__drop:hover,
503
+ .doc-form__drop--over {
504
+ border-color: var(--text);
505
+ background-color: var(--hover);
506
+ }
507
+
508
+ .doc-form__chip {
509
+ display: flex;
510
+ align-items: center;
511
+ padding: 12px;
512
+ border: 1px dashed var(--border);
513
+ border-radius: var(--r-inner);
514
+ }
515
+
516
+ .doc-form__table :deep(th) {
517
+ font-size: var(--fs-cell);
518
+ font-weight: 500;
519
+ white-space: nowrap;
520
+ }
521
+
522
+ .doc-form__table :deep(td) {
523
+ padding-top: 8px;
524
+ padding-bottom: 8px;
525
+ }
526
+ </style>