@7365admin1/layer-common 1.8.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 (198) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.editorconfig +12 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/.nuxtrc +1 -0
  7. package/.playground/app.vue +41 -0
  8. package/.playground/eslint.config.mjs +6 -0
  9. package/.playground/nuxt.config.ts +22 -0
  10. package/.playground/pages/feedback.vue +30 -0
  11. package/CHANGELOG.md +263 -0
  12. package/README.md +73 -0
  13. package/app.vue +3 -0
  14. package/components/AccessCardAddForm.vue +363 -0
  15. package/components/AccessManagement.vue +420 -0
  16. package/components/Avatar/Main.vue +68 -0
  17. package/components/BillingMain.vue +66 -0
  18. package/components/BtnUploadFile.vue +139 -0
  19. package/components/BuildingForm.vue +303 -0
  20. package/components/BuildingManagement/buildings.vue +335 -0
  21. package/components/BuildingManagement/units.vue +350 -0
  22. package/components/BuildingUnitFormAdd.vue +441 -0
  23. package/components/BuildingUnitFormEdit.vue +429 -0
  24. package/components/CameraForm.vue +264 -0
  25. package/components/CameraMain.vue +352 -0
  26. package/components/Card/DeleteConfirmation.vue +51 -0
  27. package/components/Card/MemberInfoSummary.vue +44 -0
  28. package/components/Card/Toggle.vue +25 -0
  29. package/components/Chat/Bubbles.vue +53 -0
  30. package/components/Chat/Information.vue +416 -0
  31. package/components/Chat/ListCard.vue +62 -0
  32. package/components/Chat/Message.vue +158 -0
  33. package/components/Chat/Navigation.vue +150 -0
  34. package/components/ConfirmDialog.vue +66 -0
  35. package/components/Container/Standard.vue +33 -0
  36. package/components/DashboardPlaceholder.vue +1524 -0
  37. package/components/Dialog/DeleteConfirmation.vue +51 -0
  38. package/components/Dialog/ReplaceAutofillPrompt.vue +49 -0
  39. package/components/Dialog/UpdateMoreAction.vue +103 -0
  40. package/components/DocumentForm.vue +187 -0
  41. package/components/DocumentManagement.vue +376 -0
  42. package/components/Editor.vue +95 -0
  43. package/components/EntryPassMain.vue +518 -0
  44. package/components/Feedback/Form.vue +173 -0
  45. package/components/FeedbackDetail.vue +599 -0
  46. package/components/FeedbackMain.vue +588 -0
  47. package/components/FormDialog.vue +65 -0
  48. package/components/ImageCarousel.vue +138 -0
  49. package/components/Input/Date.vue +177 -0
  50. package/components/Input/DateTimePicker.vue +131 -0
  51. package/components/Input/File.vue +236 -0
  52. package/components/Input/FileV2.vue +234 -0
  53. package/components/Input/InputPhoneNumberV2.vue +164 -0
  54. package/components/Input/ListGroupSelection.vue +96 -0
  55. package/components/Input/NRICNumber.vue +53 -0
  56. package/components/Input/NewDate.vue +123 -0
  57. package/components/Input/Number.vue +124 -0
  58. package/components/Input/Password.vue +22 -0
  59. package/components/Input/PhoneNumber.vue +188 -0
  60. package/components/Input/VehicleNumber.vue +49 -0
  61. package/components/InputLabel.vue +22 -0
  62. package/components/InvitationForm.vue +359 -0
  63. package/components/InvitationMain.vue +310 -0
  64. package/components/Layout/Header.vue +129 -0
  65. package/components/Layout/NavigationDrawer.vue +44 -0
  66. package/components/ListItem.vue +35 -0
  67. package/components/ListView.vue +87 -0
  68. package/components/LocalPagination.vue +31 -0
  69. package/components/MemberMain.vue +459 -0
  70. package/components/NFC/NFCPatrolReportMain.vue +591 -0
  71. package/components/NFC/NFCPatrolRouteForm.vue +596 -0
  72. package/components/NFC/NFCPatrolRouteMain.vue +539 -0
  73. package/components/NFC/NFCTagForm.vue +236 -0
  74. package/components/NFC/NFCTagMain.vue +337 -0
  75. package/components/NFC/PatrolSettings.vue +130 -0
  76. package/components/NavigationItem.vue +83 -0
  77. package/components/NumberSettingField.vue +107 -0
  78. package/components/OnlineFormConfigurationForm.vue +290 -0
  79. package/components/OnlineFormsConfiguration.vue +429 -0
  80. package/components/PeopleForm.vue +452 -0
  81. package/components/PlaceholderComponent.vue +34 -0
  82. package/components/RolePermissionFormCreate.vue +161 -0
  83. package/components/RolePermissionFormPreviewUpdate.vue +183 -0
  84. package/components/RolePermissionMain.vue +361 -0
  85. package/components/SearchVehicleNumberUser.vue +91 -0
  86. package/components/ServiceProviderFormCreate.vue +154 -0
  87. package/components/ServiceProviderMain.vue +547 -0
  88. package/components/SignaturePad.vue +73 -0
  89. package/components/Snackbar.vue +23 -0
  90. package/components/SpecificAttr.vue +53 -0
  91. package/components/SupplyManagement.vue +292 -0
  92. package/components/SwitchContext.vue +108 -0
  93. package/components/TableList.vue +150 -0
  94. package/components/TableListSecondary.vue +164 -0
  95. package/components/TableMain.vue +142 -0
  96. package/components/TableWithButton.vue +94 -0
  97. package/components/VehicleUpdateMoreAction.vue +84 -0
  98. package/components/VideoPlayer.vue +125 -0
  99. package/components/VisitorForm.vue +659 -0
  100. package/components/VisitorFormSelection.vue +53 -0
  101. package/components/VisitorManagement.vue +490 -0
  102. package/components/WorkOrder/Create.vue +284 -0
  103. package/components/WorkOrder/Detail.vue +71 -0
  104. package/components/WorkOrder/ListView.vue +96 -0
  105. package/components/WorkOrder/Main.vue +489 -0
  106. package/components/Workorder.vue +1 -0
  107. package/composables/useAddress.ts +107 -0
  108. package/composables/useBuilding.ts +250 -0
  109. package/composables/useBuildingUnit.ts +116 -0
  110. package/composables/useCard.ts +46 -0
  111. package/composables/useCommonPermission.ts +207 -0
  112. package/composables/useCustomer.ts +113 -0
  113. package/composables/useCustomerSite.ts +56 -0
  114. package/composables/useDashboard.ts +31 -0
  115. package/composables/useDashboardData.ts +425 -0
  116. package/composables/useDocument.ts +57 -0
  117. package/composables/useFacility.ts +246 -0
  118. package/composables/useFeedback.ts +119 -0
  119. package/composables/useFile.ts +55 -0
  120. package/composables/useInvoice.ts +18 -0
  121. package/composables/useLocal.ts +131 -0
  122. package/composables/useLocalAuth.ts +137 -0
  123. package/composables/useLocalSetup.ts +13 -0
  124. package/composables/useMember.ts +111 -0
  125. package/composables/useNFCPatrolRoute.ts +77 -0
  126. package/composables/useNFCPatrolSettings.ts +19 -0
  127. package/composables/useNFCPatrolTag.ts +53 -0
  128. package/composables/useOnlineForm.ts +67 -0
  129. package/composables/useOrg.ts +129 -0
  130. package/composables/usePDFDownload.ts +25 -0
  131. package/composables/usePaymentMethod.ts +101 -0
  132. package/composables/usePeople.ts +81 -0
  133. package/composables/usePermission.ts +54 -0
  134. package/composables/usePhoneCountries.ts +561 -0
  135. package/composables/usePrice.ts +15 -0
  136. package/composables/usePromoCode.ts +36 -0
  137. package/composables/useRecapPermission.ts +26 -0
  138. package/composables/useRole.ts +104 -0
  139. package/composables/useSecurityUtils.ts +18 -0
  140. package/composables/useServiceProvider.ts +224 -0
  141. package/composables/useSite.ts +109 -0
  142. package/composables/useSiteEntryPassSettings.ts +46 -0
  143. package/composables/useSiteSettings.ts +123 -0
  144. package/composables/useSubscription.ts +150 -0
  145. package/composables/useUser.ts +132 -0
  146. package/composables/useUtils.ts +445 -0
  147. package/composables/useVerification.ts +34 -0
  148. package/composables/useVisitor.ts +120 -0
  149. package/composables/useWorkOrder.ts +85 -0
  150. package/error.vue +41 -0
  151. package/layouts/plain.vue +7 -0
  152. package/middleware/01.auth.ts +20 -0
  153. package/middleware/02.org.ts +21 -0
  154. package/middleware/03.customer.ts +13 -0
  155. package/middleware/member.ts +4 -0
  156. package/nuxt.config.ts +54 -0
  157. package/package.json +39 -0
  158. package/pages/index.vue +3 -0
  159. package/pages/payment-method-linked.vue +31 -0
  160. package/pages/require-customer.vue +56 -0
  161. package/pages/require-organization-membership.vue +47 -0
  162. package/pages/unauthorized.vue +29 -0
  163. package/plugins/API.ts +21 -0
  164. package/plugins/iconify.client.ts +5 -0
  165. package/plugins/secure-member.client.ts +86 -0
  166. package/plugins/vuetify.ts +62 -0
  167. package/public/bg-camera.jpg +0 -0
  168. package/public/bg-city.jpg +0 -0
  169. package/public/bg-condo.jpg +0 -0
  170. package/public/images/icons/delete-icon.png +0 -0
  171. package/public/sprite.svg +1 -0
  172. package/tsconfig.json +3 -0
  173. package/types/address.d.ts +13 -0
  174. package/types/building.d.ts +27 -0
  175. package/types/camera.d.ts +31 -0
  176. package/types/card.d.ts +22 -0
  177. package/types/customer.d.ts +27 -0
  178. package/types/document.d.ts +6 -0
  179. package/types/feedback.d.ts +68 -0
  180. package/types/local.d.ts +74 -0
  181. package/types/member.d.ts +21 -0
  182. package/types/online-form.d.ts +15 -0
  183. package/types/org.d.ts +13 -0
  184. package/types/people.d.ts +24 -0
  185. package/types/permission.d.ts +25 -0
  186. package/types/phone-number.d.ts +10 -0
  187. package/types/price.d.ts +17 -0
  188. package/types/promo-code.d.ts +19 -0
  189. package/types/role.d.ts +11 -0
  190. package/types/select.d.ts +4 -0
  191. package/types/service-provider.d.ts +15 -0
  192. package/types/site.d.ts +20 -0
  193. package/types/subscription.d.ts +23 -0
  194. package/types/user.d.ts +19 -0
  195. package/types/verification.d.ts +20 -0
  196. package/types/visitor.d.ts +42 -0
  197. package/types/work-order.d.ts +42 -0
  198. package/utils/phoneMasks.ts +1703 -0
@@ -0,0 +1,596 @@
1
+ <template>
2
+ <v-card width="100%" max-width="800">
3
+ <v-toolbar color="white" elevation="0">
4
+ <v-row no-gutters class="fill-height px-6" align="center">
5
+ <span class="font-weight-bold text-h5">
6
+ {{ title }}
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+
11
+ <v-divider />
12
+
13
+ <v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-6">
14
+ <v-form ref="formRef" v-model="valid" @submit.prevent="submit">
15
+ <v-row no-gutters>
16
+ <!-- Route Name -->
17
+ <v-col cols="12" class="mb-4">
18
+ <label class="text-caption text-grey-darken-1 mb-1 d-block">
19
+ Route Name <span class="text-red">*</span>
20
+ </label>
21
+ <v-text-field
22
+ v-model="form.name"
23
+ placeholder="Enter route name"
24
+ variant="outlined"
25
+ density="comfortable"
26
+ :rules="[rules.required]"
27
+ hide-details="auto"
28
+ />
29
+ </v-col>
30
+
31
+ <!-- Checkpoints, Tolerance, Repeat Every -->
32
+ <v-col cols="12" md="4" class="mb-4 pr-md-2">
33
+ <label class="text-caption text-grey-darken-1 mb-1 d-block">
34
+ Checkpoints <span class="text-red">*</span>
35
+ </label>
36
+ <v-select
37
+ v-model="form.checkPointNumber"
38
+ :items="checkpointOptions"
39
+ variant="outlined"
40
+ density="comfortable"
41
+ hide-details
42
+ @update:model-value="updateCheckpointsCount"
43
+ />
44
+ </v-col>
45
+
46
+ <v-col cols="12" md="4" class="mb-4 px-md-1">
47
+ <label class="text-caption text-grey-darken-1 mb-1 d-block">
48
+ Tolerance (mins) <span class="text-red">*</span>
49
+ </label>
50
+ <v-text-field
51
+ v-model="form.tolerance"
52
+ placeholder="60 mins"
53
+ variant="outlined"
54
+ density="comfortable"
55
+ hide-details
56
+ />
57
+ </v-col>
58
+
59
+ <v-col cols="12" md="4" class="mb-4 pl-md-2">
60
+ <label class="text-caption text-grey-darken-1 mb-1 d-block">
61
+ Days <span class="text-red">*</span>
62
+ </label>
63
+ <v-select
64
+ v-model="form.days"
65
+ :items="dayOptions"
66
+ multiple
67
+ variant="outlined"
68
+ density="comfortable"
69
+ hide-details
70
+ chips
71
+ />
72
+ </v-col>
73
+
74
+ <!-- Start Times -->
75
+ <v-col cols="12" class="mb-4">
76
+ <v-card variant="outlined" class="pa-4" rounded="lg">
77
+ <div class="d-flex align-center justify-space-between mb-3">
78
+ <span class="text-h6 font-weight-medium">Start Times</span>
79
+ <span class="text-body-2 text-grey-darken-1">Schedule</span>
80
+ </div>
81
+
82
+ <!-- Start Time Slots -->
83
+ <div
84
+ v-for="(startTime, index) in form.startTimes"
85
+ :key="startTimeKeys[index]"
86
+ >
87
+ <v-divider v-if="index > 0" class="mb-3" />
88
+ <v-row no-gutters class="mb-2 align-center">
89
+ <v-col cols="12" md="" lg="" class="pr-2">
90
+ <v-text-field
91
+ v-model="form.startTimes[index]"
92
+ label="Start Time"
93
+ placeholder="08:00"
94
+ variant="filled"
95
+ density="comfortable"
96
+ prepend-inner-icon="mdi-clock-outline"
97
+ hide-details
98
+ type="time"
99
+ bg-color="grey-lighten-4"
100
+ rounded="lg"
101
+ @update:model-value="syncEndTimeAt(index)"
102
+ />
103
+ </v-col>
104
+ <v-col
105
+ cols="12"
106
+ md=""
107
+ lg=""
108
+ class="pr-2 mt-4 mt-sm-2 mt-md-0"
109
+ >
110
+ <v-text-field
111
+ v-model="form.endTimes[index]"
112
+ label="End Time"
113
+ placeholder="08:00"
114
+ variant="filled"
115
+ density="comfortable"
116
+ prepend-inner-icon="mdi-clock-outline"
117
+ hide-details
118
+ type="time"
119
+ bg-color="grey-lighten-2"
120
+ rounded="lg"
121
+ readonly
122
+ />
123
+ </v-col>
124
+ <v-col
125
+ cols="12"
126
+ md="auto"
127
+ lg="auto"
128
+ class="d-flex justify-end mt-4 mt-sm-2 mt-md-0"
129
+ >
130
+ <v-btn
131
+ v-if="index === form.startTimes.length - 1"
132
+ icon
133
+ size="small"
134
+ color="primary"
135
+ variant="flat"
136
+ rounded="lg"
137
+ @click="addStartTime"
138
+ >
139
+ <v-icon size="large">mdi-plus</v-icon>
140
+ </v-btn>
141
+ <v-btn
142
+ v-else
143
+ icon
144
+ size="small"
145
+ color="error"
146
+ variant="flat"
147
+ rounded="lg"
148
+ @click="removeStartTime(index)"
149
+ >
150
+ <v-icon size="large">mdi-delete-outline</v-icon>
151
+ </v-btn>
152
+ </v-col>
153
+ </v-row>
154
+ </div>
155
+ </v-card>
156
+ </v-col>
157
+
158
+ <!-- Checkpoints Table -->
159
+ <v-col cols="12" class="mb-2">
160
+ <v-row no-gutters class="mb-2">
161
+ <v-col
162
+ cols="2"
163
+ class="text-caption text-grey-darken-1 font-weight-medium"
164
+ >
165
+ Checkpoints
166
+ </v-col>
167
+ <v-col
168
+ cols="6"
169
+ class="text-caption text-grey-darken-1 font-weight-medium pl-4"
170
+ >
171
+ NFC Tag
172
+ </v-col>
173
+ <v-col
174
+ cols="4"
175
+ class="text-caption text-grey-darken-1 font-weight-medium text-right"
176
+ >
177
+ Travel time
178
+ </v-col>
179
+ </v-row>
180
+ </v-col>
181
+
182
+ <v-col cols="12" class="mb-4">
183
+ <v-row
184
+ v-for="(checkpoint, index) in form.checkPoints"
185
+ :key="index"
186
+ no-gutters
187
+ class="mb-3 align-center"
188
+ >
189
+ <v-col cols="1" class="text-body-2">
190
+ {{ index + 1 }}
191
+ </v-col>
192
+ <v-col cols="7" class="pr-2">
193
+ <v-select
194
+ v-model="checkpoint.nfcTag_id"
195
+ :items="nfcTags"
196
+ item-title="name"
197
+ item-value="_id"
198
+ placeholder="Select NFC Tag"
199
+ variant="outlined"
200
+ density="compact"
201
+ hide-details
202
+ />
203
+ </v-col>
204
+ <v-col cols="4">
205
+ <v-text-field
206
+ v-model.number="checkpoint.travelTime"
207
+ placeholder="10"
208
+ variant="outlined"
209
+ density="compact"
210
+ hide-details
211
+ suffix="min"
212
+ type="number"
213
+ />
214
+ </v-col>
215
+ </v-row>
216
+ </v-col>
217
+
218
+ <!-- Summary -->
219
+ <v-col cols="12">
220
+ <v-row no-gutters class="mb-2">
221
+ <v-col cols="6" class="text-body-2 text-grey-darken-1">
222
+ Tolerance Balance:
223
+ <span class="font-weight-bold text-grey-darken-3"
224
+ >{{ toleranceBalance }} min</span
225
+ >
226
+ </v-col>
227
+ <v-col cols="6" class="text-body-2 text-grey-darken-1 text-right">
228
+ Total Travel Time:
229
+ <span class="font-weight-bold text-grey-darken-3"
230
+ >{{ totalTravelTime }} min</span
231
+ >
232
+ </v-col>
233
+ </v-row>
234
+ </v-col>
235
+ </v-row>
236
+ </v-form>
237
+ </v-card-text>
238
+
239
+ <v-divider />
240
+
241
+ <v-toolbar class="pa-0" density="compact" color="white" elevation="0">
242
+ <v-row no-gutters>
243
+ <v-col cols="6" class="pa-0">
244
+ <v-btn
245
+ block
246
+ variant="text"
247
+ class="text-none"
248
+ size="large"
249
+ @click="handleCancel"
250
+ height="56"
251
+ :disabled="loading"
252
+ rounded="0"
253
+ >
254
+ Cancel
255
+ </v-btn>
256
+ </v-col>
257
+
258
+ <v-col cols="6" class="pa-0">
259
+ <v-btn
260
+ block
261
+ variant="flat"
262
+ class="text-none"
263
+ size="large"
264
+ height="56"
265
+ color="primary"
266
+ @click="submit"
267
+ :loading="loading"
268
+ :disabled="!valid || loading"
269
+ rounded="0"
270
+ >
271
+ Submit
272
+ </v-btn>
273
+ </v-col>
274
+ </v-row>
275
+ </v-toolbar>
276
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
277
+ </v-card>
278
+ </template>
279
+
280
+ <script setup lang="ts">
281
+ import useNFCPatrolRoute from "../../composables/useNFCPatrolRoute";
282
+ import useNFCPatrolTag from "../../composables/useNFCPatrolTag";
283
+
284
+ const props = defineProps({
285
+ title: {
286
+ type: String,
287
+ default: "Add Route",
288
+ },
289
+ site: {
290
+ type: String,
291
+ required: true,
292
+ },
293
+ orgId: {
294
+ type: String,
295
+ required: true,
296
+ },
297
+ mode: {
298
+ type: String as PropType<"create" | "edit">,
299
+ default: "create",
300
+ },
301
+ route: {
302
+ type: Object as PropType<any>,
303
+ default: () => ({
304
+ routeName: "",
305
+ numberOfCheckpoints: 5,
306
+ tolerance: "",
307
+ repeatDays: "Mon, Tue, Wed, Thu, Friday",
308
+ schedules: [{ start: "13:00", end: "14:00" }],
309
+ checkpoints: [],
310
+ }),
311
+ },
312
+ });
313
+
314
+ const emit = defineEmits(["cancel", "success", "error"]);
315
+
316
+ const message = ref<string>("");
317
+ const messageSnackbar = ref<boolean>(false);
318
+ const messageColor = ref<string>("");
319
+
320
+ function showMessage(msg: string, color: string) {
321
+ message.value = msg;
322
+ messageColor.value = color;
323
+ messageSnackbar.value = true;
324
+ }
325
+
326
+ // Reset form initialized flag when canceling
327
+ function handleCancel() {
328
+ formInitialized.value = false;
329
+ emit("cancel");
330
+ }
331
+
332
+ const formRef = ref();
333
+ const valid = ref(false);
334
+ const loading = ref(false);
335
+
336
+ interface Checkpoint {
337
+ nfcTag_id: string;
338
+ travelTime: number;
339
+ }
340
+
341
+ const form = ref({
342
+ name: "",
343
+ checkPointNumber: 5,
344
+ tolerance: "60",
345
+ days: [1, 6] as number[],
346
+ startTimes: ["08:00"] as string[],
347
+ endTimes: ["09:00"] as string[],
348
+ checkPoints: [] as Checkpoint[],
349
+ });
350
+
351
+ const startTimeKeys = ref<number[]>([Date.now()]);
352
+
353
+ const checkpointOptions = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
354
+ const dayOptions = [
355
+ { title: "Monday", value: 1 },
356
+ { title: "Tuesday", value: 2 },
357
+ { title: "Wednesday", value: 3 },
358
+ { title: "Thursday", value: 4 },
359
+ { title: "Friday", value: 5 },
360
+ { title: "Saturday", value: 6 },
361
+ { title: "Sunday", value: 0 },
362
+ ];
363
+
364
+ const rules = {
365
+ required: (v: any) => !!v || "This field is required",
366
+ };
367
+
368
+ // Fetch NFC Tags
369
+ const nfcTags = ref<Array<any>>([]);
370
+ const { getAll: getAllNFCTags } = useNFCPatrolTag();
371
+
372
+ async function fetchNFCTags() {
373
+ try {
374
+ const response = await getAllNFCTags({ site: props.site, limit: 100 });
375
+ nfcTags.value = response.items || [];
376
+ } catch (error) {
377
+ console.error("Error fetching NFC tags:", error);
378
+ }
379
+ }
380
+
381
+ // Initialize checkpoints based on number
382
+ function updateCheckpointsCount(count: number) {
383
+ const currentLength = form.value.checkPoints.length;
384
+
385
+ if (count > currentLength) {
386
+ // Add new checkpoints
387
+ for (let i = currentLength; i < count; i++) {
388
+ form.value.checkPoints.push({
389
+ nfcTag_id: "",
390
+ travelTime: 10,
391
+ });
392
+ }
393
+ } else if (count < currentLength) {
394
+
395
+ form.value.checkPoints = form.value.checkPoints.slice(0, count);
396
+ }
397
+
398
+ }
399
+
400
+
401
+ const hasDuplicateStartTimes = computed(() => {
402
+ const uniqueTimes = new Set(form.value.startTimes);
403
+ return uniqueTimes.size !== form.value.startTimes.length;
404
+ });
405
+
406
+ function getEndTime(timeStr: string, minutesToAdd: number | null): string {
407
+ if (!timeStr) return "";
408
+
409
+ const [hh, mm] = timeStr.split(":").map(Number);
410
+ if (Number.isNaN(hh) || Number.isNaN(mm)) return timeStr;
411
+
412
+ const total = hh * 60 + mm + Number(minutesToAdd || 0);
413
+ const wrapped = ((total % 1440) + 1440) % 1440;
414
+
415
+ const outH = String(Math.floor(wrapped / 60)).padStart(2, "0");
416
+ const outM = String(wrapped % 60).padStart(2, "0");
417
+ return `${outH}:${outM}`;
418
+ }
419
+
420
+ function syncEndTimeAt(index: number) {
421
+ form.value.endTimes[index] = getEndTime(
422
+ form.value.startTimes[index],
423
+ minutesToAddForEndTime.value
424
+ );
425
+ }
426
+
427
+ const minutesToAddForEndTime = computed(() => {
428
+ return Number.parseInt(form.value.tolerance || "0", 10) || 0;
429
+ });
430
+
431
+ function syncAllEndTimes() {
432
+ if (hasDuplicateStartTimes.value)
433
+ return showMessage("Duplicate start times are not allowed.", "error");
434
+
435
+ const mins = minutesToAddForEndTime.value;
436
+ form.value.endTimes = form.value.startTimes.map((t) => getEndTime(t, mins));
437
+ }
438
+
439
+ watch(
440
+ [() => form.value.startTimes, () => minutesToAddForEndTime.value],
441
+ () => syncAllEndTimes(),
442
+
443
+ { deep: true, immediate: true }
444
+ );
445
+
446
+ function addStartTime() {
447
+ if (hasDuplicateStartTimes.value)
448
+ return showMessage("Duplicate start times are not allowed.", "error");
449
+
450
+ const times = form.value.startTimes;
451
+ let candidateTime = times.length > 0 ? times[times.length - 1] : "08:00";
452
+
453
+ if (times.length > 0) {
454
+ candidateTime = getEndTime(candidateTime, 1);
455
+ }
456
+
457
+ let safetyCounter = 0;
458
+ while (times.includes(candidateTime) && safetyCounter < 1440) {
459
+ candidateTime = getEndTime(candidateTime, 1);
460
+ safetyCounter++;
461
+ }
462
+
463
+ form.value.startTimes = [...times, candidateTime];
464
+ startTimeKeys.value = [...startTimeKeys.value, Date.now() + Math.random()];
465
+ }
466
+
467
+ function removeStartTime(index: number) {
468
+ form.value.startTimes.splice(index, 1);
469
+ form.value.endTimes.splice(index, 1);
470
+ startTimeKeys.value.splice(index, 1);
471
+ }
472
+
473
+ // Computed values
474
+ const totalTravelTime = computed(() => {
475
+ const total = form.value.checkPoints.reduce((sum, checkpoint) => {
476
+ return sum + (checkpoint.travelTime || 0);
477
+ }, 0);
478
+ return total;
479
+ });
480
+
481
+ const toleranceBalance = computed(() => {
482
+ const tolerance = parseInt(form.value.tolerance) || 0;
483
+ const balance = tolerance - totalTravelTime.value;
484
+ return balance;
485
+ });
486
+
487
+ // Initialize form
488
+ onMounted(() => {
489
+ fetchNFCTags();
490
+
491
+ if (props.mode === "create") {
492
+ updateCheckpointsCount(form.value.checkPointNumber);
493
+ }
494
+ });
495
+
496
+
497
+ const formInitialized = ref(false);
498
+
499
+ // Function to initialize form for edit mode
500
+ async function initializeEditForm(newRoute: any) {
501
+ if (!newRoute || !newRoute._id) return;
502
+
503
+ // Ensure NFC tags are loaded first
504
+ if (nfcTags.value.length === 0) {
505
+ await fetchNFCTags();
506
+ }
507
+
508
+ const startTimes =
509
+ Array.isArray(newRoute.startTimes) && newRoute.startTimes.length > 0
510
+ ? [...newRoute.startTimes]
511
+ : ["08:00"];
512
+
513
+ const endTimes = startTimes.map((t) =>
514
+ getEndTime(t, minutesToAddForEndTime.value)
515
+ );
516
+
517
+ const existingCheckPoints =
518
+ Array.isArray(newRoute.checkPoints) && newRoute.checkPoints.length > 0
519
+ ? newRoute.checkPoints.map((cp: any) => ({
520
+ nfcTag_id: cp.nfcTag_id || "",
521
+ travelTime: cp.travelTime || 10
522
+ }))
523
+ : [];
524
+
525
+ // Set form values
526
+ form.value.name = newRoute.name || "";
527
+ form.value.checkPointNumber = newRoute.checkPointNumber || existingCheckPoints.length || 5;
528
+ form.value.tolerance = newRoute.tolerance?.toString() || "60";
529
+ form.value.days = Array.isArray(newRoute.days) ? [...newRoute.days] : [1, 6];
530
+ form.value.startTimes = startTimes;
531
+ form.value.endTimes = endTimes;
532
+ form.value.checkPoints = existingCheckPoints;
533
+
534
+ // Reinitialize keys for each start time
535
+ startTimeKeys.value = startTimes.map(() => Date.now() + Math.random());
536
+
537
+ formInitialized.value = true;
538
+ }
539
+
540
+ watch(
541
+ () => props.route,
542
+ (newRoute) => {
543
+ if (props.mode === "edit" && newRoute && newRoute._id) {
544
+ // Reset initialized flag to allow re-initialization
545
+ formInitialized.value = false;
546
+ // Initialize form with route data
547
+ nextTick(() => {
548
+ initializeEditForm(newRoute);
549
+ });
550
+ }
551
+ },
552
+ { immediate: true, deep: true }
553
+ );
554
+
555
+ const { add: addRoute, updateById: updateRoute } = useNFCPatrolRoute();
556
+
557
+ async function submit() {
558
+ const { valid: isValid } = await formRef.value.validate();
559
+
560
+ if (!isValid) return;
561
+
562
+ try {
563
+ loading.value = true;
564
+
565
+
566
+ const uniqueStartTimes = [
567
+ ...new Set(
568
+ form.value.startTimes.filter((time) => time && time.trim() !== "")
569
+ ),
570
+ ];
571
+
572
+ const payload = {
573
+ name: form.value.name,
574
+ checkPointNumber: form.value.checkPointNumber,
575
+ tolerance: form.value.tolerance,
576
+ days: form.value.days,
577
+ startTimes: uniqueStartTimes,
578
+ checkPoints: form.value.checkPoints,
579
+ site: props.site,
580
+ };
581
+
582
+ if (props.mode === "create") {
583
+ await addRoute(payload);
584
+ } else {
585
+ await updateRoute(props.route._id, payload);
586
+ }
587
+
588
+ formInitialized.value = false;
589
+ emit("success");
590
+ } catch (error) {
591
+ emit("error", error);
592
+ } finally {
593
+ loading.value = false;
594
+ }
595
+ }
596
+ </script>