@7365admin1/layer-common 1.8.6 → 1.10.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.
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <div class="d-flex flex-column">
3
+ <v-text-field v-bind="$attrs" ref="datePickerRef" :model-value="dateFormattedReadOnly" autocomplete="off"
4
+ :placeholder="placeholder" :rules="rules" style="z-index: 10" @click="openDatePicker">
5
+ <template #append-inner>
6
+ <v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
7
+ </template>
8
+ </v-text-field>
9
+ <div class="w-100 d-flex align-end ga-3 hidden-input">
10
+ <input ref="dateInput" type="date" v-model="date" />
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+
17
+ const prop = defineProps({
18
+ rules: {
19
+ type: Array as PropType<Array<any>>,
20
+ default: () => []
21
+ },
22
+ placeholder: {
23
+ type: String,
24
+ default: 'MM/DD/YYYY'
25
+ }
26
+ })
27
+
28
+ const date = defineModel<string | null>({ default: null }) // YYYY-MM-DD format
29
+
30
+ const dateFormattedReadOnly = ref<string | null>(null)
31
+
32
+ const dateInput = ref<HTMLInputElement | null>(null)
33
+ const datePickerRef = ref<HTMLInputElement | null>(null)
34
+
35
+ const isInitialLoad = ref(true)
36
+
37
+ function openDatePicker() {
38
+ setTimeout(() => {
39
+ dateInput.value?.showPicker?.()
40
+ }, 0)
41
+ }
42
+
43
+ function validate() {
44
+ (datePickerRef.value as any)?.validate()
45
+ }
46
+
47
+ function convertToReadableFormat(dateStr: string): string {
48
+ if (!dateStr) return ""
49
+ const dateObj = new Date(dateStr + "T00:00:00")
50
+ const options: Intl.DateTimeFormatOptions = {
51
+ year: 'numeric',
52
+ month: '2-digit',
53
+ day: '2-digit'
54
+ }
55
+ return dateObj.toLocaleDateString('en-US', options)
56
+ }
57
+
58
+ function handleInitialDate() {
59
+ if (date.value) {
60
+ dateFormattedReadOnly.value = convertToReadableFormat(date.value)
61
+ } else {
62
+ dateFormattedReadOnly.value = null
63
+ }
64
+ }
65
+
66
+ watch(date, (dateVal) => {
67
+ if (isInitialLoad.value) return
68
+ if (!dateVal) {
69
+ dateFormattedReadOnly.value = null
70
+ return
71
+ }
72
+ dateFormattedReadOnly.value = convertToReadableFormat(dateVal)
73
+ }, { immediate: false })
74
+
75
+ watch(date, () => {
76
+ handleInitialDate()
77
+ }, { immediate: true })
78
+
79
+ onMounted(async () => {
80
+ await nextTick()
81
+ isInitialLoad.value = false
82
+ const nativeInput = (datePickerRef.value as any)?.$el?.querySelector('input')
83
+ if (nativeInput) {
84
+ nativeInput.addEventListener('click', (e: MouseEvent) => {
85
+ e.stopPropagation()
86
+ openDatePicker()
87
+ })
88
+ }
89
+ })
90
+
91
+ defineExpose({
92
+ validate
93
+ })
94
+ </script>
95
+
96
+ <style scoped>
97
+ .hidden-input {
98
+ opacity: 0;
99
+ height: 0;
100
+ width: 1px;
101
+ }
102
+ </style>
@@ -93,7 +93,7 @@
93
93
  <v-icon v-bind="props">mdi-dots-horizontal</v-icon>
94
94
  </template>
95
95
  <v-list>
96
- <v-list-item @click="openConfirmDialog(item._id ?? '')">
96
+ <v-list-item @click="openConfirmDialog(item._id ?? '')" v-if="item.status === 'pending'">
97
97
  Cancel Invite
98
98
  </v-list-item>
99
99
  </v-list>
@@ -118,21 +118,23 @@
118
118
  </p>
119
119
  </template>
120
120
  <template #footer>
121
- <v-btn
122
- variant="text"
123
- @click="confirmDialog = false"
124
- :disabled="cancelLoading"
125
- >
126
- Close
127
- </v-btn>
128
- <v-btn
129
- color="primary"
130
- variant="flat"
131
- @click="onConfirmCancel"
132
- :loading="cancelLoading"
133
- >
134
- Cancel Invite
135
- </v-btn>
121
+ <div class="d-flex justify-center ga-3 w-100">
122
+ <v-btn
123
+ variant="text"
124
+ @click="confirmDialog = false"
125
+ :disabled="cancelLoading"
126
+ >
127
+ Close
128
+ </v-btn>
129
+ <v-btn
130
+ color="primary"
131
+ variant="flat"
132
+ @click="onConfirmCancel"
133
+ :loading="cancelLoading"
134
+ >
135
+ Cancel Invite
136
+ </v-btn>
137
+ </div>
136
138
  </template>
137
139
  </ConfirmDialog>
138
140
  <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
@@ -245,7 +247,7 @@ const {
245
247
  status: props.status,
246
248
  search: headerSearch.value,
247
249
  type: "user-invite,member-invite",
248
- app: props.app
250
+ app: props.app,
249
251
  })
250
252
  );
251
253
 
@@ -0,0 +1,95 @@
1
+ <template>
2
+ <v-card width="100%" :disabled="loading || processingPassword" :loading="loading || processingPassword">
3
+ <v-toolbar density="compact">
4
+ <v-row no-gutters class="pa-3 text-h6 font-weight-bold">
5
+ <span>{{ promptTitle }}</span>
6
+ </v-row>
7
+ </v-toolbar>
8
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-5 px-7 text-center">
9
+ <v-row no-gutters class="w-100">
10
+ <v-col cols="12">
11
+ <v-icon icon="mdi-lock-outline" :color="'gray'" class="text-h2" />
12
+ </v-col>
13
+ <v-col cols="12" v-if="message" class="mt-2">
14
+ {{ message }}
15
+ </v-col>
16
+ <v-col cols="12" class="mt-5 text-start">
17
+ <v-text-field v-model="passwordInput" label="Password" type="password" density="compact" :error-messages="errorMessage"></v-text-field>
18
+ </v-col>
19
+ </v-row>
20
+ </v-card-text>
21
+
22
+ <v-toolbar class="pa-0" density="compact">
23
+ <v-row no-gutters>
24
+ <v-col cols="6" class="pa-0">
25
+ <v-btn block variant="text" class="text-none" size="large" tile @click="emit('cancel')" height="48">
26
+ Cancel
27
+ </v-btn>
28
+ </v-col>
29
+
30
+ <v-col cols="6" class="pa-0">
31
+ <v-btn block tile variant="flat" class="text-none" size="large" height="48" color="red" :disabled="!passwordInput"
32
+ @click="handleConfirm">
33
+ Confirm
34
+ </v-btn>
35
+ </v-col>
36
+ </v-row>
37
+ </v-toolbar>
38
+ </v-card>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+
43
+ const props = defineProps({
44
+ message: {
45
+ type: String,
46
+ default: "Confirm your digital signature with password to proceed."
47
+ },
48
+ promptTitle: {
49
+ type: String,
50
+ default: "Confirm Action"
51
+ },
52
+ loading: {
53
+ type: Boolean,
54
+ default: false
55
+ },
56
+ })
57
+
58
+ const { confirmPassword } = useDOBEntries();
59
+ const { currentUser } = useLocalAuth();
60
+
61
+ const emit = defineEmits(["cancel", "confirm"])
62
+
63
+ const processingPassword = ref(false)
64
+ const errorMessage = ref('')
65
+
66
+
67
+ const passwordInput = ref('')
68
+
69
+
70
+
71
+ async function handleConfirm() {
72
+ errorMessage.value = ''
73
+ try {
74
+ processingPassword.value = true;
75
+ const userId = currentUser.value?._id;
76
+ if (!userId) {
77
+ throw new Error('User not found. Please login again.');
78
+ }
79
+ const res = await confirmPassword({ userId, password: passwordInput.value });
80
+ if (res) {
81
+ emit('confirm');
82
+ }
83
+
84
+ } catch (err: any) {
85
+ const errMessage = err?.response?._data?.message || 'Something went wrong. Please try again.';
86
+ console.log('[ERROR]', err)
87
+ errorMessage.value = errMessage;
88
+ } finally {
89
+ processingPassword.value = false;
90
+ }
91
+ }
92
+
93
+ </script>
94
+
95
+ <style scoped></style>
@@ -119,21 +119,23 @@
119
119
  </template>
120
120
 
121
121
  <template #footer>
122
- <v-btn
123
- variant="text"
124
- @click="confirmDialog = false"
125
- :disabled="deleteLoading"
126
- >
127
- Close
128
- </v-btn>
129
- <v-btn
130
- color="primary"
131
- variant="flat"
132
- @click="handleDeleteRole"
133
- :loading="deleteLoading"
134
- >
135
- Delete Role
136
- </v-btn>
122
+ <div class="d-flex justify-center ga-3 w-100">
123
+ <v-btn
124
+ variant="text"
125
+ @click="confirmDialog = false"
126
+ :disabled="deleteLoading"
127
+ >
128
+ Close
129
+ </v-btn>
130
+ <v-btn
131
+ color="primary"
132
+ variant="flat"
133
+ @click="handleDeleteRole"
134
+ :loading="deleteLoading"
135
+ >
136
+ Delete Role
137
+ </v-btn>
138
+ </div>
137
139
  </template>
138
140
  </ConfirmDialog>
139
141
 
@@ -40,6 +40,7 @@
40
40
  </div>
41
41
 
42
42
  <v-text-field
43
+ v-model="searchText"
43
44
  placeholder="Search..."
44
45
  variant="outlined"
45
46
  density="comfortable"
@@ -420,19 +421,27 @@ const {
420
421
  invite: inviteServiceProvider,
421
422
  } = useServiceProvider();
422
423
 
423
- const loading = ref(true);
424
-
425
424
  const { getSiteById } = useSite();
426
425
 
426
+ const { getColorStatus, debounce } = useUtils();
427
+
427
428
  const { data: site } = await useLazyAsyncData(
428
429
  "get-site-by-id-" + props.siteId,
429
430
  () => getSiteById(props.siteId)
430
431
  );
431
432
 
432
- const { data: serviceProviderReq, refresh: _getAllServiceProvider } =
433
- await useLazyAsyncData("get-all-service-providers", () =>
434
- getAllServiceProvider({ siteId: props.siteId })
435
- );
433
+ const loading = computed(() => getAllReqStatus.value === "pending");
434
+
435
+ const {
436
+ data: serviceProviderReq,
437
+ refresh: _getAllServiceProvider,
438
+ status: getAllReqStatus,
439
+ } = await useLazyAsyncData("get-all-service-providers", () =>
440
+ getAllServiceProvider({
441
+ siteId: props.siteId,
442
+ search: searchText.value || "",
443
+ })
444
+ );
436
445
 
437
446
  watchEffect(() => {
438
447
  // console.log("serviceProviderReq", serviceProviderReq.value);
@@ -441,12 +450,14 @@ watchEffect(() => {
441
450
  pages.value = serviceProviderReq.value.pages;
442
451
  pageRange.value = serviceProviderReq.value.pageRange;
443
452
  }
444
- loading.value = false;
453
+ // loading.value = false;
445
454
  });
446
455
 
447
456
  const createDialog = ref(false);
448
457
  const createInviteDialog = ref(false);
449
458
 
459
+ const searchText = ref("");
460
+
450
461
  const success = () => {
451
462
  createDialog.value = false;
452
463
  // getServiceProvider();
@@ -544,4 +555,13 @@ async function submitServiceProviderInvite() {
544
555
  disableServiceProvider.value = false;
545
556
  }
546
557
  }
558
+
559
+ const debounceSearch = debounce(_getAllServiceProvider, 500);
560
+
561
+ watch(
562
+ () => searchText.value,
563
+ () => {
564
+ debounceSearch();
565
+ }
566
+ );
547
567
  </script>
@@ -1,10 +1,11 @@
1
1
  <template>
2
2
  <v-card width="100%" :loading="processing">
3
3
  <v-toolbar>
4
- <v-row no-gutters class="fill-height px-6" align="center">
4
+ <v-row no-gutters class="fill-height px-6 d-flex justify-space-between align-center" align="center">
5
5
  <span class="font-weight-bold text-h5 text-capitalize">
6
6
  {{ prop.mode }} {{ formatVisitorType(type) }}
7
7
  </span>
8
+ <ButtonClose @click="emit('close:all')" />
8
9
  </v-row>
9
10
  </v-toolbar>
10
11
  <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-5 my-3">
@@ -238,6 +239,7 @@ const emit = defineEmits([
238
239
  "done:more",
239
240
  "error",
240
241
  "close",
242
+ "close:all"
241
243
  ]);
242
244
 
243
245
  const visitor = reactive<Partial<TVisitorPayload>>({
@@ -114,7 +114,7 @@
114
114
 
115
115
  <v-dialog v-model="dialog.addVisitor" v-if="activeVisitorFormType" width="450" persistent>
116
116
  <VisitorForm mode="add" :org="orgId" :site="siteId" :type="activeVisitorFormType" @back="handleClickBack"
117
- @done="handleVisitorFormDone" @done:more="handleVisitorFormCreateMore" />
117
+ @done="handleVisitorFormDone" @done:more="handleVisitorFormCreateMore" @close:all="handleCloseAll" />
118
118
  </v-dialog>
119
119
 
120
120
  <v-dialog v-model="dialog.viewVisitor" width="450" persistent>
@@ -385,6 +385,11 @@ function showMessage(msg: string, color: string) {
385
385
  messageSnackbar.value = true;
386
386
  }
387
387
 
388
+ function handleCloseAll() {
389
+ dialog.showSelection = false;
390
+ dialog.addVisitor = false;
391
+ }
392
+
388
393
  function handleUpdateAutofillDetails(people: TPeople){
389
394
  dialog.vehicleNumberUsersList = false
390
395
  console.log('people', people)
@@ -20,19 +20,51 @@
20
20
  </v-col>
21
21
  </v-row>
22
22
  </div>
23
+ <WorkOrderCreate
24
+ v-model="showCreateDialog"
25
+ created-from="workOrder"
26
+ :work-order="_workOrder"
27
+ @update:work-order="(val: TWorkOrderCreate) => (_workOrder = val)"
28
+ :is-edit-mode="isEditMode"
29
+ :loading="isSubmitting"
30
+ :categories="serviceProviders"
31
+ :theme="theme"
32
+ :errored-images="erroredImages"
33
+ :max-files="5"
34
+ :message-fn="showMessage"
35
+ @close="handleCloseDialog"
36
+ @file-added="handleFileAdded"
37
+ @file-deleted="deleteFile"
38
+ @submit="submitWorkOrder"
39
+ />
40
+
41
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
23
42
  </template>
24
43
  <script lang="ts" setup>
44
+ import { useTheme } from "vuetify";
25
45
  const route = useRoute();
26
46
  const id = route.params.id;
27
47
 
28
48
  const {
29
49
  workOrder,
30
- workOrders,
31
50
  getWorkOrderById,
32
51
  getWorkOrders: _getWorkOrders,
52
+ updateWorkOrder,
53
+ createWorkOrder,
33
54
  } = useWorkOrder();
34
55
 
35
56
  const { getServiceProviderNames } = useServiceProvider();
57
+ const erroredImages = ref<string[]>([]);
58
+
59
+ const message = ref("");
60
+ const messageColor = ref("");
61
+ const messageSnackbar = ref(false);
62
+
63
+ function showMessage(msg: string, color: string) {
64
+ message.value = msg;
65
+ messageColor.value = color;
66
+ messageSnackbar.value = true;
67
+ }
36
68
 
37
69
  const _getWorkOrderById = async () => {
38
70
  try {
@@ -67,5 +99,150 @@ const showCreateDialog = ref(false);
67
99
  const showCompleteDialog = ref(false);
68
100
  const showDeleteDialog = ref(false);
69
101
 
70
- function openEditDialog() {}
102
+ const _workOrder = ref<TWorkOrderCreate>({
103
+ attachments: [],
104
+ category: "",
105
+ subject: "",
106
+ location: "",
107
+ description: "",
108
+ highPriority: false,
109
+ block: "",
110
+ level: "",
111
+ unit: "",
112
+ serviceProvider: "",
113
+ assignee: "",
114
+ organization: "",
115
+ site: "",
116
+ });
117
+
118
+ const isEditMode = ref(false);
119
+ const isSubmitting = ref(false);
120
+ const theme = useTheme().name;
121
+
122
+ const _workOrderId = ref<string | null>(null);
123
+
124
+ const serviceProviders = ref<
125
+ Array<{ title: string; value: string; subtitle: string }>
126
+ >([]);
127
+
128
+ const { getBySiteAsServiceProvider } = useCustomerSite();
129
+
130
+ const { data: getAllReq } = useLazyAsyncData(
131
+ "get-by-site-as-service-provider",
132
+ () => getBySiteAsServiceProvider(useRoute().params.site as string)
133
+ );
134
+
135
+ watchEffect(() => {
136
+ if (getAllReq.value) {
137
+ serviceProviders.value = getAllReq.value.map((i: any) => ({
138
+ title: i.nature.replace(/_/g, " "),
139
+ subtitle: i.title,
140
+ value: i.nature,
141
+ }));
142
+ }
143
+ });
144
+
145
+ function openEditDialog() {
146
+ isEditMode.value = true;
147
+ _workOrder.value = { ...workOrder.value };
148
+ showCreateDialog.value = true;
149
+ }
150
+
151
+ function handleCloseDialog() {
152
+ resetWorkOrderForm();
153
+ isEditMode.value = false;
154
+ showCreateDialog.value = false;
155
+ }
156
+
157
+ function resetWorkOrderForm() {
158
+ _workOrder.value = {
159
+ attachments: [],
160
+ category: "",
161
+ subject: "",
162
+ location: "",
163
+ description: "",
164
+ highPriority: false,
165
+ block: "",
166
+ level: "",
167
+ unit: "",
168
+ serviceProvider: "",
169
+ assignee: "",
170
+ organization: "",
171
+ site: "",
172
+ };
173
+ }
174
+
175
+ const { addFile, deleteFile: _deleteFile } = useFile();
176
+
177
+ const API_DO_STORAGE_ENDPOINT =
178
+ useRuntimeConfig().public.API_DO_STORAGE_ENDPOINT;
179
+
180
+ async function handleFileAdded(file: File) {
181
+ try {
182
+ const res = await addFile(file);
183
+ const uploadedId = res?.id;
184
+ if (uploadedId) {
185
+ const url = `${API_DO_STORAGE_ENDPOINT}/${uploadedId}`;
186
+ _workOrder.value.attachments = _workOrder.value.attachments ?? [];
187
+ _workOrder.value.attachments.push(url);
188
+ }
189
+ } catch (error) {
190
+ console.error("Error uploading file:", error);
191
+ showMessage("Failed to upload file", "error");
192
+ }
193
+ }
194
+
195
+ async function deleteFile(value: string) {
196
+ try {
197
+ await _deleteFile(value);
198
+ _workOrder.value.attachments = (_workOrder.value.attachments ?? []).filter(
199
+ (file) => file !== value
200
+ );
201
+ } catch (error) {
202
+ console.log(error);
203
+ showMessage("Failed to delete file", "error");
204
+ }
205
+ }
206
+
207
+ async function submitWorkOrder() {
208
+ if (!isEditMode.value || !workOrder.value?._id) {
209
+ console.warn("Update called without valid edit mode or work order ID.");
210
+ return;
211
+ }
212
+ try {
213
+ isSubmitting.value = true;
214
+
215
+ const payload = {
216
+ // ..._workOrder.value,
217
+ attachments: _workOrder.value.attachments,
218
+ category: _workOrder.value.category,
219
+ description: _workOrder.value.description,
220
+ highPriority: _workOrder.value.highPriority,
221
+ organization: route.params.org as string,
222
+ site: route.params.site as string,
223
+ subject: _workOrder.value.subject,
224
+ unit: _workOrder.value.unit,
225
+ };
226
+
227
+ let res: Record<string, any> = {};
228
+
229
+ if (isEditMode.value) {
230
+ console.log("updating...");
231
+ res = await updateWorkOrder(workOrder.value._id as string, payload);
232
+ } else {
233
+ res = await createWorkOrder(payload);
234
+ }
235
+
236
+ showMessage(res.message, "success");
237
+ showCreateDialog.value = false;
238
+
239
+ await _getWorkOrderById();
240
+
241
+ resetWorkOrderForm();
242
+ } catch (err) {
243
+ showMessage((err as Error).message, "error");
244
+ } finally {
245
+ isSubmitting.value = false;
246
+ }
247
+ }
71
248
  </script>