@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/components/AccessCardDeleteDialog.vue +109 -0
  3. package/components/AccessCardHistoryDialog.vue +133 -0
  4. package/components/AccessCardPreviewDialog.vue +308 -0
  5. package/components/AccessCardQrTagging.vue +183 -0
  6. package/components/AccessCardReplaceForm.vue +179 -0
  7. package/components/AccessManagement.vue +61 -251
  8. package/components/AreaChecklistHistoryLogs.vue +1 -1
  9. package/components/BuildingManagement/units.vue +33 -2
  10. package/components/BuildingUnitFormAdd.vue +45 -99
  11. package/components/BuildingUnitFormEdit.vue +59 -148
  12. package/components/BulletinBoardManagement.vue +6 -1
  13. package/components/BulletinBoardView.vue +2 -2
  14. package/components/Button/Close.vue +3 -1
  15. package/components/CleaningScheduleMain.vue +20 -9
  16. package/components/IncidentReport/IncidentInformation.vue +45 -6
  17. package/components/IncidentReport/affectedEntities.vue +29 -0
  18. package/components/PeopleForm.vue +1 -1
  19. package/components/PlateNumberDisplay.vue +45 -0
  20. package/components/ScheduleAreaMain.vue +5 -2
  21. package/components/ScheduleTaskForm.vue +59 -114
  22. package/components/ScheduleTaskMain.vue +19 -15
  23. package/components/VehicleAddSelection.vue +58 -0
  24. package/components/VehicleForm.vue +600 -0
  25. package/components/VehicleManagement.vue +298 -0
  26. package/components/VisitorForm.vue +1 -1
  27. package/composables/useAccessManagement.ts +16 -0
  28. package/composables/useBulletin.ts +11 -9
  29. package/composables/useCard.ts +14 -0
  30. package/composables/useScheduleTask.ts +4 -8
  31. package/composables/useVehicle.ts +114 -0
  32. package/package.json +1 -1
  33. package/types/bulletin-board.d.ts +1 -1
  34. package/types/checkout-item.d.ts +1 -0
  35. package/types/cleaner-schedule.d.ts +1 -1
  36. package/types/people.d.ts +1 -1
  37. 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>