@7365admin1/layer-common 1.10.9 → 1.11.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 (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardAddForm.vue +1 -1
  3. package/components/AccessCardAssignToUnitForm.vue +10 -13
  4. package/components/AccessCardQrTagging.vue +2 -2
  5. package/components/BulletinBoardManagement.vue +18 -8
  6. package/components/Chat/SkeletonLoader.vue +71 -0
  7. package/components/DashboardMain.vue +176 -0
  8. package/components/DeliveryCompany.vue +240 -0
  9. package/components/EntryPassInformation.vue +38 -8
  10. package/components/FeedbackMain.vue +4 -19
  11. package/components/FileInputWithList.vue +304 -0
  12. package/components/IncidentReport/Authorities.vue +189 -151
  13. package/components/IncidentReport/IncidentInformation.vue +28 -12
  14. package/components/IncidentReport/IncidentInformationDownload.vue +225 -0
  15. package/components/IncidentReport/affectedEntities.vue +13 -57
  16. package/components/Signature.vue +133 -0
  17. package/components/SiteSettings.vue +285 -0
  18. package/components/SlideCardGroup.vue +194 -0
  19. package/components/Tooltip/Info.vue +33 -0
  20. package/components/VisitorForm.vue +65 -3
  21. package/components/VisitorManagement.vue +23 -6
  22. package/composables/useAccessManagement.ts +44 -6
  23. package/composables/useBulletin.ts +8 -3
  24. package/composables/useBulletinBoardPermission.ts +48 -0
  25. package/composables/useCleaningPermission.ts +2 -0
  26. package/composables/useComment.ts +147 -0
  27. package/composables/useCommonPermission.ts +29 -1
  28. package/composables/useFeedback.ts +79 -29
  29. package/composables/useFile.ts +6 -0
  30. package/composables/usePDFDownload.ts +1 -1
  31. package/composables/useSiteSettings.ts +1 -1
  32. package/composables/useVisitor.ts +6 -5
  33. package/composables/useWorkOrder.ts +61 -26
  34. package/constants/app.ts +12 -0
  35. package/nuxt.config.ts +2 -0
  36. package/package.json +3 -1
  37. package/plugins/vue-draggable-next.client.ts +5 -0
  38. package/public/default-image.svg +4 -0
  39. package/public/placeholder-image.svg +6 -0
  40. package/types/comment.d.ts +38 -0
  41. package/types/dashboard.d.ts +12 -0
  42. package/types/feedback.d.ts +56 -20
  43. package/types/site.d.ts +2 -1
  44. package/types/work-order.d.ts +54 -18
  45. package/utils/data.ts +31 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 167c359: realease for ir download latest
8
+
9
+ ## 1.10.10
10
+
11
+ ### Patch Changes
12
+
13
+ - 276911d: Update Bulletin Board changes
14
+
3
15
  ## 1.10.9
4
16
 
5
17
  ### Patch Changes
@@ -655,7 +655,7 @@ watch(() => card.value.showAssign, async (newVal) => {
655
655
  });
656
656
  buildingsData.value = response.items || [];
657
657
  buildingItems.value = buildingsData.value.map((building: any) => ({
658
- name: building.name,
658
+ name: building.block,
659
659
  value: building._id,
660
660
  }));
661
661
  } catch (error) {
@@ -22,7 +22,7 @@
22
22
  hide-details="auto"
23
23
  item-title="name"
24
24
  item-value="value"
25
- placeholder="Select building..."
25
+ placeholder="Select block..."
26
26
  persistent-placeholder
27
27
  :rules="[requiredRule]"
28
28
  :loading="buildingLoading"
@@ -98,7 +98,6 @@
98
98
  <InputLabel
99
99
  class="text-capitalize font-weight-bold"
100
100
  title="Access Level"
101
- required
102
101
  />
103
102
  <v-select
104
103
  v-model="form.accessLevel"
@@ -107,7 +106,6 @@
107
106
  hide-details="auto"
108
107
  item-title="name"
109
108
  item-value="no"
110
- :rules="[requiredRule]"
111
109
  :loading="levelsLoading"
112
110
  placeholder="Select access level..."
113
111
  />
@@ -118,7 +116,6 @@
118
116
  <InputLabel
119
117
  class="text-capitalize font-weight-bold"
120
118
  title="Lift Access Level"
121
- required
122
119
  />
123
120
  <v-select
124
121
  v-model="form.liftAccessLevel"
@@ -127,7 +124,6 @@
127
124
  hide-details="auto"
128
125
  item-title="name"
129
126
  item-value="no"
130
- :rules="[requiredRule]"
131
127
  :loading="levelsLoading"
132
128
  placeholder="Select lift access level..."
133
129
  />
@@ -245,8 +241,9 @@ const typeItems = [
245
241
  { label: "Non Physical Access Card", value: "QRCODE" },
246
242
  ];
247
243
 
248
- const accessLevelItems = ref<{ name: string; no: string }[]>([]);
249
- const liftAccessLevelItems = ref<{ name: string; no: string }[]>([]);
244
+ const NO_SELECTION = { name: "No Selection", no: null };
245
+ const accessLevelItems = ref<{ name: string; no: string | null }[]>([]);
246
+ const liftAccessLevelItems = ref<{ name: string; no: string | null }[]>([]);
250
247
  const buildingItems = ref<{ name: string; value: string }[]>([]);
251
248
  const buildingsData = ref<Record<string, any>[]>([]);
252
249
  const levelItems = ref<{ name: string; value: string }[]>([]);
@@ -268,7 +265,7 @@ function maxQuantityRule(v: number) {
268
265
  }
269
266
 
270
267
  async function fetchAvailableCount() {
271
- if (!form.value.accessLevel || !form.value.liftAccessLevel) {
268
+ if (!form.value.accessLevel && !form.value.liftAccessLevel) {
272
269
  availableCount.value = null;
273
270
  return;
274
271
  }
@@ -315,7 +312,7 @@ onMounted(async () => {
315
312
  if (buildingsResult.status === "fulfilled") {
316
313
  buildingsData.value = buildingsResult.value.items || [];
317
314
  buildingItems.value = buildingsData.value.map((b: any) => ({
318
- name: b.name,
315
+ name: b.block,
319
316
  value: b._id,
320
317
  }));
321
318
  }
@@ -329,8 +326,8 @@ onMounted(async () => {
329
326
  _getDoorAccessLevels(acmUrl),
330
327
  _getLiftAccessLevels(acmUrl),
331
328
  ]);
332
- accessLevelItems.value = doorLevels.data ?? [];
333
- liftAccessLevelItems.value = liftLevels.data ?? [];
329
+ accessLevelItems.value = [NO_SELECTION, ...(doorLevels.data ?? [])];
330
+ liftAccessLevelItems.value = [NO_SELECTION, ...(liftLevels.data ?? [])];
334
331
  } catch {
335
332
  emit(
336
333
  "error",
@@ -423,8 +420,8 @@ async function submit() {
423
420
  type: form.value.type,
424
421
  site: siteId.value,
425
422
  userType: form.value.userType,
426
- accessLevel: form.value.accessLevel ?? "",
427
- liftAccessLevel: form.value.liftAccessLevel ?? "",
423
+ accessLevel: form.value.accessLevel,
424
+ liftAccessLevel: form.value.liftAccessLevel,
428
425
  });
429
426
  emit("success");
430
427
  } catch (error: any) {
@@ -158,7 +158,7 @@
158
158
  <qrcode-vue
159
159
  v-for="qrCode in qrCodesToGenerate"
160
160
  :key="qrCode._id"
161
- :value="qrCode.qrData"
161
+ :value="qrCode.cardNo"
162
162
  :size="150"
163
163
  :data-card-id="qrCode._id"
164
164
  />
@@ -340,7 +340,7 @@ async function generateQrCodes() {
340
340
  qrCodesToGenerate.value = filteredCards.map((card) => ({
341
341
  _id: card._id,
342
342
  cardNo: card.cardNo,
343
- qrData: card.qrData,
343
+ qrData: card.cardNo,
344
344
  qrTagCardNo: card.qrTagCardNo ?? "",
345
345
  }));
346
346
 
@@ -2,7 +2,7 @@
2
2
  <v-row no-gutters>
3
3
  <TableMain :headers="headers" :items="paginatePlaceholderItem" v-model:search="searchInput"
4
4
  :loading="getAnnouncementPending" :page="page" :pages="pages" :pageRange="pageRange"
5
- @refresh="getAnnouncementsRefresh" show-header @update:page="handleUpdatePage" @row-click="handleRowClick"
5
+ @refresh="getAnnouncementsRefresh" :show-header="APP_CONSTANTS.RESIDENT === props.recipients" @update:page="handleUpdatePage" @row-click="handleRowClick"
6
6
  @create="handleCreateEvent" :can-create="canCreateBulletinBoard" create-label="Add Announcement">
7
7
  <template #extension>
8
8
  <v-row no-gutters class="w-100 d-flex flex-column">
@@ -60,6 +60,8 @@
60
60
  </v-row>
61
61
  </template>
62
62
  <script setup lang="ts">
63
+ import { APP_CONSTANTS } from '../constants/app';
64
+
63
65
  definePageMeta({
64
66
  memberOnly: true,
65
67
  })
@@ -104,13 +106,21 @@ const { debounce } = useUtils()
104
106
 
105
107
 
106
108
 
107
- const headers = [
108
- { title: "Title", value: "title" },
109
- { title: "Date Created", value: "createdAt", align: "start" },
110
- { title: "Start Date/End Date", value: "duration", align: "start" },
111
- { title: "No Expiration", value: "noExpiration", align: "start" },
112
- { title: "", value: "actions" },
113
- ];
109
+ const headers = computed(() => {
110
+ const arr = [
111
+ { title: "Title", value: "title", align: "start" },
112
+ ]
113
+
114
+ if (props.recipients === APP_CONSTANTS.RESIDENT) {
115
+ arr.push({ title: "Date Created", value: "createdAt", align: "start" });
116
+ arr.push({ title: "Start Date/End Date", value: "duration", align: "start" });
117
+ arr.push({ title: "No Expiration", value: "noExpiration", align: "start" });
118
+ arr.push({ title: "", value: "actions", align: "center" });
119
+ }
120
+
121
+ return arr;
122
+
123
+ });
114
124
  const items = ref<TAnnouncement[]>([]);
115
125
  const page = ref(1);
116
126
  const pages = ref(0);
@@ -0,0 +1,71 @@
1
+ <template>
2
+ <div>
3
+ <!-- Chat Messages Skeleton Loader -->
4
+ <v-row no-gutters class="chat-content">
5
+ <v-col
6
+ v-for="(message, index) in chatSkeletons"
7
+ :key="index"
8
+ cols="12"
9
+ class="d-flex my-2"
10
+ :class="message.isRight ? 'justify-end' : 'justify-start'"
11
+ >
12
+ <v-skeleton-loader
13
+ class="rounded-lg border"
14
+ type="article"
15
+ width="400"
16
+ boilerplate
17
+ ></v-skeleton-loader>
18
+ </v-col>
19
+ </v-row>
20
+
21
+ <v-footer class="pa-0 chat-footer" color="background">
22
+ <v-row align="center" justify="space-between" no-gutters class="">
23
+ <v-col cols="9" class="pr-3">
24
+ <v-skeleton-loader
25
+ type="paragraph"
26
+ height="100"
27
+ class="rounded-lg border"
28
+ ></v-skeleton-loader>
29
+ </v-col>
30
+
31
+ <v-col cols="3" class="d-flex justify-end">
32
+ <v-skeleton-loader
33
+ color="background"
34
+ type="button"
35
+ width="150"
36
+ height="150"
37
+ class="rounded-lg"
38
+ ></v-skeleton-loader>
39
+ </v-col>
40
+ </v-row>
41
+ </v-footer>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup>
46
+ import { ref } from "vue";
47
+
48
+ const chatSkeletons = ref([
49
+ { isRight: false },
50
+ { isRight: true },
51
+ { isRight: false },
52
+ ]);
53
+ </script>
54
+
55
+ <style scoped>
56
+ .chat-content {
57
+ max-height: calc(100vh - (55px + 98px + 100px));
58
+ overflow-y: auto;
59
+ }
60
+
61
+ .chat-footer {
62
+ position: sticky;
63
+ bottom: 0;
64
+ width: 100%;
65
+ background-color: white;
66
+ border-top: 1px solid #ddd;
67
+ display: flex;
68
+ justify-content: center;
69
+ align-items: center;
70
+ }
71
+ </style>
@@ -0,0 +1,176 @@
1
+ <template>
2
+ <v-row no-gutters class="mb-6">
3
+ <v-col cols="12">
4
+ <h1 class="text-h4 text-md-h3 font-weight-bold">
5
+ Dashboard{{ currentSiteName ? ` - ${currentSiteName}` : "" }}
6
+ </h1>
7
+ </v-col>
8
+ </v-row>
9
+
10
+ <v-row>
11
+ <v-col cols="12">
12
+ <v-progress-linear
13
+ v-if="loading"
14
+ indeterminate
15
+ color="primary"
16
+ class="mb-4"
17
+ />
18
+ </v-col>
19
+ </v-row>
20
+
21
+ <v-row>
22
+ <v-col
23
+ v-for="card in countCardList"
24
+ :key="card.id"
25
+ cols="12"
26
+ sm="6"
27
+ md="6"
28
+ lg="4"
29
+ xl="2"
30
+ >
31
+ <v-card flat border class="fill-height" elevation="0" hover>
32
+ <v-card-text class="pa-4 pa-md-6">
33
+ <v-row no-gutters align="start">
34
+ <v-col cols="8">
35
+ <v-card-subtitle class="text-caption text-uppercase pa-0 mb-2">
36
+ {{ card.label }}
37
+ </v-card-subtitle>
38
+ <v-card-title class="text-h4 text-md-h3 pa-0 mb-3">
39
+ {{ card.value }}
40
+ </v-card-title>
41
+ <v-chip :color="card.chipColor" size="small" variant="flat">
42
+ <v-icon size="14" start>mdi-trending-up</v-icon>
43
+ {{ card.percentage }}
44
+ </v-chip>
45
+ </v-col>
46
+ <v-col cols="4" class="d-flex justify-end">
47
+ <v-avatar :color="card.color" size="56" rounded="lg">
48
+ <v-icon :icon="card.icon" color="white" size="28"></v-icon>
49
+ </v-avatar>
50
+ </v-col>
51
+ </v-row>
52
+ <v-row no-gutters class="mt-4">
53
+ <v-col cols="12">
54
+ <v-select
55
+ :model-value="card.period"
56
+ :items="periodOptions"
57
+ density="compact"
58
+ hide-details
59
+ variant="outlined"
60
+ rounded="lg"
61
+ :disabled="loading"
62
+ @update:model-value="onUpdateCardPeriod(card.key, $event)"
63
+ ></v-select>
64
+ </v-col>
65
+ </v-row>
66
+ </v-card-text>
67
+ </v-card>
68
+ </v-col>
69
+ </v-row>
70
+
71
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
72
+ </template>
73
+
74
+ <script setup lang="ts">
75
+
76
+ const props = defineProps({
77
+ orgId: {
78
+ type: String,
79
+ default: "",
80
+ },
81
+ site: {
82
+ type: String,
83
+ default: "",
84
+ },
85
+ dashboardData: {
86
+ type: Object as () => Record<string, any>,
87
+ default: () => ({}),
88
+ },
89
+ loading: {
90
+ type: Boolean,
91
+ default: false,
92
+ },
93
+ cardPeriods: {
94
+ type: Object as () => TPeriodState,
95
+ required: true,
96
+ },
97
+ currentSiteName: {
98
+ type: String,
99
+ default: "",
100
+ }, cardValues: {
101
+ type: Array as () => TDashboardCardValue[],
102
+ required: true,
103
+ },
104
+ })
105
+
106
+ const emit = defineEmits<{
107
+ updatePeriod: [payload: { key: string; period: string }];
108
+ }>();
109
+
110
+ const periodOptions = ["Today", "This Week", "This Month"];
111
+
112
+ const periodValue: Record<string, TDashboardValues> = {
113
+ Today: "today",
114
+ "This Week": "thisWeek",
115
+ "This Month": "thisMonth",
116
+ };
117
+
118
+ const datePeriod: Record<TDashboardValues, string> = {
119
+ today: "Today",
120
+ thisWeek: "This Week",
121
+ thisMonth: "This Month",
122
+ };
123
+
124
+ const message = ref("");
125
+ const messageSnackbar = ref(false);
126
+ const messageColor = ref("");
127
+
128
+ const countCardList = computed(() => {
129
+ const fetchedData = props.dashboardData || {};
130
+ const hasFetchedData = fetchedData && Object.keys(fetchedData).length > 0;
131
+
132
+ const sourceCards = hasFetchedData
133
+ ? buildCardsFromFetchedData(fetchedData)
134
+ : [];
135
+
136
+ return sourceCards.map((card: any, idx: number) => {
137
+ const periodKey = card.periodKey as keyof TPeriodState;
138
+ const apiPeriod = props.cardPeriods[periodKey] || "today";
139
+ const displayPeriod = datePeriod[apiPeriod] || "Today";
140
+
141
+ return {
142
+ id: idx + 1,
143
+ key: card.key,
144
+ label: card.title || `Card ${idx + 1}`,
145
+ value:
146
+ card.value >= 1000
147
+ ? card.value.toLocaleString()
148
+ : card.value.toString(),
149
+ icon: card.icon,
150
+ color: card.color,
151
+ percentage: `${Number(card.percentage) > 0 ? "+" : ""}${
152
+ card.percentage
153
+ }%`,
154
+ chipColor: card.isPositive ? "success" : "error",
155
+ period: displayPeriod,
156
+ };
157
+ });
158
+ });
159
+
160
+ function buildCardsFromFetchedData(data: Record<string, any>) {
161
+ return props.cardValues.map((mapping) => ({
162
+ key: mapping.key,
163
+ periodKey: mapping.periodKey,
164
+ title: mapping.title,
165
+ value: data[mapping.key]?.count ?? 0,
166
+ percentage: data[mapping.key]?.percentage ?? 0,
167
+ icon: mapping.icon,
168
+ color: mapping.color,
169
+ isPositive: (data[mapping.key]?.percentage ?? 0) >= 0,
170
+ }));
171
+ }
172
+
173
+ function onUpdateCardPeriod(cardKey: string, period: string) {
174
+ emit("updatePeriod", { key: cardKey, period });
175
+ }
176
+ </script>
@@ -0,0 +1,240 @@
1
+ <template>
2
+ <v-row no-gutters class="pa-3">
3
+ <v-col cols="12">
4
+ <v-card width="100%" variant="outlined" border="thin" rounded="lg" max-width="700" max-height="500"
5
+ style="overflow-y: auto;">
6
+ <v-toolbar density="compact" color="grey-lighten-4 pl-2 pr-4">
7
+ <v-row v-if="!hideTitle" no-gutters class="px-3 font-weight-bold">
8
+ Delivery Companies
9
+ </v-row>
10
+
11
+ <template #append>
12
+ <v-btn v-if="!props.readOnly" variant="flat" color="primary" class="text-none"
13
+ @click="handleAdd">
14
+ Add
15
+ </v-btn>
16
+ </template>
17
+ </v-toolbar>
18
+
19
+ <v-card-text class="pa-0" style="min-height: 100px;">
20
+ <template v-if="companies.length > 0">
21
+ <draggable :list="companies" item-key="index" class="drag-area"
22
+ :disabled="editingIndex !== null">
23
+ <v-list-item v-for="(item, index) in companies" :key="index" class="mt-1">
24
+ <v-row class="d-flex justify-space-between align-center" no-gutters>
25
+ <span class="d-flex align-center">
26
+ <v-btn icon="mdi-drag" class="drag-handle text-grey mr-2" flat
27
+ density="compact" />
28
+
29
+ <div v-if="editingIndex === index" class="w-full">
30
+ <v-text-field v-model="editedValue" class="text-subtitle-2"
31
+ prepend-icon="mdi-truck" style="min-width: 300px;" density="compact"
32
+ hide-details autofocus />
33
+ </div>
34
+
35
+ <div v-else>
36
+ <span>
37
+ <v-icon icon="mdi-truck" class="text-h6 mr-2" />
38
+ </span>
39
+ {{ item }}
40
+ </div>
41
+ </span>
42
+
43
+ <span class="d-flex align-center ga-2">
44
+ <template v-if="editingIndex === index">
45
+ <v-btn icon="mdi-check" color="green" density="compact"
46
+ @click="saveEdit(index)" />
47
+ <v-btn icon="mdi-close" color="grey" density="compact"
48
+ @click="cancelEdit" />
49
+ </template>
50
+
51
+ <template v-else>
52
+ <v-btn v-if="!props.readOnly" flat icon="mdi-pencil" class="text-grey"
53
+ density="compact" @click="startEdit(index)" />
54
+ <v-btn v-if="!props.readOnly" flat icon="mdi-trash-outline" class="text-red"
55
+ density="compact" @click="handleRemove(index)" />
56
+ </template>
57
+ </span>
58
+ </v-row>
59
+ </v-list-item>
60
+ </draggable>
61
+ </template>
62
+
63
+ <div v-else class="text-center py-5">
64
+ No delivery companies added yet.
65
+ </div>
66
+ </v-card-text>
67
+ </v-card>
68
+ </v-col>
69
+
70
+ <v-dialog v-model="dialog.add" persistent width="450">
71
+ <v-card width="100%">
72
+ <v-toolbar>
73
+ <v-row no-gutters class="fill-height px-6" align="center">
74
+ <span class="font-weight-bold text-h5 text-capitalize">
75
+ Add Delivery Company
76
+ </span>
77
+ </v-row>
78
+ </v-toolbar>
79
+
80
+ <v-card-text>
81
+ <v-text-field v-model="editedValue" placeholder="Company Name" autofocus />
82
+ </v-card-text>
83
+
84
+ <v-toolbar>
85
+ <v-row no-gutters justify="end" align="center" class="px-5">
86
+ <v-btn variant="text" text="Cancel" @click="dialog.add = false" />
87
+ <v-btn variant="flat" color="primary" class="text-none ml-2"
88
+ @click="saveEdit(companies.length)">
89
+ Add
90
+ </v-btn>
91
+ </v-row>
92
+ </v-toolbar>
93
+ </v-card>
94
+ </v-dialog>
95
+
96
+ <Snackbar v-model="toast.show" :text="toast.message" :color="toast.color" />
97
+
98
+ <v-dialog v-model="dialog.delete" persistent width="450">
99
+ <CardDeleteConfirmation
100
+ :prompt-title="`Are you sure you want to delete this ${companies?.[deleteIndex!]}?`"
101
+ @close="dialog.delete = false"
102
+ @delete="removeCompany(deleteIndex!)"
103
+ />
104
+ </v-dialog>
105
+ </v-row>
106
+ </template>
107
+
108
+ <script setup lang="ts">
109
+ import useSiteSettings from '../composables/useSiteSettings'
110
+
111
+ const props = defineProps<{
112
+ site: string
113
+ readOnly?: boolean
114
+ hideTitle?: boolean
115
+ }>()
116
+
117
+ const emit = defineEmits<{
118
+ (e: 'update:companiesValue', value: string[]): void
119
+ (e: 'refresh-site'): void
120
+ }>()
121
+
122
+ const { updateSitebyId } = useSiteSettings()
123
+
124
+ const initialCompanies = defineModel<string[]>('initial', {
125
+ type: Array,
126
+ default: []
127
+ })
128
+
129
+ const dialog = reactive({
130
+ add: false,
131
+ delete: false,
132
+ })
133
+
134
+ const toast = reactive({
135
+ show: false,
136
+ message: '',
137
+ color: 'success',
138
+ })
139
+
140
+ const companies = ref<string[]>([])
141
+ const editingIndex = ref<number | null>(null)
142
+ const editedValue = ref('')
143
+ const isHydrating = ref(true)
144
+ const deleteIndex = ref<number | null>(null)
145
+
146
+
147
+
148
+ watch(
149
+ companies,
150
+ async (newVal) => {
151
+ await persistCompanies()
152
+ },
153
+ { deep: true, immediate: false }
154
+ )
155
+
156
+ const startEdit = (index: number) => {
157
+ editingIndex.value = index
158
+ editedValue.value = companies.value[index] || ''
159
+ }
160
+
161
+ const saveEdit = async (index: number) => {
162
+ const value = editedValue.value.trim()
163
+ if (!value) return
164
+
165
+ const updated = [...companies.value]
166
+
167
+ if (index >= updated.length) {
168
+ updated.push(value)
169
+ } else {
170
+ updated[index] = value
171
+ }
172
+
173
+ companies.value = updated
174
+ editingIndex.value = null
175
+ editedValue.value = ''
176
+ dialog.add = false
177
+ }
178
+
179
+ const cancelEdit = () => {
180
+ editingIndex.value = null
181
+ editedValue.value = ''
182
+ }
183
+
184
+ const handleAdd = () => {
185
+ dialog.add = true
186
+ editedValue.value = ''
187
+ editingIndex.value = null
188
+ }
189
+
190
+ const removeCompany = async (index: number) => {
191
+ const updated = [...companies.value]
192
+ updated.splice(index, 1)
193
+ companies.value = updated
194
+ dialog.delete = false
195
+ }
196
+
197
+ const handleRemove = async (index: number) => {
198
+ deleteIndex.value = index;
199
+ dialog.delete = true;
200
+ }
201
+
202
+ const persistCompanies = async () => {
203
+ try {
204
+ const isSame =
205
+ companies.value.length === initialCompanies.value.length &&
206
+ companies.value.every((c, i) => c === initialCompanies.value[i]);
207
+ if (isSame) return
208
+
209
+
210
+ await updateSitebyId(props.site, {
211
+ deliveryCompanyList: companies.value,
212
+ })
213
+
214
+ toast.message = 'Delivery companies updated successfully!'
215
+ toast.color = 'success'
216
+ toast.show = true
217
+ emit('refresh-site')
218
+ } catch (error) {
219
+ console.error('Failed to update delivery companies:', error)
220
+ toast.message = 'Failed to update delivery companies. Please try again.'
221
+ toast.color = 'error'
222
+ toast.show = true
223
+ }
224
+ }
225
+
226
+ watch(initialCompanies, (val) => {
227
+ companies.value = Array.isArray(val) ? [...val] : []
228
+ isHydrating.value = false
229
+ }, { immediate: true })
230
+ </script>
231
+
232
+ <style scoped>
233
+ .drag-area {
234
+ cursor: grab;
235
+ }
236
+
237
+ .drag-handle {
238
+ cursor: grab;
239
+ }
240
+ </style>