@7365admin1/layer-common 3.0.0 → 3.0.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.
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <v-card
3
+ flat
4
+ border
5
+ rounded="lg"
6
+ class="d-flex flex-column w-100"
7
+ style="min-height: 200px"
8
+ >
9
+ <div class="d-flex align-center justify-space-between pa-5 pb-3">
10
+ <h2 class="text-body-1 font-weight-black text-blue-grey-darken-4 mb-0">
11
+ {{ title }}
12
+ </h2>
13
+ <v-btn
14
+ v-if="actionLabel"
15
+ variant="text"
16
+ color="primary"
17
+ size="small"
18
+ class="text-none font-weight-bold"
19
+ @click="$emit('action')"
20
+ >
21
+ {{ actionLabel }}
22
+ </v-btn>
23
+ </div>
24
+
25
+ <div class="flex-1-1-100 overflow-y-auto pa-5 pt-0" style="max-height: 400px">
26
+ <slot />
27
+ </div>
28
+ </v-card>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ defineProps({
33
+ title: {
34
+ type: String,
35
+ required: true,
36
+ },
37
+ actionLabel: {
38
+ type: String,
39
+ default: '',
40
+ },
41
+ });
42
+
43
+ defineEmits(['action']);
44
+ </script>
@@ -125,17 +125,35 @@ const routeParentId = computed(() =>
125
125
  );
126
126
  const effectiveParentId = computed(() => prop.parentId || routeParentId.value);
127
127
 
128
+ function getAttachmentId(attachment: any): string {
129
+ if (!attachment) return "";
130
+ if (Array.isArray(attachment)) {
131
+ const first = attachment[0];
132
+ if (!first) return "";
133
+ if (typeof first === "string") return first;
134
+ return String(first?._id || first?.id || "");
135
+ }
136
+ if (typeof attachment === "string") return attachment;
137
+ return String(attachment?._id || attachment?.id || "");
138
+ }
139
+
128
140
  if (prop.mode === "edit") {
129
141
  document.value.name = prop.document.name;
130
- document.value.attachment = [prop.document.attachment];
142
+
143
+ const extractedId = getAttachmentId(prop.document.attachment);
144
+ document.value.attachment = extractedId ? [extractedId] : [];
145
+
131
146
  document.value.type = prop.document.type;
132
147
  }
133
148
 
134
149
  watch(
135
150
  () => document.value.attachment,
136
- async (newVal) => {
137
- if (newVal && prop.mode !== "edit") {
138
- const fileId = Array.isArray(newVal) ? newVal[0] : newVal;
151
+ async (newVal, oldVal) => {
152
+ const newId = Array.isArray(newVal) ? newVal[0] : newVal;
153
+ const oldId = Array.isArray(oldVal) ? oldVal[0] : oldVal;
154
+
155
+ if (newId && newId !== oldId) {
156
+ const fileId = newId;
139
157
  const fileData = await getFileById(fileId);
140
158
  if (fileData) {
141
159
  document.value.name = fileData.data.name;
@@ -177,6 +177,7 @@
177
177
  <!-- Create Dialog -->
178
178
  <v-dialog v-model="createDialog" width="450" persistent>
179
179
  <DocumentForm
180
+ v-if="createDialog"
180
181
  :parent-id="activeParentId"
181
182
  @cancel="createDialog = false"
182
183
  @success="successCreate()"
@@ -253,6 +254,7 @@
253
254
  <!-- Edit Dialog -->
254
255
  <v-dialog v-model="editDialog" width="450" persistent>
255
256
  <DocumentForm
257
+ v-if="editDialog"
256
258
  mode="edit"
257
259
  @cancel="editDialog = false"
258
260
  @success="successUpdate()"
@@ -277,13 +279,19 @@
277
279
  <v-col cols="6" class="text-right text-body-1">
278
280
  <NuxtLink
279
281
  v-if="selectedDocument?.attachment && isViewable"
280
- :to="getFileUrl(selectedDocument.attachment)"
282
+ :to="
283
+ getFileUrl(getAttachmentId(selectedDocument.attachment))
284
+ "
281
285
  external
282
286
  target="_blank"
283
287
  >
284
288
  View Document
285
289
  </NuxtLink>
286
- <span v-else-if="selectedDocument?.attachment" class="text-grey" style="opacity: 0.6; cursor: not-allowed;">
290
+ <span
291
+ v-else-if="selectedDocument?.attachment"
292
+ class="text-grey"
293
+ style="opacity: 0.6; cursor: not-allowed"
294
+ >
287
295
  View Document
288
296
  </span>
289
297
  <span v-else>N/A</span>
@@ -528,6 +536,20 @@ const {
528
536
  } = documentService;
529
537
  const { getFileUrl } = useFile();
530
538
 
539
+ function getAttachmentId(attachment: any): string {
540
+ if (!attachment) return "";
541
+
542
+ if (Array.isArray(attachment)) {
543
+ const first = attachment[0];
544
+ if (!first) return "";
545
+ if (typeof first === "string") return first;
546
+ return String(first?._id || first?.id || "");
547
+ }
548
+ if (typeof attachment === "string") return attachment;
549
+
550
+ return String(attachment?._id || attachment?.id || "");
551
+ }
552
+
531
553
  const router = useRouter();
532
554
  const route = useRoute();
533
555
 
@@ -627,11 +649,26 @@ function formatBytes(bytes = 0) {
627
649
  return `${parseFloat((b / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
628
650
  }
629
651
 
652
+ const KNOWN_FILE_TYPES = new Set(["pdf", "csv", "doc", "docx", "folder", "mixed"]);
653
+
654
+ function inferDisplayType(doc: any): string {
655
+ if (doc.type === "folder") return "Folder";
656
+ // If stored type is a known file format, use it directly
657
+ const t = String(doc.type || "").toLowerCase().trim();
658
+ if (t && KNOWN_FILE_TYPES.has(t)) return t;
659
+ // Otherwise infer from the document name (handles legacy "public" ACL type)
660
+ const name = String(doc.name || "").toLowerCase();
661
+ if (name.endsWith(".pdf")) return "pdf";
662
+ if (name.endsWith(".csv")) return "csv";
663
+ if (name.endsWith(".docx") || name.endsWith(".doc")) return "doc";
664
+ return t || "N/A";
665
+ }
666
+
630
667
  const tableItems = computed(() => {
631
668
  return items.value.map((doc: any) => ({
632
669
  ...doc,
633
670
  _raw: doc,
634
- file_type: doc.type === "folder" ? "Folder" : doc.type || "N/A",
671
+ file_type: inferDisplayType(doc),
635
672
  file_size:
636
673
  doc.type === "folder"
637
674
  ? "—"
@@ -664,8 +701,8 @@ const selectedDocument = ref<TDocument>({
664
701
  });
665
702
 
666
703
  const isViewable = computed(() => {
667
- const lowerType = String(selectedDocument.value?.type || "").trim().toLowerCase();
668
- return ["pdf", "doc", "csv"].includes(lowerType);
704
+ const effectiveType = inferDisplayType(selectedDocument.value as any).toLowerCase();
705
+ return ["pdf", "doc", "csv"].includes(effectiveType);
669
706
  });
670
707
 
671
708
  const previewDetails = computed(() => {
@@ -769,7 +806,7 @@ function downloadDocument(document?: TDocument) {
769
806
  return;
770
807
  }
771
808
 
772
- const fileUrl = getFileUrl(document.attachment);
809
+ const fileUrl = getFileUrl(getAttachmentId(document.attachment));
773
810
  if (fileUrl) {
774
811
  window.open(fileUrl, "_blank", "noopener,noreferrer");
775
812
  }
@@ -1,19 +1,25 @@
1
1
  <template>
2
2
  <ClientOnly>
3
3
  <v-row no-gutters class="ckeditor-container w-100">
4
- <ckeditor v-model="data" :editor="ClassicEditor" :config="config" @ready="onReady" :key="prop.readOnly" />
4
+ <ckeditor
5
+ v-model="data"
6
+ :editor="ClassicEditor"
7
+ :config="config"
8
+ @ready="onReady"
9
+ :key="prop.readOnly"
10
+ />
5
11
  </v-row>
6
12
  </ClientOnly>
7
13
  </template>
8
14
 
9
15
  <script setup>
10
-
11
16
  const prop = defineProps({
12
17
  readOnly: {
13
18
  type: Boolean,
14
- required: false
15
- }
16
- })
19
+ required: false,
20
+ default: false,
21
+ },
22
+ });
17
23
  import {
18
24
  ClassicEditor,
19
25
  Essentials,
@@ -25,28 +31,50 @@ import {
25
31
  Link,
26
32
  Alignment,
27
33
  Font,
28
- } from 'ckeditor5'
29
- import { Ckeditor } from '@ckeditor/ckeditor5-vue'
30
- import 'ckeditor5/ckeditor5.css';
34
+ } from "ckeditor5";
35
+ import { Ckeditor } from "@ckeditor/ckeditor5-vue";
36
+ import "ckeditor5/ckeditor5.css";
31
37
 
32
- import 'ckeditor5/ckeditor5.css'
38
+ import "ckeditor5/ckeditor5.css";
33
39
 
34
- const data = defineModel({ required: true, default: "" })
40
+ const data = defineModel({ required: true, default: "" });
35
41
 
36
42
  const editorInstance = ref(null);
37
43
 
38
44
  const config = computed(() => ({
39
- licenseKey: 'GPL',
40
- plugins: [Essentials, Paragraph, Bold, Italic, Heading, List, Alignment, Font],
45
+ licenseKey: "GPL",
46
+ plugins: [
47
+ Essentials,
48
+ Paragraph,
49
+ Bold,
50
+ Italic,
51
+ Heading,
52
+ List,
53
+ Alignment,
54
+ Font,
55
+ ],
41
56
  toolbar: [
42
- 'undo', 'redo',
43
- '|', 'bold', 'italic',
44
- '|', 'fontSize', 'fontColor', 'fontBackgroundColor',
45
- '|', 'bulletedList', 'numberedList',
46
- '|', 'heading', '|', 'alignment:left', 'alignment:center', 'alignment:right', 'alignment:justify',
57
+ "undo",
58
+ "redo",
59
+ "|",
60
+ "bold",
61
+ "italic",
62
+ "|",
63
+ "fontSize",
64
+ "fontColor",
65
+ "fontBackgroundColor",
66
+ "|",
67
+ "bulletedList",
68
+ "numberedList",
69
+ "|",
70
+ "heading",
71
+ "|",
72
+ "alignment:left",
73
+ "alignment:center",
74
+ "alignment:right",
75
+ "alignment:justify",
47
76
  ],
48
- }))
49
-
77
+ }));
50
78
 
51
79
  function onReady(editor) {
52
80
  editorInstance.value = editor;
@@ -56,7 +84,6 @@ function onReady(editor) {
56
84
  function toggleReadOnly(isReadOnly) {
57
85
  if (!editorInstance.value) return;
58
86
 
59
-
60
87
  if (isReadOnly) {
61
88
  editorInstance.value.enableReadOnlyMode("view-mode-lock");
62
89
  } else {
@@ -70,8 +97,6 @@ watch(
70
97
  toggleReadOnly(newVal);
71
98
  }
72
99
  );
73
-
74
-
75
100
  </script>
76
101
 
77
102
  <style scoped>
@@ -62,7 +62,6 @@
62
62
  <AttendanceCheckInOutDialog
63
63
  v-model="dialogShowForm"
64
64
  :action="currentAction"
65
- :serviceType="props.serviceType"
66
65
  @saved="onAttendanceSaved"
67
66
  @close="dialogShowForm = false"
68
67
  />
@@ -203,7 +202,7 @@ const onAttendanceSaved = async (
203
202
  try {
204
203
  let response;
205
204
  if (currentAction.value === "checkIn") {
206
- response = await checkIn(props.site, payload as TAttendanceCheckIn);
205
+ response = await checkIn(props.site, payload as TAttendanceCheckIn, props.serviceType);
207
206
  } else {
208
207
  if (!todayAttendance.value?._id) {
209
208
  throw new Error("Attendance ID not found");
@@ -1446,8 +1446,8 @@ function generateCSVData(visitors: any[]): string {
1446
1446
  visitor.name || "",
1447
1447
  formatExportTypes(formatType(visitor)) || "",
1448
1448
  visitor.company || "",
1449
- visitor.block || "",
1450
- visitor.level || "",
1449
+ visitor.block?.name || "",
1450
+ visitor.level?.name || "",
1451
1451
  visitor.unitName || "",
1452
1452
  visitor.contact || "",
1453
1453
  visitor.plateNumber || "",
@@ -40,8 +40,8 @@
40
40
  <v-icon icon="mdi-storefront-outline" size="15" />
41
41
  <span class="text-capitalize" style="word-break: break-word;">{{
42
42
  formatLocation({
43
- block: item.block,
44
- level: item.level,
43
+ block: item.block?.name || "",
44
+ level: item.level?.name,
45
45
  unit: item.unitName,
46
46
  })
47
47
  }}</span>
@@ -182,10 +182,10 @@ watch(getSitesReq, (newData: any) => {
182
182
  }
183
183
  })
184
184
 
185
- const formatLocation = ({ block, level, unit }: { block: number, level: string, unit: string | TBuildingUnit }) => {
185
+ const formatLocation = ({ block, level, unit }: { block: string, level: string, unit: string | TBuildingUnit }) => {
186
186
  const parts = [];
187
187
 
188
- if (block) parts.push(`Block ${block}`);
188
+ if (block) parts.push(`${block}`);
189
189
  if (level) parts.push(level);
190
190
  if (unit) parts.push(unit);
191
191
 
@@ -37,12 +37,12 @@ export default function useAttendance() {
37
37
  });
38
38
  }
39
39
 
40
- function checkIn(site: string, payload: TAttendanceCheckIn) {
40
+ function checkIn(site: string, payload: TAttendanceCheckIn, serviceType: string = "") {
41
41
  return useNuxtApp().$api<Record<string, any>>(
42
42
  `/api/attendances/site/${site}/check-in`,
43
43
  {
44
44
  method: "POST",
45
- body: payload,
45
+ body: { ...payload, serviceType },
46
46
  }
47
47
  );
48
48
  }
@@ -1,31 +1,90 @@
1
- export default function(){
2
-
3
-
4
- type TGetAllParams = {
5
- site?: string;
1
+ export default function () {
2
+ function getDashboardHygiene(
3
+ site = "",
4
+ period: TDashboardValues = "today",
5
+ serviceType: TServiceType
6
+ ) {
7
+ return useNuxtApp().$api<THygieneDashboardData>(
8
+ `/api/dashboard/serviceType/${serviceType}/site/${site}`,
9
+ {
10
+ method: "GET",
11
+ query: {
12
+ period,
13
+ },
6
14
  }
15
+ );
16
+ }
7
17
 
18
+ function getDashboardHygieneWeeklyActivity(
19
+ site = "",
20
+ serviceType: TServiceType = "cleaning_services"
21
+ ) {
22
+ return useNuxtApp().$api<THygieneWeeklyActivityItem[]>(
23
+ `/api/dashboard/serviceType/${serviceType}/site/${site}/weekly-activity`,
24
+ {
25
+ method: "GET",
26
+ }
27
+ );
28
+ }
8
29
 
30
+ function getDashboardHygieneTaskSchedule(
31
+ site = "",
32
+ serviceType: TServiceType,
33
+ page = 1,
34
+ limit = 4
35
+ ) {
36
+ return useNuxtApp().$api<
37
+ THygienePaginatedResponse<THygieneTaskScheduleItem>
38
+ >(`/api/dashboard/serviceType/${serviceType}/site/${site}/task-schedule`, {
39
+ method: "GET",
40
+ query: {
41
+ page,
42
+ limit,
43
+ },
44
+ });
45
+ }
9
46
 
10
- async function getDashboardData({
11
- site
12
- } : TGetAllParams) {
13
- return await useNuxtApp().$api<Record<string, any>>(
14
- "/api/dashboard",
47
+ function getDashboardHygieneAttendance(
48
+ site = "",
49
+ serviceType: TServiceType,
50
+ page = 1,
51
+ limit = 4
52
+ ) {
53
+ return useNuxtApp().$api<THygienePaginatedResponse<THygieneAttendanceItem>>(
54
+ `/api/dashboard/serviceType/${serviceType}/site/${site}/attendance`,
15
55
  {
16
56
  method: "GET",
17
57
  query: {
18
-
58
+ page,
59
+ limit,
19
60
  },
20
61
  }
21
62
  );
22
63
  }
23
64
 
65
+ function getDashboardHygieneFeedbacks(
66
+ site = "",
67
+ serviceType: TServiceType,
68
+ page = 1,
69
+ limit = 4
70
+ ) {
71
+ return useNuxtApp().$api<THygienePaginatedResponse<THygieneFeedbackItem>>(
72
+ `/api/dashboard/serviceType/${serviceType}/site/${site}/feedbacks`,
73
+ {
74
+ method: "GET",
75
+ query: {
76
+ page,
77
+ limit,
78
+ },
79
+ }
80
+ );
81
+ }
24
82
 
25
-
26
-
27
- return {
28
- getDashboardData,
29
-
30
- }
31
- }
83
+ return {
84
+ getDashboardHygiene,
85
+ getDashboardHygieneWeeklyActivity,
86
+ getDashboardHygieneTaskSchedule,
87
+ getDashboardHygieneAttendance,
88
+ getDashboardHygieneFeedbacks,
89
+ };
90
+ }
@@ -18,6 +18,7 @@ export default function useDocument() {
18
18
  page,
19
19
  search,
20
20
  limit,
21
+ type,
21
22
  site,
22
23
  parentId,
23
24
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@7365admin1/layer-common",
3
3
  "license": "MIT",
4
4
  "type": "module",
5
- "version": "3.0.0",
5
+ "version": "3.0.2",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -1,5 +1,15 @@
1
1
  declare type TDashboardValues = "today" | "thisWeek" | "thisMonth";
2
2
 
3
+ declare type TServiceType =
4
+ | "real_estate_developer"
5
+ | "property_management_agency"
6
+ | "security_agency"
7
+ | "cleaning_services"
8
+ | "mechanical_electrical_services"
9
+ | "landscaping_services"
10
+ | "pest_control_services"
11
+ | "pool_maintenance_services";
12
+
3
13
  declare type TDashboardCardValue = {
4
14
  key: string;
5
15
  periodKey: string;
@@ -8,4 +18,60 @@ declare type TDashboardCardValue = {
8
18
  color: string;
9
19
  };
10
20
 
21
+ declare type TDashboardMetric = {
22
+ count?: number;
23
+ percentage?: number;
24
+ overdue?: number;
25
+ inProgress?: number;
26
+ pending?: number;
27
+ highSeverity?: number;
28
+ currentlyOnSite?: number;
29
+ lowStock?: number;
30
+ outOfStock?: number;
31
+ completed?: number;
32
+ total?: number;
33
+ };
34
+
35
+ declare type THygieneDashboardData = {
36
+ openWorkOrder?: TDashboardMetric;
37
+ supplyAlert?: TDashboardMetric;
38
+ taskCompleted?: TDashboardMetric;
39
+ };
40
+
41
+ declare type THygieneWeeklyActivityItem = {
42
+ day: string;
43
+ workOrder: number;
44
+ taskCompleted: number;
45
+ };
46
+
47
+ declare type THygieneTaskScheduleItem = {
48
+ _id: string;
49
+ name: string;
50
+ type: string;
51
+ status: string;
52
+ };
53
+
54
+ declare type THygieneAttendanceItem = {
55
+ _id: string;
56
+ name?: string;
57
+ role?: string;
58
+ status?: string;
59
+ [key: string]: any;
60
+ };
61
+
62
+ declare type THygieneFeedbackItem = {
63
+ _id: string;
64
+ subject: string;
65
+ app: string;
66
+ description: string;
67
+ status: string;
68
+ createdAt: string;
69
+ createdByName: string;
70
+ };
71
+
72
+ declare type THygienePaginatedResponse<T> = {
73
+ items: T[];
74
+ pages: number;
75
+ pageRange: string;
76
+ };
11
77