@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.
- package/CHANGELOG.md +12 -0
- package/components/AccessCardAddForm.vue +1 -1
- package/components/AccessCardAssignToUnitForm.vue +10 -13
- package/components/AccessCardQrTagging.vue +2 -2
- package/components/BulletinBoardManagement.vue +18 -8
- package/components/Chat/SkeletonLoader.vue +71 -0
- package/components/DashboardMain.vue +176 -0
- package/components/DeliveryCompany.vue +240 -0
- package/components/EntryPassInformation.vue +38 -8
- package/components/FeedbackMain.vue +4 -19
- package/components/FileInputWithList.vue +304 -0
- package/components/IncidentReport/Authorities.vue +189 -151
- package/components/IncidentReport/IncidentInformation.vue +28 -12
- package/components/IncidentReport/IncidentInformationDownload.vue +225 -0
- package/components/IncidentReport/affectedEntities.vue +13 -57
- package/components/Signature.vue +133 -0
- package/components/SiteSettings.vue +285 -0
- package/components/SlideCardGroup.vue +194 -0
- package/components/Tooltip/Info.vue +33 -0
- package/components/VisitorForm.vue +65 -3
- package/components/VisitorManagement.vue +23 -6
- package/composables/useAccessManagement.ts +44 -6
- package/composables/useBulletin.ts +8 -3
- package/composables/useBulletinBoardPermission.ts +48 -0
- package/composables/useCleaningPermission.ts +2 -0
- package/composables/useComment.ts +147 -0
- package/composables/useCommonPermission.ts +29 -1
- package/composables/useFeedback.ts +79 -29
- package/composables/useFile.ts +6 -0
- package/composables/usePDFDownload.ts +1 -1
- package/composables/useSiteSettings.ts +1 -1
- package/composables/useVisitor.ts +6 -5
- package/composables/useWorkOrder.ts +61 -26
- package/constants/app.ts +12 -0
- package/nuxt.config.ts +2 -0
- package/package.json +3 -1
- package/plugins/vue-draggable-next.client.ts +5 -0
- package/public/default-image.svg +4 -0
- package/public/placeholder-image.svg +6 -0
- package/types/comment.d.ts +38 -0
- package/types/dashboard.d.ts +12 -0
- package/types/feedback.d.ts +56 -20
- package/types/site.d.ts +2 -1
- package/types/work-order.d.ts +54 -18
- package/utils/data.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
249
|
-
const
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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>
|