@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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.editorconfig +12 -0
- package/.github/workflows/main.yml +17 -0
- package/.github/workflows/publish.yml +39 -0
- package/.nuxtrc +1 -0
- package/.playground/app.vue +41 -0
- package/.playground/eslint.config.mjs +6 -0
- package/.playground/nuxt.config.ts +22 -0
- package/.playground/pages/feedback.vue +30 -0
- package/CHANGELOG.md +263 -0
- package/README.md +73 -0
- package/app.vue +3 -0
- package/components/AccessCardAddForm.vue +363 -0
- package/components/AccessManagement.vue +420 -0
- package/components/Avatar/Main.vue +68 -0
- package/components/BillingMain.vue +66 -0
- package/components/BtnUploadFile.vue +139 -0
- package/components/BuildingForm.vue +303 -0
- package/components/BuildingManagement/buildings.vue +335 -0
- package/components/BuildingManagement/units.vue +350 -0
- package/components/BuildingUnitFormAdd.vue +441 -0
- package/components/BuildingUnitFormEdit.vue +429 -0
- package/components/CameraForm.vue +264 -0
- package/components/CameraMain.vue +352 -0
- package/components/Card/DeleteConfirmation.vue +51 -0
- package/components/Card/MemberInfoSummary.vue +44 -0
- package/components/Card/Toggle.vue +25 -0
- package/components/Chat/Bubbles.vue +53 -0
- package/components/Chat/Information.vue +416 -0
- package/components/Chat/ListCard.vue +62 -0
- package/components/Chat/Message.vue +158 -0
- package/components/Chat/Navigation.vue +150 -0
- package/components/ConfirmDialog.vue +66 -0
- package/components/Container/Standard.vue +33 -0
- package/components/DashboardPlaceholder.vue +1524 -0
- package/components/Dialog/DeleteConfirmation.vue +51 -0
- package/components/Dialog/ReplaceAutofillPrompt.vue +49 -0
- package/components/Dialog/UpdateMoreAction.vue +103 -0
- package/components/DocumentForm.vue +187 -0
- package/components/DocumentManagement.vue +376 -0
- package/components/Editor.vue +95 -0
- package/components/EntryPassMain.vue +518 -0
- package/components/Feedback/Form.vue +173 -0
- package/components/FeedbackDetail.vue +599 -0
- package/components/FeedbackMain.vue +588 -0
- package/components/FormDialog.vue +65 -0
- package/components/ImageCarousel.vue +138 -0
- package/components/Input/Date.vue +177 -0
- package/components/Input/DateTimePicker.vue +131 -0
- package/components/Input/File.vue +236 -0
- package/components/Input/FileV2.vue +234 -0
- package/components/Input/InputPhoneNumberV2.vue +164 -0
- package/components/Input/ListGroupSelection.vue +96 -0
- package/components/Input/NRICNumber.vue +53 -0
- package/components/Input/NewDate.vue +123 -0
- package/components/Input/Number.vue +124 -0
- package/components/Input/Password.vue +22 -0
- package/components/Input/PhoneNumber.vue +188 -0
- package/components/Input/VehicleNumber.vue +49 -0
- package/components/InputLabel.vue +22 -0
- package/components/InvitationForm.vue +359 -0
- package/components/InvitationMain.vue +310 -0
- package/components/Layout/Header.vue +129 -0
- package/components/Layout/NavigationDrawer.vue +44 -0
- package/components/ListItem.vue +35 -0
- package/components/ListView.vue +87 -0
- package/components/LocalPagination.vue +31 -0
- package/components/MemberMain.vue +459 -0
- package/components/NFC/NFCPatrolReportMain.vue +591 -0
- package/components/NFC/NFCPatrolRouteForm.vue +596 -0
- package/components/NFC/NFCPatrolRouteMain.vue +539 -0
- package/components/NFC/NFCTagForm.vue +236 -0
- package/components/NFC/NFCTagMain.vue +337 -0
- package/components/NFC/PatrolSettings.vue +130 -0
- package/components/NavigationItem.vue +83 -0
- package/components/NumberSettingField.vue +107 -0
- package/components/OnlineFormConfigurationForm.vue +290 -0
- package/components/OnlineFormsConfiguration.vue +429 -0
- package/components/PeopleForm.vue +452 -0
- package/components/PlaceholderComponent.vue +34 -0
- package/components/RolePermissionFormCreate.vue +161 -0
- package/components/RolePermissionFormPreviewUpdate.vue +183 -0
- package/components/RolePermissionMain.vue +361 -0
- package/components/SearchVehicleNumberUser.vue +91 -0
- package/components/ServiceProviderFormCreate.vue +154 -0
- package/components/ServiceProviderMain.vue +547 -0
- package/components/SignaturePad.vue +73 -0
- package/components/Snackbar.vue +23 -0
- package/components/SpecificAttr.vue +53 -0
- package/components/SupplyManagement.vue +292 -0
- package/components/SwitchContext.vue +108 -0
- package/components/TableList.vue +150 -0
- package/components/TableListSecondary.vue +164 -0
- package/components/TableMain.vue +142 -0
- package/components/TableWithButton.vue +94 -0
- package/components/VehicleUpdateMoreAction.vue +84 -0
- package/components/VideoPlayer.vue +125 -0
- package/components/VisitorForm.vue +659 -0
- package/components/VisitorFormSelection.vue +53 -0
- package/components/VisitorManagement.vue +490 -0
- package/components/WorkOrder/Create.vue +284 -0
- package/components/WorkOrder/Detail.vue +71 -0
- package/components/WorkOrder/ListView.vue +96 -0
- package/components/WorkOrder/Main.vue +489 -0
- package/components/Workorder.vue +1 -0
- package/composables/useAddress.ts +107 -0
- package/composables/useBuilding.ts +250 -0
- package/composables/useBuildingUnit.ts +116 -0
- package/composables/useCard.ts +46 -0
- package/composables/useCommonPermission.ts +207 -0
- package/composables/useCustomer.ts +113 -0
- package/composables/useCustomerSite.ts +56 -0
- package/composables/useDashboard.ts +31 -0
- package/composables/useDashboardData.ts +425 -0
- package/composables/useDocument.ts +57 -0
- package/composables/useFacility.ts +246 -0
- package/composables/useFeedback.ts +119 -0
- package/composables/useFile.ts +55 -0
- package/composables/useInvoice.ts +18 -0
- package/composables/useLocal.ts +131 -0
- package/composables/useLocalAuth.ts +137 -0
- package/composables/useLocalSetup.ts +13 -0
- package/composables/useMember.ts +111 -0
- package/composables/useNFCPatrolRoute.ts +77 -0
- package/composables/useNFCPatrolSettings.ts +19 -0
- package/composables/useNFCPatrolTag.ts +53 -0
- package/composables/useOnlineForm.ts +67 -0
- package/composables/useOrg.ts +129 -0
- package/composables/usePDFDownload.ts +25 -0
- package/composables/usePaymentMethod.ts +101 -0
- package/composables/usePeople.ts +81 -0
- package/composables/usePermission.ts +54 -0
- package/composables/usePhoneCountries.ts +561 -0
- package/composables/usePrice.ts +15 -0
- package/composables/usePromoCode.ts +36 -0
- package/composables/useRecapPermission.ts +26 -0
- package/composables/useRole.ts +104 -0
- package/composables/useSecurityUtils.ts +18 -0
- package/composables/useServiceProvider.ts +224 -0
- package/composables/useSite.ts +109 -0
- package/composables/useSiteEntryPassSettings.ts +46 -0
- package/composables/useSiteSettings.ts +123 -0
- package/composables/useSubscription.ts +150 -0
- package/composables/useUser.ts +132 -0
- package/composables/useUtils.ts +445 -0
- package/composables/useVerification.ts +34 -0
- package/composables/useVisitor.ts +120 -0
- package/composables/useWorkOrder.ts +85 -0
- package/error.vue +41 -0
- package/layouts/plain.vue +7 -0
- package/middleware/01.auth.ts +20 -0
- package/middleware/02.org.ts +21 -0
- package/middleware/03.customer.ts +13 -0
- package/middleware/member.ts +4 -0
- package/nuxt.config.ts +54 -0
- package/package.json +39 -0
- package/pages/index.vue +3 -0
- package/pages/payment-method-linked.vue +31 -0
- package/pages/require-customer.vue +56 -0
- package/pages/require-organization-membership.vue +47 -0
- package/pages/unauthorized.vue +29 -0
- package/plugins/API.ts +21 -0
- package/plugins/iconify.client.ts +5 -0
- package/plugins/secure-member.client.ts +86 -0
- package/plugins/vuetify.ts +62 -0
- package/public/bg-camera.jpg +0 -0
- package/public/bg-city.jpg +0 -0
- package/public/bg-condo.jpg +0 -0
- package/public/images/icons/delete-icon.png +0 -0
- package/public/sprite.svg +1 -0
- package/tsconfig.json +3 -0
- package/types/address.d.ts +13 -0
- package/types/building.d.ts +27 -0
- package/types/camera.d.ts +31 -0
- package/types/card.d.ts +22 -0
- package/types/customer.d.ts +27 -0
- package/types/document.d.ts +6 -0
- package/types/feedback.d.ts +68 -0
- package/types/local.d.ts +74 -0
- package/types/member.d.ts +21 -0
- package/types/online-form.d.ts +15 -0
- package/types/org.d.ts +13 -0
- package/types/people.d.ts +24 -0
- package/types/permission.d.ts +25 -0
- package/types/phone-number.d.ts +10 -0
- package/types/price.d.ts +17 -0
- package/types/promo-code.d.ts +19 -0
- package/types/role.d.ts +11 -0
- package/types/select.d.ts +4 -0
- package/types/service-provider.d.ts +15 -0
- package/types/site.d.ts +20 -0
- package/types/subscription.d.ts +23 -0
- package/types/user.d.ts +19 -0
- package/types/verification.d.ts +20 -0
- package/types/visitor.d.ts +42 -0
- package/types/work-order.d.ts +42 -0
- 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>
|