@7365admin1/layer-common 1.10.3 → 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 +12 -0
- package/components/AccessCardDeleteDialog.vue +109 -0
- package/components/AccessCardHistoryDialog.vue +133 -0
- package/components/AccessCardPreviewDialog.vue +308 -0
- package/components/AccessCardQrTagging.vue +183 -0
- package/components/AccessCardReplaceForm.vue +179 -0
- package/components/AccessManagement.vue +61 -251
- package/components/AreaChecklistHistoryLogs.vue +1 -1
- package/components/BuildingManagement/units.vue +33 -2
- package/components/BuildingUnitFormAdd.vue +45 -99
- package/components/BuildingUnitFormEdit.vue +59 -148
- package/components/BulletinBoardManagement.vue +6 -1
- package/components/BulletinBoardView.vue +2 -2
- package/components/Button/Close.vue +3 -1
- package/components/CleaningScheduleMain.vue +20 -9
- package/components/IncidentReport/IncidentInformation.vue +45 -6
- package/components/IncidentReport/affectedEntities.vue +29 -0
- package/components/PeopleForm.vue +1 -1
- package/components/PlateNumberDisplay.vue +45 -0
- package/components/ScheduleAreaMain.vue +5 -2
- package/components/ScheduleTaskForm.vue +59 -114
- package/components/ScheduleTaskMain.vue +19 -15
- package/components/VehicleAddSelection.vue +58 -0
- package/components/VehicleForm.vue +600 -0
- package/components/VehicleManagement.vue +298 -0
- package/components/VisitorForm.vue +1 -1
- package/composables/useAccessManagement.ts +16 -0
- package/composables/useBulletin.ts +11 -9
- package/composables/useCard.ts +14 -0
- package/composables/useScheduleTask.ts +4 -8
- package/composables/useVehicle.ts +114 -0
- package/package.json +1 -1
- package/types/bulletin-board.d.ts +1 -1
- package/types/checkout-item.d.ts +1 -0
- package/types/cleaner-schedule.d.ts +1 -1
- package/types/people.d.ts +1 -1
- package/types/vehicle.d.ts +43 -0
|
@@ -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>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-card width="100%">
|
|
3
|
+
<v-toolbar>
|
|
4
|
+
<v-row no-gutters class="fill-height px-6" align="center">
|
|
5
|
+
<span class="font-weight-bold text-h5">Replace Access Card</span>
|
|
6
|
+
</v-row>
|
|
7
|
+
</v-toolbar>
|
|
8
|
+
<v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-0">
|
|
9
|
+
<v-form v-model="validForm" :disabled="loading">
|
|
10
|
+
<v-row no-gutters class="px-6 pt-4 pb-6">
|
|
11
|
+
<!-- Card being replaced -->
|
|
12
|
+
<v-col cols="12" class="px-1 mb-4">
|
|
13
|
+
<div class="text-caption text-grey mb-1 font-weight-bold">
|
|
14
|
+
Card being replaced
|
|
15
|
+
</div>
|
|
16
|
+
<v-chip size="small" variant="tonal" color="warning">
|
|
17
|
+
{{ card.cardNo }}
|
|
18
|
+
</v-chip>
|
|
19
|
+
</v-col>
|
|
20
|
+
|
|
21
|
+
<!-- Replacement card -->
|
|
22
|
+
<v-col cols="12" class="px-1">
|
|
23
|
+
<InputLabel
|
|
24
|
+
class="text-capitalize font-weight-bold"
|
|
25
|
+
title="Replacement Card"
|
|
26
|
+
required
|
|
27
|
+
/>
|
|
28
|
+
<v-autocomplete
|
|
29
|
+
v-model="form.replacementCardId"
|
|
30
|
+
density="compact"
|
|
31
|
+
:items="replacementOptions"
|
|
32
|
+
hide-details="auto"
|
|
33
|
+
item-title="cardNo"
|
|
34
|
+
item-value="_id"
|
|
35
|
+
placeholder="Select replacement card..."
|
|
36
|
+
persistent-placeholder
|
|
37
|
+
:rules="[requiredRule]"
|
|
38
|
+
:no-data-text="noDataText"
|
|
39
|
+
/>
|
|
40
|
+
</v-col>
|
|
41
|
+
|
|
42
|
+
<!-- Remarks -->
|
|
43
|
+
<v-col cols="12" class="px-1 mt-2">
|
|
44
|
+
<InputLabel
|
|
45
|
+
class="text-capitalize font-weight-bold"
|
|
46
|
+
title="Remarks"
|
|
47
|
+
required
|
|
48
|
+
/>
|
|
49
|
+
<v-textarea
|
|
50
|
+
v-model="form.remarks"
|
|
51
|
+
density="compact"
|
|
52
|
+
hide-details="auto"
|
|
53
|
+
placeholder="Enter remarks..."
|
|
54
|
+
persistent-placeholder
|
|
55
|
+
rows="3"
|
|
56
|
+
auto-grow
|
|
57
|
+
:rules="[requiredRule]"
|
|
58
|
+
/>
|
|
59
|
+
</v-col>
|
|
60
|
+
</v-row>
|
|
61
|
+
</v-form>
|
|
62
|
+
</v-card-text>
|
|
63
|
+
|
|
64
|
+
<v-toolbar density="compact">
|
|
65
|
+
<v-row no-gutters>
|
|
66
|
+
<v-col cols="6">
|
|
67
|
+
<v-btn
|
|
68
|
+
tile
|
|
69
|
+
block
|
|
70
|
+
variant="text"
|
|
71
|
+
class="text-none"
|
|
72
|
+
size="48"
|
|
73
|
+
@click="cancel"
|
|
74
|
+
:disabled="loading"
|
|
75
|
+
>
|
|
76
|
+
Cancel
|
|
77
|
+
</v-btn>
|
|
78
|
+
</v-col>
|
|
79
|
+
<v-col cols="6">
|
|
80
|
+
<v-btn
|
|
81
|
+
tile
|
|
82
|
+
block
|
|
83
|
+
variant="flat"
|
|
84
|
+
color="black"
|
|
85
|
+
class="text-none"
|
|
86
|
+
size="48"
|
|
87
|
+
:disabled="!validForm || loading"
|
|
88
|
+
:loading="loading"
|
|
89
|
+
@click="submit"
|
|
90
|
+
>
|
|
91
|
+
Replace
|
|
92
|
+
</v-btn>
|
|
93
|
+
</v-col>
|
|
94
|
+
</v-row>
|
|
95
|
+
</v-toolbar>
|
|
96
|
+
</v-card>
|
|
97
|
+
</template>
|
|
98
|
+
<script setup lang="ts">
|
|
99
|
+
const props = defineProps({
|
|
100
|
+
card: {
|
|
101
|
+
type: Object as PropType<Record<string, any>>,
|
|
102
|
+
required: true,
|
|
103
|
+
},
|
|
104
|
+
unit: {
|
|
105
|
+
type: Object as PropType<Record<string, any>>,
|
|
106
|
+
required: true,
|
|
107
|
+
},
|
|
108
|
+
siteId: {
|
|
109
|
+
type: String,
|
|
110
|
+
default: "",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const emit = defineEmits(["cancel", "success", "error"]);
|
|
114
|
+
|
|
115
|
+
const { requiredRule } = useUtils();
|
|
116
|
+
|
|
117
|
+
const validForm = ref(false);
|
|
118
|
+
const loading = ref(false);
|
|
119
|
+
|
|
120
|
+
const form = ref({
|
|
121
|
+
replacementCardId: null as string | null,
|
|
122
|
+
remarks: "",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const cardType = computed(() => {
|
|
126
|
+
const id = props.card._id;
|
|
127
|
+
if (props.unit.available?.physical?.some((c: any) => c._id === id)) return "physical";
|
|
128
|
+
if (props.unit.assigned?.physical?.some((c: any) => c._id === id)) return "physical";
|
|
129
|
+
if (props.unit.available?.non_physical?.some((c: any) => c._id === id)) return "non_physical";
|
|
130
|
+
if (props.unit.assigned?.non_physical?.some((c: any) => c._id === id)) return "non_physical";
|
|
131
|
+
return null;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const replacementOptions = computed(() => {
|
|
135
|
+
const id = props.card._id;
|
|
136
|
+
if (cardType.value === "physical") {
|
|
137
|
+
return (props.unit.available?.physical ?? []).filter((c: any) => c._id !== id);
|
|
138
|
+
}
|
|
139
|
+
if (cardType.value === "non_physical") {
|
|
140
|
+
return (props.unit.available?.non_physical ?? []).filter((c: any) => c._id !== id);
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const noDataText = computed(() => {
|
|
146
|
+
if (!cardType.value) return "Unable to determine card type";
|
|
147
|
+
const label = cardType.value === "physical" ? "physical" : "non-physical";
|
|
148
|
+
return `No available ${label} cards for replacement`;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function cancel() {
|
|
152
|
+
emit("cancel");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function submit() {
|
|
156
|
+
loading.value = true;
|
|
157
|
+
try {
|
|
158
|
+
const { replaceCard } = useCard();
|
|
159
|
+
const { currentUser } = useLocalAuth();
|
|
160
|
+
// @TODO: userId should be the ID of the unit owner, not the current user
|
|
161
|
+
await replaceCard({
|
|
162
|
+
cardId: props.card._id,
|
|
163
|
+
issuedCardId: form.value.replacementCardId!,
|
|
164
|
+
unitId: props.unit._id,
|
|
165
|
+
remarks: form.value.remarks,
|
|
166
|
+
userId: currentUser.value?._id ?? "",
|
|
167
|
+
});
|
|
168
|
+
emit("success");
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
const msg =
|
|
171
|
+
error?.response?._data?.message ||
|
|
172
|
+
error?.data?.message ||
|
|
173
|
+
"Failed to replace access card.";
|
|
174
|
+
emit("error", msg);
|
|
175
|
+
} finally {
|
|
176
|
+
loading.value = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
</script>
|