@iservice365/layer-common 1.3.0 → 1.3.2

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 34a35ff: Update dependencies
8
+
9
+ ## 1.3.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 25ae6f4: Building Mgmt - add building mgmt components
14
+
3
15
  ## 1.3.0
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1,303 @@
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 text-capitalize">
6
+ {{ prop.mode }} Building
7
+ </span>
8
+ </v-row>
9
+ </v-toolbar>
10
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pa-0">
11
+ <v-form v-model="validForm" :disabled="disable">
12
+ <v-row no-gutters class="px-6 pt-4">
13
+ <v-col cols="12" class="mt-2">
14
+ <v-row no-gutters>
15
+ <InputLabel
16
+ class="text-capitalize font-weight-bold"
17
+ title="Name"
18
+ required
19
+ />
20
+ <v-col cols="12">
21
+ <v-text-field
22
+ v-model="building.name"
23
+ density="comfortable"
24
+ :rules="[requiredRule]"
25
+ ></v-text-field>
26
+ </v-col>
27
+ </v-row>
28
+ </v-col>
29
+
30
+ <v-col cols="12">
31
+ <v-row>
32
+ <v-col cols="6" class="mt-2">
33
+ <v-row no-gutters>
34
+ <InputLabel
35
+ class="text-capitalize font-weight-bold"
36
+ title="Block"
37
+ required
38
+ />
39
+ <v-col cols="12">
40
+ <v-select
41
+ v-model.number="building.block"
42
+ :items="blocks"
43
+ density="comfortable"
44
+ :rules="[
45
+ requiredRule,
46
+ () =>
47
+ (building.block && building.block > 0) ||
48
+ 'Block is required',
49
+ ]"
50
+ type="number"
51
+ ></v-select>
52
+ </v-col>
53
+ </v-row>
54
+ </v-col>
55
+
56
+ <v-col cols="6" class="mt-2">
57
+ <v-row no-gutters>
58
+ <InputLabel
59
+ class="text-capitalize font-weight-bold"
60
+ title="Levels"
61
+ required
62
+ />
63
+ <v-col cols="12">
64
+ <v-text-field
65
+ v-model.number="buildingLevels"
66
+ density="comfortable"
67
+ :rules="[requiredRule]"
68
+ type="number"
69
+ @update:model-value="setBuildingLevels()"
70
+ ></v-text-field>
71
+ </v-col>
72
+ </v-row>
73
+ </v-col>
74
+ </v-row>
75
+ </v-col>
76
+
77
+ <v-col v-if="buildingLevels" cols="12">
78
+ <v-row justify="center">
79
+ <v-col cols="6">
80
+ <v-btn
81
+ block
82
+ color="primary"
83
+ variant="text"
84
+ class="text-none font-weight-bold"
85
+ text="Set level labels"
86
+ @click="show = !show"
87
+ ></v-btn>
88
+ </v-col>
89
+ </v-row>
90
+ </v-col>
91
+ </v-row>
92
+
93
+ <v-expand-transition>
94
+ <v-row v-show="show" no-gutters class="px-6">
95
+ <template
96
+ v-for="(level, levelIndex) in building.levels"
97
+ :key="levelIndex"
98
+ >
99
+ <v-col cols="12">
100
+ <v-row no-gutters>
101
+ <InputLabel
102
+ class="text-capitalize font-weight-bold"
103
+ :title="`Level ${levelIndex + 1}`"
104
+ />
105
+ <v-col cols="12">
106
+ <v-text-field
107
+ v-model.trim="building.levels[levelIndex]"
108
+ density="comfortable"
109
+ ></v-text-field>
110
+ </v-col>
111
+ </v-row>
112
+ </v-col>
113
+ </template>
114
+ </v-row>
115
+ </v-expand-transition>
116
+
117
+ <v-col cols="12" class="px-6">
118
+ <InputLabel class="text-capitalize" title="Building Floor plan" />
119
+ <InputFileV2 v-model="building.buildingFloorPlan" :multiple="true" :max-length="10" title="Upload Images" />
120
+ </v-col>
121
+
122
+ <v-col cols="12" class="mt-2">
123
+ <v-checkbox v-model="createMore" density="comfortable" hide-details>
124
+ <template #label>
125
+ <span class="text-subtitle-2 font-weight-bold">
126
+ Create more
127
+ </span>
128
+ </template>
129
+ </v-checkbox>
130
+ </v-col>
131
+
132
+ <v-col cols="12">
133
+ <v-row no-gutters>
134
+ <v-col cols="12" class="text-center">
135
+ <span
136
+ class="text-none text-subtitle-2 font-weight-medium text-error"
137
+ >
138
+ {{ message }}
139
+ </span>
140
+ </v-col>
141
+ </v-row>
142
+ </v-col>
143
+ </v-form>
144
+ </v-card-text>
145
+
146
+ <v-toolbar density="compact">
147
+ <v-row no-gutters>
148
+ <v-col cols="6">
149
+ <v-btn
150
+ tile
151
+ block
152
+ variant="text"
153
+ class="text-none"
154
+ size="48"
155
+ @click="cancel"
156
+ :disabled="disable"
157
+ >
158
+ Cancel
159
+ </v-btn>
160
+ </v-col>
161
+
162
+ <v-col cols="6">
163
+ <v-btn
164
+ tile
165
+ block
166
+ variant="flat"
167
+ color="black"
168
+ class="text-none"
169
+ size="48"
170
+ :disabled="!validForm || disable"
171
+ @click="submit"
172
+ :loading="disable"
173
+ >
174
+ Submit
175
+ </v-btn>
176
+ </v-col>
177
+ </v-row>
178
+ </v-toolbar>
179
+ </v-card>
180
+ </template>
181
+
182
+ <script setup lang="ts">
183
+ const prop = defineProps({
184
+ site: {
185
+ type: String,
186
+ default: "",
187
+ },
188
+ mode: {
189
+ type: String,
190
+ default: "add",
191
+ },
192
+ building: {
193
+ type: Object as PropType<TBuilding>,
194
+ default: () => ({
195
+ site: "",
196
+ name: "",
197
+ block: null,
198
+ levels: [],
199
+ buildingFloorPlan: []
200
+ }),
201
+ },
202
+ });
203
+
204
+ const { getSiteById } = useSite();
205
+
206
+ const site = ref<TSite | null>(null);
207
+
208
+ const { data: getSiteReq } = useLazyAsyncData(
209
+ "get-site-by-id-" + prop.site,
210
+ async () => getSiteById(prop.site)
211
+ );
212
+
213
+ watchEffect(() => {
214
+ if (getSiteReq.value) {
215
+ site.value = getSiteReq.value;
216
+ }
217
+ });
218
+
219
+ const blocks = computed(() => {
220
+ if (!site.value) return [];
221
+ return Array.from({ length: site.value?.metadata?.block || 0 }, (_, i) => i + 1);
222
+ });
223
+
224
+ const emit = defineEmits(["cancel", "success", "success:create-more"]);
225
+
226
+ const validForm = ref(false);
227
+
228
+ const buildingLevels = ref(0);
229
+
230
+ const show = ref(false);
231
+
232
+ function setBuildingLevels() {
233
+ building.value.levels.length = 0;
234
+ for (let index = 0; index < buildingLevels.value; index++) {
235
+ building.value.levels.push(`Lvl${index + 1}`);
236
+ }
237
+ }
238
+
239
+ const building = ref<TBuilding>({
240
+ site: "",
241
+ name: "",
242
+ block: null,
243
+ levels: [],
244
+ buildingFloorPlan: []
245
+ });
246
+
247
+ building.value.site = prop.site;
248
+ if (prop.mode === "edit") {
249
+ building.value = JSON.parse(JSON.stringify(prop.building));
250
+ buildingLevels.value = building.value.levels.length;
251
+ }
252
+
253
+ const createMore = ref(false);
254
+ const disable = ref(false);
255
+
256
+ const { requiredRule } = useUtils();
257
+
258
+ const message = ref("");
259
+
260
+ const { createBuilding, updateById } = useBuilding();
261
+
262
+ async function submit() {
263
+ disable.value = true;
264
+
265
+ try {
266
+ if (prop.mode === "add") {
267
+ await createBuilding(building.value);
268
+ }
269
+
270
+ if (prop.mode === "edit") {
271
+ await updateById(building.value._id ?? "", {
272
+ name: building.value.name,
273
+ block: building.value.block,
274
+ levels: building.value.levels,
275
+ buildingFloorPlan: building.value.buildingFloorPlan
276
+ });
277
+ }
278
+
279
+ if (createMore.value) {
280
+ building.value.levels = [];
281
+ building.value.name = "";
282
+ building.value.block = null;
283
+
284
+ message.value = "";
285
+ emit("success:create-more");
286
+ return;
287
+ }
288
+
289
+ emit("success");
290
+ } catch (error: any) {
291
+ message.value =
292
+ error.response?._data?.message || "Failed to create building";
293
+ } finally {
294
+ disable.value = false;
295
+ }
296
+ }
297
+
298
+ function cancel() {
299
+ createMore.value = false;
300
+ message.value = "";
301
+ emit("cancel");
302
+ }
303
+ </script>
@@ -0,0 +1,395 @@
1
+ <template>
2
+ <v-row no-gutters>
3
+ <v-col cols="12" class="mb-2">
4
+ <v-row no-gutters>
5
+ <v-btn
6
+ class="text-none"
7
+ rounded="pill"
8
+ variant="tonal"
9
+ @click="setBuilding()"
10
+ size="large"
11
+ v-if="canCreate && canCreateBuilding"
12
+ >
13
+ Add Building
14
+ </v-btn>
15
+ </v-row>
16
+ </v-col>
17
+ <v-col cols="12">
18
+ <v-card
19
+ width="100%"
20
+ variant="outlined"
21
+ border="thin"
22
+ rounded="lg"
23
+ :loading="loading"
24
+ >
25
+ <v-toolbar density="compact" color="grey-lighten-4">
26
+ <template #prepend>
27
+ <v-btn fab icon density="comfortable" @click="getBuildings()">
28
+ <v-icon>mdi-refresh</v-icon>
29
+ </v-btn>
30
+ </template>
31
+
32
+ <template #append>
33
+ <v-row no-gutters justify="end" align="center">
34
+ <span class="mr-2 text-caption text-fontgray">
35
+ {{ pageRange }}
36
+ </span>
37
+ <local-pagination
38
+ v-model="page"
39
+ :length="pages"
40
+ @update:value="getBuildings()"
41
+ />
42
+ </v-row>
43
+ </template>
44
+ </v-toolbar>
45
+
46
+ <v-data-table
47
+ :headers="headers"
48
+ :items="items"
49
+ item-value="_id"
50
+ items-per-page="20"
51
+ fixed-header
52
+ hide-default-footer
53
+ hide-default-header
54
+ @click:row="tableRowClickHandler"
55
+ style="max-height: calc(100vh - (200px))"
56
+ >
57
+ <template #item.createdAt="{ value }">
58
+ {{ new Date(value).toLocaleDateString() }}
59
+ </template>
60
+ </v-data-table>
61
+ </v-card>
62
+ </v-col>
63
+
64
+ <!-- Create Dialog -->
65
+ <v-dialog v-model="createDialog" width="450" persistent>
66
+ <BuildingForm
67
+ :site="site"
68
+ @cancel="createDialog = false"
69
+ @success="successCreate()"
70
+ @success:create-more="getBuildings()"
71
+ />
72
+ </v-dialog>
73
+
74
+ <!-- Edit Dialog -->
75
+ <v-dialog v-model="editDialog" width="450" persistent>
76
+ <BuildingForm
77
+ :site="site"
78
+ mode="edit"
79
+ @cancel="editDialog = false"
80
+ @success="successUpdate()"
81
+ :building="selectedBuilding"
82
+ />
83
+ </v-dialog>
84
+
85
+ <!-- Preview Dialog -->
86
+ <v-dialog v-if="canViewBuildingDetails" v-model="previewDialog" width="450" persistent>
87
+ <v-card width="100%">
88
+ <v-card-text style="max-height: 100vh; overflow-y: auto" class="pb-0">
89
+ <v-row no-gutters class="mb-4">
90
+ <v-col cols="12">
91
+ <strong>Name:</strong> {{ selectedBuilding?.name ?? "N/A" }}
92
+ </v-col>
93
+
94
+ <v-col cols="12">
95
+ <strong>Block:</strong>
96
+ {{ selectedBuilding?.block ?? "N/A" }}
97
+ </v-col>
98
+
99
+ <v-col cols="12">
100
+ <strong>Levels:</strong>
101
+ {{ selectedBuilding?.levels.length ?? "N/A" }}
102
+ </v-col>
103
+ </v-row>
104
+ </v-card-text>
105
+
106
+ <v-toolbar class="pa-0" density="compact">
107
+ <v-row no-gutters>
108
+ <v-col cols="6" class="pa-0">
109
+ <v-btn
110
+ block
111
+ variant="text"
112
+ class="text-none"
113
+ size="large"
114
+ @click="previewDialog = false"
115
+ height="48"
116
+ >
117
+ Close
118
+ </v-btn>
119
+ </v-col>
120
+
121
+ <v-col cols="6" class="pa-0" v-if="canUpdate">
122
+ <v-menu>
123
+ <template #activator="{ props }">
124
+ <v-btn
125
+ block
126
+ variant="flat"
127
+ color="black"
128
+ class="text-none"
129
+ height="48"
130
+ v-bind="props"
131
+ tile
132
+ :disabled="!canUpdateBuilding && !canDeleteBuilding"
133
+ >
134
+ More actions
135
+ </v-btn>
136
+ </template>
137
+
138
+ <v-list class="pa-0">
139
+ <v-list-item v-if="canUpdateBuilding" @click="openEditDialog()">
140
+ <v-list-item-title class="text-subtitle-2">
141
+ Edit Building
142
+ </v-list-item-title>
143
+ </v-list-item>
144
+
145
+ <v-list-item v-if="canDeleteBuilding" @click="openDeleteDialog()" class="text-red">
146
+ <v-list-item-title class="text-subtitle-2">
147
+ Delete Building
148
+ </v-list-item-title>
149
+ </v-list-item>
150
+ </v-list>
151
+ </v-menu>
152
+ </v-col>
153
+ </v-row>
154
+ </v-toolbar>
155
+ </v-card>
156
+ </v-dialog>
157
+
158
+ <v-dialog
159
+ v-model="confirmDialog"
160
+ :loading="deleteLoading"
161
+ width="450"
162
+ persistent
163
+ >
164
+ <v-card width="100%">
165
+ <v-toolbar density="compact" class="pl-4">
166
+ <span class="font-weight-medium text-h5">Delete Building</span>
167
+ </v-toolbar>
168
+
169
+ <v-card-text>
170
+ <p class="text-subtitle-2 text-center">
171
+ Are you sure you want to delete this building? This action cannot be
172
+ undone.
173
+ </p>
174
+
175
+ <v-row v-if="message" no-gutters justify="center" class="mt-4">
176
+ <span class="text-caption text-error text-center">
177
+ {{ message }}
178
+ </span>
179
+ </v-row>
180
+ </v-card-text>
181
+
182
+ <v-toolbar density="compact">
183
+ <v-row no-gutters>
184
+ <v-col cols="6">
185
+ <v-btn
186
+ tile
187
+ block
188
+ size="48"
189
+ variant="text"
190
+ class="text-none"
191
+ @click="confirmDialog = false"
192
+ :disabled="deleteLoading"
193
+ >
194
+ Close
195
+ </v-btn>
196
+ </v-col>
197
+ <v-col cols="6">
198
+ <v-btn
199
+ tile
200
+ block
201
+ size="48"
202
+ color="black"
203
+ variant="flat"
204
+ class="text-none"
205
+ @click="handleDeleteBuilding"
206
+ :loading="deleteLoading"
207
+ >
208
+ Delete Building
209
+ </v-btn>
210
+ </v-col>
211
+ </v-row>
212
+ </v-toolbar>
213
+ </v-card>
214
+ </v-dialog>
215
+
216
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
217
+ </v-row>
218
+ </template>
219
+
220
+ <script setup lang="ts">
221
+ definePageMeta({
222
+ middleware: ["01-auth", "02-org"],
223
+ memberOnly: true,
224
+ });
225
+
226
+ const props = defineProps({
227
+ headers: {
228
+ type: Array as PropType<Array<Record<string, any>>>,
229
+ default: () => [
230
+ {
231
+ title: "Name",
232
+ value: "name",
233
+ },
234
+ {
235
+ title: "Director",
236
+ value: "directorName",
237
+ },
238
+ { title: "Action", value: "action-table" },
239
+ ],
240
+ },
241
+ canCreate: {
242
+ type: Boolean,
243
+ default: true,
244
+ },
245
+ canUpdate: {
246
+ type: Boolean,
247
+ default: true,
248
+ },
249
+ canDelete: {
250
+ type: Boolean,
251
+ default: true,
252
+ },
253
+ canCreateBuilding: {
254
+ type: Boolean,
255
+ default: true,
256
+ },
257
+ canUpdateBuilding: {
258
+ type: Boolean,
259
+ default: true,
260
+ },
261
+ canDeleteBuilding: {
262
+ type: Boolean,
263
+ default: true,
264
+ },
265
+ canViewBuildingDetails: {
266
+ type: Boolean,
267
+ default: true,
268
+ },
269
+ });
270
+
271
+ const site = (useRoute().params.site as string) ?? "";
272
+ const status = (useRoute().params.status as string) ?? "active";
273
+
274
+ const { headerSearch } = useLocal();
275
+ const { getAll: _getBuildings, deleteById } = useBuilding();
276
+
277
+ const page = ref(1);
278
+ const pages = ref(0);
279
+ const pageRange = ref("-- - -- of --");
280
+
281
+ const message = ref("");
282
+ const messageSnackbar = ref(false);
283
+ const messageColor = ref("");
284
+
285
+ const items = ref<Array<Record<string, any>>>([]);
286
+
287
+ const {
288
+ data: getBuildingReq,
289
+ refresh: getBuildings,
290
+ status: getBuildingReqStatus,
291
+ } = useLazyAsyncData(
292
+ "buildings-get-all",
293
+ () =>
294
+ _getBuildings({
295
+ page: page.value,
296
+ search: headerSearch.value,
297
+ status,
298
+ site,
299
+ }),
300
+ {
301
+ watch: [page, headerSearch],
302
+ }
303
+ );
304
+
305
+ const loading = computed(() => getBuildingReqStatus.value === "pending");
306
+
307
+ watchEffect(() => {
308
+ if (getBuildingReq.value) {
309
+ items.value = getBuildingReq.value.items;
310
+ pages.value = getBuildingReq.value.pages;
311
+ pageRange.value = getBuildingReq.value.pageRange;
312
+ }
313
+ });
314
+
315
+ const createDialog = ref(false);
316
+ const editDialog = ref(false);
317
+ const previewDialog = ref(false);
318
+ const selectedBuilding = ref<TBuilding>({
319
+ _id: "",
320
+ site: "",
321
+ name: "",
322
+ block: null,
323
+ levels: [],
324
+ buildingFloorPlan: [],
325
+ });
326
+
327
+ function tableRowClickHandler(_: any, data: any) {
328
+ selectedBuilding.value = data.item as TBuilding;
329
+ previewDialog.value = true;
330
+ }
331
+
332
+ function successCreate() {
333
+ createDialog.value = false;
334
+ getBuildings();
335
+ showMessage("Building created successfully!", "success");
336
+ }
337
+
338
+ function successUpdate() {
339
+ editDialog.value = false;
340
+ previewDialog.value = false;
341
+ getBuildings();
342
+ showMessage("Building updated successfully!", "success");
343
+ }
344
+
345
+ function openEditDialog() {
346
+ editDialog.value = true;
347
+ }
348
+
349
+ const confirmDialog = ref(false);
350
+ const selectedBuildingId = ref<string | null>(null);
351
+ const deleteLoading = ref(false);
352
+
353
+ function openDeleteDialog() {
354
+ confirmDialog.value = true;
355
+ message.value = "";
356
+ }
357
+
358
+ function showMessage(msg: string, color: string) {
359
+ message.value = msg;
360
+ messageColor.value = color;
361
+ messageSnackbar.value = true;
362
+ }
363
+
364
+ async function handleDeleteBuilding() {
365
+ deleteLoading.value = true;
366
+ try {
367
+ await deleteById(selectedBuilding.value._id ?? "");
368
+ await getBuildings();
369
+ selectedBuildingId.value = null;
370
+ confirmDialog.value = false;
371
+ previewDialog.value = false
372
+ } catch (error: any) {
373
+ message.value =
374
+ error?.response?._data?.message || "Failed to delete region";
375
+ } finally {
376
+ deleteLoading.value = false;
377
+ }
378
+ }
379
+
380
+ function setBuilding({
381
+ mode = "create",
382
+ dialog = true,
383
+ data = {} as TBuilding,
384
+ } = {}) {
385
+ if (mode === "create") {
386
+ createDialog.value = dialog;
387
+ } else if (mode === "edit") {
388
+ editDialog.value = dialog;
389
+ selectedBuilding.value = data;
390
+ } else if (mode === "preview") {
391
+ previewDialog.value = dialog;
392
+ selectedBuilding.value = data;
393
+ }
394
+ }
395
+ </script>