@7365admin1/layer-common 1.10.4 → 1.10.5
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 +6 -0
- package/components/AccessCardDeleteDialog.vue +109 -0
- package/components/AccessCardHistoryDialog.vue +133 -0
- package/components/AccessCardPreviewDialog.vue +12 -1
- package/components/AccessCardQrTagging.vue +183 -0
- package/components/AccessManagement.vue +18 -57
- package/components/BulletinBoardManagement.vue +6 -1
- package/components/CleaningScheduleMain.vue +18 -7
- package/components/IncidentReport/affectedEntities.vue +29 -0
- package/components/PlateNumberDisplay.vue +45 -0
- package/components/ScheduleAreaMain.vue +3 -0
- package/components/VehicleAddSelection.vue +58 -0
- package/components/VehicleForm.vue +600 -0
- package/components/VehicleManagement.vue +298 -0
- package/composables/useAccessManagement.ts +9 -1
- package/composables/useBulletin.ts +6 -4
- package/composables/useVehicle.ts +114 -0
- package/package.json +1 -1
- package/types/vehicle.d.ts +43 -0
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog :model-value="modelValue" width="450" persistent>
|
|
3
|
+
<v-card width="100%">
|
|
4
|
+
<v-toolbar density="compact" class="pl-4">
|
|
5
|
+
<span class="font-weight-medium text-h5">Delete Card</span>
|
|
6
|
+
</v-toolbar>
|
|
7
|
+
|
|
8
|
+
<v-card-text>
|
|
9
|
+
<p class="text-subtitle-2 text-center mb-4">
|
|
10
|
+
Are you sure you want to delete this card? This action cannot be
|
|
11
|
+
undone.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<v-form v-model="validForm" :disabled="loading">
|
|
15
|
+
<InputLabel class="text-capitalize font-weight-bold" title="Remarks" required />
|
|
16
|
+
<v-textarea
|
|
17
|
+
v-model="remarks"
|
|
18
|
+
placeholder="Enter remarks..."
|
|
19
|
+
persistent-placeholder
|
|
20
|
+
rows="3"
|
|
21
|
+
auto-grow
|
|
22
|
+
hide-details="auto"
|
|
23
|
+
:rules="[requiredRule]"
|
|
24
|
+
/>
|
|
25
|
+
</v-form>
|
|
26
|
+
|
|
27
|
+
<v-row v-if="error" no-gutters justify="center" class="mt-3">
|
|
28
|
+
<span class="text-caption text-error text-center">{{ error }}</span>
|
|
29
|
+
</v-row>
|
|
30
|
+
</v-card-text>
|
|
31
|
+
|
|
32
|
+
<v-toolbar density="compact">
|
|
33
|
+
<v-row no-gutters>
|
|
34
|
+
<v-col cols="6">
|
|
35
|
+
<v-btn
|
|
36
|
+
tile
|
|
37
|
+
block
|
|
38
|
+
size="48"
|
|
39
|
+
variant="text"
|
|
40
|
+
class="text-none"
|
|
41
|
+
:disabled="loading"
|
|
42
|
+
@click="handleClose"
|
|
43
|
+
>
|
|
44
|
+
Close
|
|
45
|
+
</v-btn>
|
|
46
|
+
</v-col>
|
|
47
|
+
<v-col cols="6">
|
|
48
|
+
<v-btn
|
|
49
|
+
tile
|
|
50
|
+
block
|
|
51
|
+
size="48"
|
|
52
|
+
color="black"
|
|
53
|
+
variant="flat"
|
|
54
|
+
class="text-none"
|
|
55
|
+
:loading="loading"
|
|
56
|
+
:disabled="!validForm || loading"
|
|
57
|
+
@click="handleConfirm"
|
|
58
|
+
>
|
|
59
|
+
Delete Card
|
|
60
|
+
</v-btn>
|
|
61
|
+
</v-col>
|
|
62
|
+
</v-row>
|
|
63
|
+
</v-toolbar>
|
|
64
|
+
</v-card>
|
|
65
|
+
</v-dialog>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
const props = defineProps({
|
|
70
|
+
modelValue: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
loading: {
|
|
75
|
+
type: Boolean,
|
|
76
|
+
default: false,
|
|
77
|
+
},
|
|
78
|
+
error: {
|
|
79
|
+
type: String,
|
|
80
|
+
default: "",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const emit = defineEmits<{
|
|
85
|
+
"update:modelValue": [value: boolean];
|
|
86
|
+
confirm: [remarks: string];
|
|
87
|
+
}>();
|
|
88
|
+
|
|
89
|
+
const { requiredRule } = useUtils();
|
|
90
|
+
|
|
91
|
+
const validForm = ref(false);
|
|
92
|
+
const remarks = ref("");
|
|
93
|
+
|
|
94
|
+
watch(
|
|
95
|
+
() => props.modelValue,
|
|
96
|
+
(val) => {
|
|
97
|
+
if (val) remarks.value = "";
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
function handleClose() {
|
|
102
|
+
emit("update:modelValue", false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleConfirm() {
|
|
106
|
+
if (!validForm.value) return;
|
|
107
|
+
emit("confirm", remarks.value.trim());
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog :model-value="modelValue" width="480" persistent>
|
|
3
|
+
<v-card width="100%">
|
|
4
|
+
<v-toolbar density="compact" color="black">
|
|
5
|
+
<v-toolbar-title class="text-subtitle-1 font-weight-medium">
|
|
6
|
+
Card History — {{ card?.cardNo ?? "N/A" }}
|
|
7
|
+
</v-toolbar-title>
|
|
8
|
+
<v-btn icon @click="emit('update:modelValue', false)">
|
|
9
|
+
<v-icon>mdi-close</v-icon>
|
|
10
|
+
</v-btn>
|
|
11
|
+
</v-toolbar>
|
|
12
|
+
|
|
13
|
+
<v-card-text style="max-height: 70vh; overflow-y: auto" class="pa-4">
|
|
14
|
+
<div v-if="pending" class="d-flex justify-center align-center py-8">
|
|
15
|
+
<v-progress-circular indeterminate color="black" />
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div v-else-if="!history.length" class="text-center text-grey py-8">
|
|
19
|
+
No history available.
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<v-timeline v-else side="end" density="compact" truncate-line="both">
|
|
23
|
+
<v-timeline-item
|
|
24
|
+
v-for="(item, index) in history"
|
|
25
|
+
:key="index"
|
|
26
|
+
:dot-color="actionColor(item.action)"
|
|
27
|
+
size="small"
|
|
28
|
+
>
|
|
29
|
+
<template #icon>
|
|
30
|
+
<v-icon size="14" color="white">{{ actionIcon(item.action) }}</v-icon>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<div class="mb-4">
|
|
34
|
+
<div class="d-flex align-center ga-2 mb-1">
|
|
35
|
+
<v-chip
|
|
36
|
+
:color="actionColor(item.action)"
|
|
37
|
+
variant="flat"
|
|
38
|
+
size="x-small"
|
|
39
|
+
class="text-capitalize font-weight-medium"
|
|
40
|
+
>
|
|
41
|
+
{{ actionLabel(item.action) }}
|
|
42
|
+
</v-chip>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="text-body-2">{{ item.performedBy?.name ?? "N/A" }}</div>
|
|
45
|
+
<div class="text-caption text-grey">{{ formatDate(item.date) }}</div>
|
|
46
|
+
</div>
|
|
47
|
+
</v-timeline-item>
|
|
48
|
+
</v-timeline>
|
|
49
|
+
</v-card-text>
|
|
50
|
+
</v-card>
|
|
51
|
+
</v-dialog>
|
|
52
|
+
</template>
|
|
53
|
+
|
|
54
|
+
<script setup lang="ts">
|
|
55
|
+
const props = defineProps({
|
|
56
|
+
modelValue: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: false,
|
|
59
|
+
},
|
|
60
|
+
card: {
|
|
61
|
+
type: Object as PropType<Record<string, any> | null>,
|
|
62
|
+
default: null,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
"update:modelValue": [value: boolean];
|
|
68
|
+
}>();
|
|
69
|
+
|
|
70
|
+
const { getCardHistory } = useAccessManagement();
|
|
71
|
+
|
|
72
|
+
const history = ref<Record<string, any>[]>([]);
|
|
73
|
+
const pending = ref(false);
|
|
74
|
+
|
|
75
|
+
watch(
|
|
76
|
+
() => props.modelValue,
|
|
77
|
+
async (val) => {
|
|
78
|
+
if (val && props.card?._id) {
|
|
79
|
+
pending.value = true;
|
|
80
|
+
try {
|
|
81
|
+
const data = await getCardHistory(props.card._id);
|
|
82
|
+
history.value = Array.isArray(data) ? data : [];
|
|
83
|
+
} finally {
|
|
84
|
+
pending.value = false;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
history.value = [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
function actionLabel(action: string) {
|
|
93
|
+
const map: Record<string, string> = {
|
|
94
|
+
available: "Created",
|
|
95
|
+
assign: "Assigned",
|
|
96
|
+
replace: "Replaced",
|
|
97
|
+
deleted: "Deleted",
|
|
98
|
+
};
|
|
99
|
+
return map[action] ?? action;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function actionColor(action: string) {
|
|
103
|
+
const map: Record<string, string> = {
|
|
104
|
+
available: "success",
|
|
105
|
+
assign: "primary",
|
|
106
|
+
replace: "orange",
|
|
107
|
+
deleted: "error",
|
|
108
|
+
};
|
|
109
|
+
return map[action] ?? "grey";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function actionIcon(action: string) {
|
|
113
|
+
const map: Record<string, string> = {
|
|
114
|
+
available: "mdi-plus",
|
|
115
|
+
assign: "mdi-account-check",
|
|
116
|
+
replace: "mdi-swap-horizontal",
|
|
117
|
+
deleted: "mdi-delete",
|
|
118
|
+
};
|
|
119
|
+
return map[action] ?? "mdi-circle-small";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatDate(date: string) {
|
|
123
|
+
if (!date) return "N/A";
|
|
124
|
+
return new Intl.DateTimeFormat("en-GB", {
|
|
125
|
+
day: "2-digit",
|
|
126
|
+
month: "short",
|
|
127
|
+
year: "numeric",
|
|
128
|
+
hour: "2-digit",
|
|
129
|
+
minute: "2-digit",
|
|
130
|
+
hour12: true,
|
|
131
|
+
}).format(new Date(date));
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
|
+
<AccessCardHistoryDialog
|
|
3
|
+
v-model="historyDialog"
|
|
4
|
+
:card="selectedCardInUnit"
|
|
5
|
+
/>
|
|
6
|
+
|
|
2
7
|
<v-dialog :model-value="modelValue" width="450" persistent>
|
|
3
8
|
<v-card width="100%">
|
|
4
9
|
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
|
|
@@ -214,7 +219,7 @@
|
|
|
214
219
|
Replace Card
|
|
215
220
|
</v-list-item-title>
|
|
216
221
|
</v-list-item>
|
|
217
|
-
<v-list-item :disabled="
|
|
222
|
+
<v-list-item :disabled="!isSelectedCardPhysical" @click="historyDialog = true">
|
|
218
223
|
<v-list-item-title class="text-subtitle-2 cursor-pointer">
|
|
219
224
|
Card History
|
|
220
225
|
</v-list-item-title>
|
|
@@ -268,6 +273,10 @@ const props = defineProps({
|
|
|
268
273
|
type: Boolean,
|
|
269
274
|
default: false,
|
|
270
275
|
},
|
|
276
|
+
isSelectedCardPhysical: {
|
|
277
|
+
type: Boolean,
|
|
278
|
+
default: false,
|
|
279
|
+
},
|
|
271
280
|
});
|
|
272
281
|
|
|
273
282
|
const emit = defineEmits<{
|
|
@@ -277,6 +286,8 @@ const emit = defineEmits<{
|
|
|
277
286
|
delete: [];
|
|
278
287
|
}>();
|
|
279
288
|
|
|
289
|
+
const historyDialog = ref(false);
|
|
290
|
+
|
|
280
291
|
const isSelectedCardDeletable = computed(() => {
|
|
281
292
|
if (!props.selectedCardInUnit?._id) return false;
|
|
282
293
|
const id = props.selectedCardInUnit._id;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters>
|
|
3
|
+
<v-col cols="12" class="mb-2">
|
|
4
|
+
<v-row no-gutters align="center" justify="space-between">
|
|
5
|
+
<v-row no-gutters class="ga-2">
|
|
6
|
+
<v-btn
|
|
7
|
+
class="text-none"
|
|
8
|
+
rounded="pill"
|
|
9
|
+
variant="tonal"
|
|
10
|
+
size="large"
|
|
11
|
+
:disabled="!items.length"
|
|
12
|
+
>
|
|
13
|
+
Print All
|
|
14
|
+
</v-btn>
|
|
15
|
+
<v-btn
|
|
16
|
+
class="text-none"
|
|
17
|
+
rounded="pill"
|
|
18
|
+
variant="tonal"
|
|
19
|
+
size="large"
|
|
20
|
+
:disabled="!selected.length"
|
|
21
|
+
>
|
|
22
|
+
Generate QR Code
|
|
23
|
+
</v-btn>
|
|
24
|
+
<v-btn
|
|
25
|
+
class="text-none"
|
|
26
|
+
rounded="pill"
|
|
27
|
+
variant="tonal"
|
|
28
|
+
size="large"
|
|
29
|
+
:disabled="!selected.length"
|
|
30
|
+
>
|
|
31
|
+
Print QR Code
|
|
32
|
+
</v-btn>
|
|
33
|
+
</v-row>
|
|
34
|
+
|
|
35
|
+
<v-row no-gutters class="ga-2" justify="end" style="max-width: 420px">
|
|
36
|
+
<v-select
|
|
37
|
+
v-model="typeFilter"
|
|
38
|
+
:items="typeOptions"
|
|
39
|
+
placeholder="Type"
|
|
40
|
+
variant="outlined"
|
|
41
|
+
density="comfortable"
|
|
42
|
+
hide-details
|
|
43
|
+
clearable
|
|
44
|
+
style="max-width: 160px"
|
|
45
|
+
/>
|
|
46
|
+
<v-text-field
|
|
47
|
+
v-model="searchText"
|
|
48
|
+
placeholder="Search Card..."
|
|
49
|
+
variant="outlined"
|
|
50
|
+
density="comfortable"
|
|
51
|
+
clearable
|
|
52
|
+
hide-details
|
|
53
|
+
style="max-width: 240px"
|
|
54
|
+
/>
|
|
55
|
+
</v-row>
|
|
56
|
+
</v-row>
|
|
57
|
+
</v-col>
|
|
58
|
+
|
|
59
|
+
<v-col cols="12">
|
|
60
|
+
<v-card
|
|
61
|
+
width="100%"
|
|
62
|
+
variant="outlined"
|
|
63
|
+
border="thin"
|
|
64
|
+
rounded="lg"
|
|
65
|
+
:loading="loading"
|
|
66
|
+
>
|
|
67
|
+
<v-toolbar density="compact" color="grey-lighten-4">
|
|
68
|
+
<template #prepend>
|
|
69
|
+
<v-btn fab icon density="comfortable" @click="fetchItems">
|
|
70
|
+
<v-icon>mdi-refresh</v-icon>
|
|
71
|
+
</v-btn>
|
|
72
|
+
</template>
|
|
73
|
+
|
|
74
|
+
<template #append>
|
|
75
|
+
<v-row no-gutters justify="end" align="center">
|
|
76
|
+
<span class="mr-2 text-caption text-fontgray">
|
|
77
|
+
{{ pageRange }}
|
|
78
|
+
</span>
|
|
79
|
+
<local-pagination
|
|
80
|
+
v-model="page"
|
|
81
|
+
:length="pages"
|
|
82
|
+
@update:value="fetchItems"
|
|
83
|
+
/>
|
|
84
|
+
</v-row>
|
|
85
|
+
</template>
|
|
86
|
+
</v-toolbar>
|
|
87
|
+
|
|
88
|
+
<v-data-table
|
|
89
|
+
v-model="selected"
|
|
90
|
+
:headers="tableHeaders"
|
|
91
|
+
:items="items"
|
|
92
|
+
item-value="_id"
|
|
93
|
+
items-per-page="10"
|
|
94
|
+
fixed-header
|
|
95
|
+
hide-default-footer
|
|
96
|
+
show-select
|
|
97
|
+
style="max-height: calc(100vh - 200px)"
|
|
98
|
+
>
|
|
99
|
+
<template #item.card="{ item }">
|
|
100
|
+
<v-row no-gutters align="center" class="ga-2">
|
|
101
|
+
<v-icon size="18" color="grey-darken-1">mdi-card-account-details</v-icon>
|
|
102
|
+
<span>{{ item.hid ?? "N/A" }}</span>
|
|
103
|
+
</v-row>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<template #item.qrCode="{ item }">
|
|
107
|
+
<v-icon
|
|
108
|
+
:color="item.qrCode ? 'success' : 'grey-lighten-1'"
|
|
109
|
+
size="22"
|
|
110
|
+
>
|
|
111
|
+
{{ item.qrCode ? "mdi-check-circle" : "mdi-circle-outline" }}
|
|
112
|
+
</v-icon>
|
|
113
|
+
</template>
|
|
114
|
+
</v-data-table>
|
|
115
|
+
</v-card>
|
|
116
|
+
</v-col>
|
|
117
|
+
|
|
118
|
+
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
119
|
+
</v-row>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<script setup lang="ts">
|
|
123
|
+
definePageMeta({
|
|
124
|
+
middleware: ["01-auth", "02-org"],
|
|
125
|
+
memberOnly: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const tableHeaders = [
|
|
129
|
+
{ title: "Card", key: "card", sortable: false },
|
|
130
|
+
{ title: "Location", key: "location", sortable: false },
|
|
131
|
+
{ title: "Card No.", key: "cardNo", sortable: false },
|
|
132
|
+
{ title: "HID", key: "hid", sortable: false },
|
|
133
|
+
{ title: "QR Code", key: "qrCode", sortable: false },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const typeOptions = ["Physical", "Non-Physical"];
|
|
137
|
+
|
|
138
|
+
const route = useRoute();
|
|
139
|
+
const siteId = computed(() => route.params.site as string);
|
|
140
|
+
const orgId = computed(() => route.params.org as string);
|
|
141
|
+
|
|
142
|
+
const page = ref(1);
|
|
143
|
+
const pages = ref(0);
|
|
144
|
+
const pageRange = ref("-- - -- of --");
|
|
145
|
+
|
|
146
|
+
const message = ref("");
|
|
147
|
+
const messageSnackbar = ref(false);
|
|
148
|
+
const messageColor = ref("");
|
|
149
|
+
|
|
150
|
+
const items = ref<Record<string, any>[]>([]);
|
|
151
|
+
const selected = ref<string[]>([]);
|
|
152
|
+
const searchText = ref("");
|
|
153
|
+
const typeFilter = ref<string | null>(null);
|
|
154
|
+
|
|
155
|
+
const {
|
|
156
|
+
data: qrTaggingReq,
|
|
157
|
+
refresh: fetchItems,
|
|
158
|
+
status: fetchStatus,
|
|
159
|
+
} = useLazyAsyncData(
|
|
160
|
+
"get-qr-tagging",
|
|
161
|
+
() => {
|
|
162
|
+
// TODO: wire up QR tagging API
|
|
163
|
+
return Promise.resolve({ data: { items: [], pages: 0, pageRange: "0 - 0 of 0" } });
|
|
164
|
+
},
|
|
165
|
+
{ watch: [page, searchText, typeFilter] }
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const loading = computed(() => fetchStatus.value === "pending");
|
|
169
|
+
|
|
170
|
+
watchEffect(() => {
|
|
171
|
+
if (qrTaggingReq.value?.data) {
|
|
172
|
+
items.value = qrTaggingReq.value.data.items;
|
|
173
|
+
pages.value = qrTaggingReq.value.data.pages;
|
|
174
|
+
pageRange.value = qrTaggingReq.value.data.pageRange;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
function showMessage(msg: string, color: string) {
|
|
179
|
+
message.value = msg;
|
|
180
|
+
messageColor.value = color;
|
|
181
|
+
messageSnackbar.value = true;
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
:can-replace-access-card="canReplaceAccessCard"
|
|
112
112
|
:can-delete-access-card="canDeleteAccessCard"
|
|
113
113
|
:is-selected-card-assigned-physical="isSelectedCardAssignedPhysical"
|
|
114
|
+
:is-selected-card-physical="isSelectedCardPhysical"
|
|
114
115
|
@replace="openReplaceDialog()"
|
|
115
116
|
@delete="openDeleteDialog()"
|
|
116
117
|
/>
|
|
@@ -129,61 +130,12 @@
|
|
|
129
130
|
</v-dialog>
|
|
130
131
|
|
|
131
132
|
<!-- Delete Dialog -->
|
|
132
|
-
<
|
|
133
|
+
<AccessCardDeleteDialog
|
|
133
134
|
v-model="confirmDialog"
|
|
134
135
|
:loading="deleteLoading"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
<v-card width="100%">
|
|
139
|
-
<v-toolbar density="compact" class="pl-4">
|
|
140
|
-
<span class="font-weight-medium text-h5">Delete Card</span>
|
|
141
|
-
</v-toolbar>
|
|
142
|
-
<v-card-text>
|
|
143
|
-
<p class="text-subtitle-2 text-center">
|
|
144
|
-
Are you sure you want to delete this card? This action cannot be
|
|
145
|
-
undone.
|
|
146
|
-
</p>
|
|
147
|
-
|
|
148
|
-
<v-row v-if="message" no-gutters justify="center" class="mt-4">
|
|
149
|
-
<span class="text-caption text-error text-center">
|
|
150
|
-
{{ message }}
|
|
151
|
-
</span>
|
|
152
|
-
</v-row></v-card-text
|
|
153
|
-
>
|
|
154
|
-
<v-toolbar density="compact">
|
|
155
|
-
<v-row no-gutters>
|
|
156
|
-
<v-col cols="6">
|
|
157
|
-
<v-btn
|
|
158
|
-
tile
|
|
159
|
-
block
|
|
160
|
-
size="48"
|
|
161
|
-
variant="text"
|
|
162
|
-
class="text-none"
|
|
163
|
-
@click="confirmDialog = false"
|
|
164
|
-
:disabled="deleteLoading"
|
|
165
|
-
>
|
|
166
|
-
Close
|
|
167
|
-
</v-btn>
|
|
168
|
-
</v-col>
|
|
169
|
-
<v-col cols="6">
|
|
170
|
-
<v-btn
|
|
171
|
-
tile
|
|
172
|
-
block
|
|
173
|
-
size="48"
|
|
174
|
-
color="black"
|
|
175
|
-
variant="flat"
|
|
176
|
-
class="text-none"
|
|
177
|
-
@click="handleDeleteCard"
|
|
178
|
-
:loading="deleteLoading"
|
|
179
|
-
>
|
|
180
|
-
Delete Card
|
|
181
|
-
</v-btn>
|
|
182
|
-
</v-col>
|
|
183
|
-
</v-row></v-toolbar
|
|
184
|
-
>
|
|
185
|
-
</v-card>
|
|
186
|
-
</v-dialog>
|
|
136
|
+
:error="deleteError"
|
|
137
|
+
@confirm="handleDeleteCard"
|
|
138
|
+
/>
|
|
187
139
|
|
|
188
140
|
<Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
|
|
189
141
|
</v-row>
|
|
@@ -276,6 +228,7 @@ const createDialog = ref(false);
|
|
|
276
228
|
const assignDialog = ref(false);
|
|
277
229
|
const previewDialog = ref(false);
|
|
278
230
|
const deleteLoading = ref(false);
|
|
231
|
+
const deleteError = ref("");
|
|
279
232
|
const confirmDialog = ref(false);
|
|
280
233
|
const searchText = ref("");
|
|
281
234
|
const replaceDialog = ref(false);
|
|
@@ -300,6 +253,14 @@ const isSelectedCardAssignedPhysical = computed(() =>
|
|
|
300
253
|
) ?? false
|
|
301
254
|
);
|
|
302
255
|
|
|
256
|
+
const isSelectedCardPhysical = computed(() =>
|
|
257
|
+
["available", "assigned", "replaced", "deleted"].some((state) =>
|
|
258
|
+
selectedCard.value?.[state]?.physical?.some(
|
|
259
|
+
(c: any) => c._id === selectedCardInUnit.value?._id
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
|
|
303
264
|
const { getUserTypeAccessCards, deleteCard: _deleteCard } = useAccessManagement();
|
|
304
265
|
|
|
305
266
|
const statsRef = ref<{ refresh: () => void } | null>(null);
|
|
@@ -369,7 +330,7 @@ function tableRowClickHandler(_: any, data: any) {
|
|
|
369
330
|
|
|
370
331
|
function openDeleteDialog() {
|
|
371
332
|
confirmDialog.value = true;
|
|
372
|
-
|
|
333
|
+
deleteError.value = "";
|
|
373
334
|
}
|
|
374
335
|
|
|
375
336
|
function openReplaceDialog() {
|
|
@@ -384,17 +345,17 @@ function successReplace() {
|
|
|
384
345
|
showMessage("Access card replaced successfully!", "success");
|
|
385
346
|
}
|
|
386
347
|
|
|
387
|
-
async function handleDeleteCard() {
|
|
348
|
+
async function handleDeleteCard(remarks: string) {
|
|
388
349
|
deleteLoading.value = true;
|
|
389
350
|
try {
|
|
390
|
-
await _deleteCard({ cardId: selectedCardInUnit.value?._id ?? "" });
|
|
351
|
+
await _deleteCard({ cardId: selectedCardInUnit.value?._id ?? "", remarks });
|
|
391
352
|
await getCards();
|
|
392
353
|
statsRef.value?.refresh();
|
|
393
354
|
selectedCardId.value = null;
|
|
394
355
|
confirmDialog.value = false;
|
|
395
356
|
previewDialog.value = false;
|
|
396
357
|
} catch (error: any) {
|
|
397
|
-
|
|
358
|
+
deleteError.value = error?.response?._data?.message || "Failed to delete card";
|
|
398
359
|
} finally {
|
|
399
360
|
deleteLoading.value = false;
|
|
400
361
|
}
|
|
@@ -83,6 +83,11 @@ const props = defineProps({
|
|
|
83
83
|
}, canDeleteBulletinBoard: {
|
|
84
84
|
type: Boolean,
|
|
85
85
|
default: true
|
|
86
|
+
},
|
|
87
|
+
recipients: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: "",
|
|
90
|
+
required: false
|
|
86
91
|
}
|
|
87
92
|
})
|
|
88
93
|
|
|
@@ -158,7 +163,7 @@ const {
|
|
|
158
163
|
pending: getAnnouncementPending,
|
|
159
164
|
} = await useLazyAsyncData(
|
|
160
165
|
`get-all-announcements-${page.value}`,
|
|
161
|
-
() => getAll({ page: page.value, site: siteId, status: status.value }),
|
|
166
|
+
() => getAll({ page: page.value, site: siteId, status: status.value, recipients: props.recipients }),
|
|
162
167
|
{
|
|
163
168
|
watch: [page, () => route.query],
|
|
164
169
|
}
|
|
@@ -57,10 +57,9 @@
|
|
|
57
57
|
}}
|
|
58
58
|
</template>
|
|
59
59
|
<template #item.closeIn="{ item }">
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}}</v-chip>
|
|
60
|
+
<v-chip class="text-capitalize" variant="flat" color="primary" pill>
|
|
61
|
+
{{ remainingTime[item._id] || "No Status" }}
|
|
62
|
+
</v-chip>
|
|
64
63
|
</template>
|
|
65
64
|
<template #item.status="{ value }">
|
|
66
65
|
<v-chip
|
|
@@ -129,7 +128,8 @@ const headers = [
|
|
|
129
128
|
{ title: "Completion Date", value: "completionDate" },
|
|
130
129
|
{ title: "", value: "download" },
|
|
131
130
|
];
|
|
132
|
-
const remainingTime = ref({} as
|
|
131
|
+
const remainingTime = ref({} as Record<string, string>);
|
|
132
|
+
const remainingSeconds = ref({} as Record<string, number>);
|
|
133
133
|
const downloadingId = ref<string | null>(null);
|
|
134
134
|
|
|
135
135
|
const { getCleaningSchedules, downloadChecklistPdf } = useCleaningSchedules();
|
|
@@ -148,11 +148,22 @@ const getStatusColor = (status: unknown): string => {
|
|
|
148
148
|
case "completed":
|
|
149
149
|
case "accepted":
|
|
150
150
|
return "success";
|
|
151
|
+
case "closed":
|
|
152
|
+
case "rejected":
|
|
153
|
+
return "error";
|
|
151
154
|
default:
|
|
152
155
|
return "secondary";
|
|
153
156
|
}
|
|
154
157
|
};
|
|
155
158
|
|
|
159
|
+
const getCloseInColor = (id: string): string => {
|
|
160
|
+
const secs = remainingSeconds.value[id];
|
|
161
|
+
if (secs === undefined) return "primary";
|
|
162
|
+
if (secs <= 0) return "error";
|
|
163
|
+
if (secs < 3 * 3600) return "warning";
|
|
164
|
+
return "primary";
|
|
165
|
+
};
|
|
166
|
+
|
|
156
167
|
const {
|
|
157
168
|
canDownloadSchedule,
|
|
158
169
|
canViewSchedules,
|
|
@@ -205,11 +216,11 @@ const formatTime = (seconds: number) => {
|
|
|
205
216
|
};
|
|
206
217
|
|
|
207
218
|
const updateRemainingTime = () => {
|
|
208
|
-
console.log(items.value);
|
|
209
219
|
items.value.forEach((item) => {
|
|
210
220
|
const itemId = item._id as string;
|
|
211
221
|
const _time = calculateRemainingTime(item.date as string);
|
|
212
|
-
|
|
222
|
+
remainingSeconds.value[itemId] = _time;
|
|
223
|
+
remainingTime.value[itemId] = _time <= 0 ? "00h 00m" : formatTime(_time);
|
|
213
224
|
});
|
|
214
225
|
};
|
|
215
226
|
|