@7365admin1/layer-common 4.90.2-staging.491 → 4.90.2
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.
- package/CHANGELOG.md +10 -4
- package/assets/css/primitives.css +0 -57
- package/assets/css/screens.css +4 -8
- package/components/AccessCardDetailsDialog.vue +6 -59
- package/components/AccessCardPreviewDialog.vue +53 -371
- package/components/AppSelect.vue +4 -143
- package/components/BuildingUnitFormEdit.vue +0 -6
- package/components/Dialog/UpdateMoreAction.vue +1 -11
- package/components/DocumentForm.vue +112 -407
- package/components/DocumentManagement.vue +12 -213
- package/components/Facility/BookingSetup.vue +0 -63
- package/components/HidAccessLogDashboard.vue +17 -74
- package/components/HidIntercomManagement.vue +14 -183
- package/components/HidQrCodeConfiguration.vue +195 -17
- package/components/HidReaderManagement.vue +0 -398
- package/components/HidReaderUserRoster.vue +3 -44
- package/components/HidUserEnrollment.vue +292 -2255
- package/components/InventoryItemDetail.vue +1 -2
- package/components/InventoryLinesDialog.vue +13 -91
- package/components/InventoryPhotoInput.vue +6 -44
- package/components/InventoryReportsTab.vue +3 -23
- package/components/InventoryRequestsTab.vue +4 -27
- package/components/InventoryStockTab.vue +0 -3
- package/components/InvitationClientForm.vue +1 -19
- package/components/InvitationForm.vue +4 -31
- package/components/InvitationMain.vue +22 -124
- package/components/MemberInformation.vue +4 -4
- package/components/PassInformation.vue +2 -2
- package/components/SiteSettings.vue +0 -48
- package/components/TableMain.vue +7 -22
- package/components/VehicleManagement.vue +5 -80
- package/components/VisitorForm.vue +19 -65
- package/components/VisitorManagement.vue +5 -267
- package/composables/useAccessManagement.ts +1 -17
- package/composables/useDocument.ts +0 -7
- package/composables/useFacility.ts +0 -11
- package/composables/useHidAmico.ts +0 -165
- package/composables/useHidNavigation.ts +1 -25
- package/composables/useInventory.ts +1 -4
- package/composables/useMember.ts +0 -12
- package/composables/useNFCPatrolReportFilters.ts +3 -21
- package/composables/useNFCPatrolRoute.ts +1 -8
- package/composables/usePeople.ts +0 -19
- package/composables/useServiceProvider.ts +2 -45
- package/composables/useSettingsPermission.ts +0 -14
- package/composables/useUser.ts +2 -6
- package/composables/useVehicle.ts +1 -40
- package/composables/useVerification.ts +0 -3
- package/middleware/01.auth.ts +7 -3
- package/package.json +1 -1
- package/pages/[org]/[site]/access-mgmt/administrator/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-cards/index.vue +1 -1
- package/pages/[org]/[site]/access-mgmt/hid-readers/index.vue +1 -1
- package/types/document.d.ts +0 -14
- package/types/facility.d.ts +0 -4
- package/types/inventory.d.ts +0 -27
- package/types/member.d.ts +0 -4
- package/types/people.d.ts +1 -28
- package/types/service-provider.d.ts +0 -5
- package/types/site.d.ts +1 -7
- package/components/BuildingUnitManagement.vue +0 -298
- package/components/HidAccessPermissions.vue +0 -424
- package/components/HidProfileQrCode.vue +0 -332
- package/components/InventoryPoDialog.vue +0 -111
- package/components/VehicleQrStickerDialog.vue +0 -249
- package/composables/useHidReaderSelection.ts +0 -50
- package/composables/useSiteCategory.ts +0 -45
- package/pages/[org]/[site]/access-mgmt/hid-qr-code/index.vue +0 -23
- package/utils/hid-enrolment-subject.ts +0 -344
- package/utils/hid-permission-assignments.ts +0 -198
- package/utils/hid-reader-selection.ts +0 -52
- package/utils/inventory-receiving.ts +0 -90
- package/utils/invite-group.ts +0 -61
- package/utils/invite-sites.ts +0 -25
- package/utils/occupancy-role.ts +0 -109
- package/utils/service-type.ts +0 -30
- package/utils/vehicle-qr-sticker.ts +0 -184
|
@@ -1,177 +1,45 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<v-card width="100%" class="screen-modal">
|
|
3
|
-
<v-card-title class="
|
|
3
|
+
<v-card-title class="text-capitalize">{{ prop.mode }} Document</v-card-title>
|
|
4
4
|
|
|
5
5
|
<v-card-text class="screen-modal__body pa-0">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<span class="text-body-2">{{ existingFileName || "—" }}</span>
|
|
17
|
-
</div>
|
|
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
|
+
/>
|
|
18
16
|
</v-col>
|
|
19
17
|
|
|
20
18
|
<v-col cols="12" class="px-6">
|
|
21
|
-
<InputLabel title="Document
|
|
19
|
+
<InputLabel class="text-capitalize" title="Document Name" />
|
|
22
20
|
<v-text-field
|
|
23
|
-
v-model.trim="
|
|
21
|
+
v-model.trim="document.name"
|
|
24
22
|
class="filter-field"
|
|
25
23
|
density="comfortable"
|
|
26
|
-
|
|
27
|
-
hide-details="auto"
|
|
28
|
-
/>
|
|
24
|
+
></v-text-field>
|
|
29
25
|
</v-col>
|
|
30
26
|
|
|
31
|
-
<v-col cols="12"
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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>
|
|
41
37
|
</v-col>
|
|
42
|
-
</
|
|
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>
|
|
38
|
+
</v-form>
|
|
173
39
|
</v-card-text>
|
|
174
40
|
|
|
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. -->
|
|
175
43
|
<v-card-actions class="screen-modal__footer">
|
|
176
44
|
<v-row no-gutters>
|
|
177
45
|
<v-col cols="6" class="pr-2">
|
|
@@ -179,8 +47,8 @@
|
|
|
179
47
|
block
|
|
180
48
|
variant="outlined"
|
|
181
49
|
class="text-none screen-btn-ghost"
|
|
182
|
-
:disabled="saving"
|
|
183
50
|
@click="cancel"
|
|
51
|
+
:disabled="disable"
|
|
184
52
|
>
|
|
185
53
|
Cancel
|
|
186
54
|
</v-btn>
|
|
@@ -191,38 +59,24 @@
|
|
|
191
59
|
block
|
|
192
60
|
variant="flat"
|
|
193
61
|
class="text-none screen-btn-primary"
|
|
194
|
-
:disabled="
|
|
195
|
-
|
|
62
|
+
:disabled="
|
|
63
|
+
!validForm ||
|
|
64
|
+
disable ||
|
|
65
|
+
document.attachment.length == 0 ||
|
|
66
|
+
document.name == ''
|
|
67
|
+
"
|
|
196
68
|
@click="submit"
|
|
69
|
+
:loading="disable"
|
|
197
70
|
>
|
|
198
|
-
|
|
71
|
+
Submit
|
|
199
72
|
</v-btn>
|
|
200
73
|
</v-col>
|
|
201
74
|
</v-row>
|
|
202
75
|
</v-card-actions>
|
|
203
76
|
</v-card>
|
|
204
77
|
</template>
|
|
205
|
-
|
|
206
78
|
<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
|
-
|
|
220
79
|
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
|
-
*/
|
|
226
80
|
mode: {
|
|
227
81
|
type: String,
|
|
228
82
|
default: "add",
|
|
@@ -244,55 +98,26 @@ const prop = defineProps({
|
|
|
244
98
|
const emit = defineEmits(["cancel", "success"]);
|
|
245
99
|
|
|
246
100
|
const { add, updateById } = useDocument();
|
|
247
|
-
const {
|
|
248
|
-
|
|
249
|
-
const isAdd = computed(() => prop.mode === "add");
|
|
250
|
-
const isEdit = computed(() => prop.mode === "edit");
|
|
251
|
-
const isUpdate = computed(() => prop.mode === "update");
|
|
252
|
-
|
|
253
|
-
const heading = computed(() => {
|
|
254
|
-
if (isEdit.value) return "Edit Document";
|
|
255
|
-
if (isUpdate.value) return "Update- Document";
|
|
256
|
-
return "Add Document";
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const submitLabel = computed(() => (isEdit.value ? "Apply" : "Submit"));
|
|
260
|
-
const dropLabel = computed(() => (isAdd.value ? "Select Files" : "Select File"));
|
|
101
|
+
const { getFileById } = useFile();
|
|
261
102
|
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const dragging = ref(false);
|
|
265
|
-
const uploading = ref(false);
|
|
266
|
-
const saving = ref(false);
|
|
103
|
+
const validForm = ref(false);
|
|
104
|
+
const disable = ref(false);
|
|
267
105
|
const message = ref("");
|
|
268
106
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
107
|
+
const document = ref<Record<string, any>>({
|
|
108
|
+
name: "",
|
|
109
|
+
attachment: [],
|
|
110
|
+
type: "",
|
|
111
|
+
});
|
|
272
112
|
|
|
273
113
|
const route = useRoute();
|
|
274
114
|
const siteId = route.params.site as string;
|
|
275
115
|
const orgId = route.params.org as string;
|
|
276
116
|
const routeParentId = computed(() =>
|
|
277
|
-
typeof route.query.parentId === "string" ? route.query.parentId : ""
|
|
117
|
+
typeof route.query.parentId === "string" ? route.query.parentId : ""
|
|
278
118
|
);
|
|
279
119
|
const effectiveParentId = computed(() => prop.parentId || routeParentId.value);
|
|
280
120
|
|
|
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
|
-
|
|
296
121
|
function getAttachmentId(attachment: any): string {
|
|
297
122
|
if (!attachment) return "";
|
|
298
123
|
if (Array.isArray(attachment)) {
|
|
@@ -305,222 +130,102 @@ function getAttachmentId(attachment: any): string {
|
|
|
305
130
|
return String(attachment?._id || attachment?.id || "");
|
|
306
131
|
}
|
|
307
132
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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);
|
|
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;
|
|
365
140
|
}
|
|
366
141
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (files.length) void uploadFiles(files);
|
|
373
|
-
}
|
|
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;
|
|
374
147
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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];
|
|
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;
|
|
413
153
|
}
|
|
414
154
|
}
|
|
415
|
-
} catch (error: any) {
|
|
416
|
-
message.value = error?.response?._data?.message || "Upload failed.";
|
|
417
|
-
} finally {
|
|
418
|
-
uploading.value = false;
|
|
419
155
|
}
|
|
420
|
-
|
|
156
|
+
);
|
|
421
157
|
|
|
422
|
-
function
|
|
423
|
-
|
|
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 "";
|
|
424
164
|
}
|
|
425
165
|
|
|
426
166
|
function cancel() {
|
|
167
|
+
// createMore.value = false;
|
|
427
168
|
message.value = "";
|
|
428
169
|
emit("cancel");
|
|
429
170
|
}
|
|
430
171
|
|
|
431
172
|
async function submit() {
|
|
432
|
-
|
|
433
|
-
message.value = "";
|
|
434
|
-
|
|
173
|
+
disable.value = true;
|
|
435
174
|
try {
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
+
}
|
|
187
|
+
|
|
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,
|
|
441
193
|
status: "active",
|
|
442
194
|
org: orgId,
|
|
443
195
|
site: siteId,
|
|
444
196
|
parentId: effectiveParentId.value,
|
|
445
|
-
|
|
446
|
-
name: row.customName,
|
|
447
|
-
attachment: [row.id],
|
|
448
|
-
type: deriveTypeFromFilename(row.originalName),
|
|
449
|
-
remarks: row.remarks,
|
|
450
|
-
expiryDate: toIsoOrNull(row.expiryDate),
|
|
451
|
-
})),
|
|
452
|
-
});
|
|
453
|
-
}
|
|
197
|
+
};
|
|
454
198
|
|
|
455
|
-
|
|
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
|
-
});
|
|
199
|
+
await add(payload);
|
|
464
200
|
}
|
|
465
201
|
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
name:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
}
|
|
219
|
+
|
|
220
|
+
await updateById(prop.document._id ?? "", payload);
|
|
478
221
|
}
|
|
479
222
|
|
|
480
223
|
emit("success");
|
|
481
224
|
} catch (error: any) {
|
|
482
225
|
message.value =
|
|
483
|
-
error
|
|
226
|
+
error.response?._data?.message || "Failed to create document";
|
|
484
227
|
} finally {
|
|
485
|
-
|
|
228
|
+
disable.value = false;
|
|
486
229
|
}
|
|
487
230
|
}
|
|
488
231
|
</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>
|