@7365admin1/layer-common 3.0.30-staging.1 → 3.0.30-staging.3

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.
@@ -1560,18 +1560,27 @@ const dashboardWidgetPermissions: Record<string, string> = {
1560
1560
  };
1561
1561
 
1562
1562
  const propertyWidgetPermissions: Record<string, string[]> = {
1563
- workOrders: ["workOrder:see-all-work-orders"],
1563
+ workOrders: [
1564
+ "workOrder:see-all-work-orders",
1565
+ "work-order:see-all-work-orders",
1566
+ ],
1564
1567
  incidents: ["incident-reports:see-incident-reports"],
1565
1568
  visitors: ["visitorManagement:see-all-visitor"],
1566
1569
  facilityBookings: ["facility-booking-mgmt:see-all-facility-booking"],
1567
- activity: ["workOrder:see-all-work-orders"],
1570
+ activity: [
1571
+ "workOrder:see-all-work-orders",
1572
+ "work-order:see-all-work-orders",
1573
+ ],
1568
1574
  upcomingEvents: ["event-mgmt:see-all-event"],
1569
1575
  todayReminders: ["event-mgmt:see-all-event"],
1570
1576
  todayAttentions: [
1571
1577
  "incident-reports:see-incident-reports",
1572
1578
  "facility-booking-mgmt:see-all-facility-booking",
1573
1579
  ],
1574
- workOrderStatus: ["workOrder:see-all-work-orders"],
1580
+ workOrderStatus: [
1581
+ "workOrder:see-all-work-orders",
1582
+ "work-order:see-all-work-orders",
1583
+ ],
1575
1584
  feedbacks: ["feedbacks:see-all-feedback"],
1576
1585
  activePatrol: ["virtual-patrol:see-all-virtual-patrol-logs"],
1577
1586
  };
@@ -1587,6 +1596,11 @@ const moduleWidgetPermissions: Record<string, string[]> = {
1587
1596
  "work-order:see-all-work-orders",
1588
1597
  "work-orders:see-all-work-orders",
1589
1598
  ],
1599
+ activity: [
1600
+ "work_order:see-all-work-orders",
1601
+ "work-order:see-all-work-orders",
1602
+ "work-orders:see-all-work-orders",
1603
+ ],
1590
1604
  taskSchedule: [
1591
1605
  "cleaning-schedule-mgmt:see-all-schedules",
1592
1606
  "schedule-task-mgmt:see-all-schedule-tasks",
@@ -77,6 +77,53 @@
77
77
  </v-col>
78
78
  </v-row>
79
79
  </v-col>
80
+ <v-col
81
+ v-if="item.reject && (item.remarks || item.attachment?.length)"
82
+ cols="12"
83
+ class="mt-2"
84
+ >
85
+ <v-sheet
86
+ border
87
+ rounded="lg"
88
+ class="d-inline-flex flex-column pa-3 bg-grey-lighten-5"
89
+ style="max-width: 420px"
90
+ >
91
+ <div class="d-flex align-center ga-2 mb-2">
92
+ <v-icon color="error" size="18">mdi-close-circle</v-icon>
93
+ <span class="text-caption font-weight-bold">
94
+ Rejection details
95
+ </span>
96
+ <v-chip size="x-small" color="error" variant="tonal">
97
+ Rejected
98
+ </v-chip>
99
+ </div>
100
+ <div v-if="item.remarks" class="text-body-2 text-medium-emphasis">
101
+ {{ item.remarks }}
102
+ </div>
103
+ <div
104
+ v-if="item.attachment?.length"
105
+ class="d-flex flex-wrap ga-2 mt-3"
106
+ >
107
+ <v-sheet
108
+ v-for="attachmentId in item.attachment"
109
+ :key="attachmentId"
110
+ rounded="lg"
111
+ border
112
+ class="overflow-hidden"
113
+ style="width: 96px; height: 96px"
114
+ >
115
+ <v-img
116
+ :src="getFileUrl(attachmentId)"
117
+ width="96"
118
+ height="96"
119
+ cover
120
+ style="cursor: zoom-in"
121
+ @click="openFullImage(getFileUrl(attachmentId))"
122
+ />
123
+ </v-sheet>
124
+ </div>
125
+ </v-sheet>
126
+ </v-col>
80
127
  </v-row>
81
128
  </template>
82
129
 
@@ -97,6 +144,8 @@
97
144
  : 'text'
98
145
  "
99
146
  color="error"
147
+ :loading="!!loadingActions[getKey(item, group.set)]"
148
+ :disabled="!!loadingActions[getKey(item, group.set)]"
100
149
  @click.stop="handleItemActionClick(item, group.set, 'reject')"
101
150
  />
102
151
  </v-col>
@@ -221,6 +270,69 @@
221
270
  </v-card>
222
271
  </v-dialog>
223
272
 
273
+ <v-dialog v-model="showRejectModal" max-width="600" persistent>
274
+ <v-card>
275
+ <v-card-title class="text-h5 pa-4">Reject Checklist Item</v-card-title>
276
+
277
+ <v-divider />
278
+
279
+ <v-card-text class="pa-4">
280
+ <v-row>
281
+ <v-col cols="12">
282
+ <InputLabel title="Remarks" :required="true" class="mb-2" />
283
+ <v-textarea
284
+ v-model="rejectModal.remarks"
285
+ placeholder="Enter remarks"
286
+ rows="4"
287
+ variant="outlined"
288
+ density="comfortable"
289
+ hide-details
290
+ />
291
+ </v-col>
292
+
293
+ <v-col cols="12">
294
+ <v-divider class="mb-4" />
295
+ <PhotoUpload
296
+ title="Take/Upload Photo"
297
+ :required="false"
298
+ v-model="rejectModal.photos"
299
+ v-model:photo-ids="rejectModal.photoIds"
300
+ @error="handlePhotoError"
301
+ />
302
+ </v-col>
303
+ </v-row>
304
+ </v-card-text>
305
+
306
+ <v-divider />
307
+
308
+ <v-card-actions class="pa-0">
309
+ <v-btn
310
+ color="grey"
311
+ variant="flat"
312
+ rounded="0"
313
+ class="flex-grow-1 text-none"
314
+ height="48"
315
+ :disabled="rejectModal.submitting"
316
+ @click="closeRejectDialog"
317
+ >
318
+ Cancel
319
+ </v-btn>
320
+ <v-btn
321
+ color="error"
322
+ variant="flat"
323
+ rounded="0"
324
+ class="flex-grow-1 text-none"
325
+ height="48"
326
+ :loading="rejectModal.submitting"
327
+ :disabled="!isRejectModalValid || rejectModal.submitting"
328
+ @click="submitRejectDialog"
329
+ >
330
+ Confirm
331
+ </v-btn>
332
+ </v-card-actions>
333
+ </v-card>
334
+ </v-dialog>
335
+
224
336
  <v-dialog v-model="showAttachmentDialog" max-width="700" scrollable>
225
337
  <v-card>
226
338
  <v-card-title class="d-flex align-center pa-4">
@@ -492,6 +604,20 @@ const isModalValid = computed(() => {
492
604
  return modalData.remark?.trim().length > 0;
493
605
  });
494
606
 
607
+ const showRejectModal = ref(false);
608
+ const rejectModal = reactive({
609
+ item: null as TUnitChecklistItem | null,
610
+ set: undefined as number | undefined,
611
+ remarks: "",
612
+ photos: [] as string[],
613
+ photoIds: [] as string[],
614
+ submitting: false,
615
+ });
616
+
617
+ const isRejectModalValid = computed(
618
+ () => rejectModal.remarks.trim().length > 0
619
+ );
620
+
495
621
  const {
496
622
  data: getUnitCleanerChecklistReq,
497
623
  refresh: getUnitCleanerChecklistRefresh,
@@ -590,6 +716,29 @@ function resetModalData(): void {
590
716
  modalData.lastApprovedKey = null;
591
717
  }
592
718
 
719
+ function resetRejectModal(): void {
720
+ rejectModal.item = null;
721
+ rejectModal.set = undefined;
722
+ rejectModal.remarks = "";
723
+ rejectModal.photos = [];
724
+ rejectModal.photoIds = [];
725
+ rejectModal.submitting = false;
726
+ }
727
+
728
+ function closeRejectDialog(): void {
729
+ if (rejectModal.submitting) return;
730
+ showRejectModal.value = false;
731
+ resetRejectModal();
732
+ }
733
+
734
+ function openRejectDialog(item: TUnitChecklistItem, set: number): void {
735
+ rejectModal.item = item;
736
+ rejectModal.set = set;
737
+ rejectModal.remarks = item.remarks || "";
738
+ rejectModal.photoIds = item.attachment || [];
739
+ showRejectModal.value = true;
740
+ }
741
+
593
742
  function closeModal(): void {
594
743
  const setNumber = modalData.currentSet;
595
744
  showCompletionModal.value = false;
@@ -661,6 +810,41 @@ async function submitModal(): Promise<void> {
661
810
  closeModal();
662
811
  }
663
812
 
813
+ async function submitRejectDialog(): Promise<void> {
814
+ const item = rejectModal.item;
815
+ const set = rejectModal.set;
816
+
817
+ if (!item || set === undefined || !isRejectModalValid.value) return;
818
+
819
+ const key = getKey(item, set);
820
+ rejectModal.submitting = true;
821
+ loadingActions[key] = true;
822
+
823
+ try {
824
+ const response = await updateUnitChecklist(
825
+ props.scheduleAreaId,
826
+ item.unit,
827
+ set,
828
+ "reject",
829
+ rejectModal.remarks.trim(),
830
+ rejectModal.photoIds
831
+ );
832
+ showMessage(response?.message || "Unit rejected successfully", "success");
833
+ showRejectModal.value = false;
834
+ resetRejectModal();
835
+ await getUnitCleanerChecklistRefresh();
836
+ } catch (error: any) {
837
+ console.error("Error rejecting unit checklist:", error);
838
+ showMessage(
839
+ error?.data?.message || error?.message || "Failed to reject checklist",
840
+ "error"
841
+ );
842
+ } finally {
843
+ delete loadingActions[key];
844
+ rejectModal.submitting = false;
845
+ }
846
+ }
847
+
664
848
  async function handleItemActionClick(
665
849
  item: TUnitChecklistItem,
666
850
  set: number | undefined,
@@ -688,13 +872,13 @@ async function handleItemActionClick(
688
872
  return;
689
873
  }
690
874
 
691
- activeActions[key] = action;
692
-
693
875
  if (action === "reject") {
694
- await _callUpdateUnitChecklist(item, set, action);
876
+ openRejectDialog(item, set);
695
877
  return;
696
878
  }
697
879
 
880
+ activeActions[key] = action;
881
+
698
882
  // Approve: check if this completes the entire set
699
883
  lastApprovedKey.value = key;
700
884
 
@@ -35,6 +35,7 @@
35
35
  <v-window-item value="daily">
36
36
  <DailyReportTab
37
37
  :active="activeTab === 'daily'"
38
+ :site="site"
38
39
  :filters="filters"
39
40
  :options="options"
40
41
  @update:filters="updateFilters"
@@ -45,6 +46,7 @@
45
46
  <v-window-item value="monthly">
46
47
  <MonthlyReportTab
47
48
  :active="activeTab === 'monthly'"
49
+ :site="site"
48
50
  :filters="filters"
49
51
  :options="options"
50
52
  @update:filters="updateFilters"
@@ -16,10 +16,12 @@
16
16
  </template>
17
17
 
18
18
  <ReportEmptyState
19
- v-else-if="!loading"
20
- title="No Daily Report Selected"
21
- message="Please select a route, time range, and date to view the daily report"
19
+ v-else-if="!loading && notFound"
20
+ title="No Daily Report"
21
+ message="No patrol log for this date"
22
22
  />
23
+
24
+ <ReportEmptyState v-else-if="!loading" />
23
25
  </div>
24
26
  </template>
25
27
 
@@ -36,6 +38,7 @@ import SummaryReportTable from "./SummaryReportTable.vue";
36
38
 
37
39
  const props = defineProps<{
38
40
  active: boolean;
41
+ site: string; // ✅ thêm
39
42
  filters: NFCPatrolReportFilters;
40
43
  options: NFCPatrolReportFilterOptions;
41
44
  }>();
@@ -45,7 +48,10 @@ defineEmits<{
45
48
  export: [];
46
49
  }>();
47
50
 
48
- const { report, loading, fetchReport } = useNFCPatrolDailyReport(props.filters);
51
+ const { report, loading, notFound, fetchReport } = useNFCPatrolDailyReport(
52
+ props.filters,
53
+ props.site, // ✅
54
+ );
49
55
 
50
56
  watch(
51
57
  () => [props.filters.route, props.filters.timeRange, props.filters.date, props.active],
@@ -36,6 +36,7 @@ import SummaryReportTable from "./SummaryReportTable.vue";
36
36
 
37
37
  const props = defineProps<{
38
38
  active: boolean;
39
+ site: string;
39
40
  filters: NFCPatrolReportFilters;
40
41
  options: NFCPatrolReportFilterOptions;
41
42
  }>();
@@ -45,7 +46,8 @@ defineEmits<{
45
46
  export: [];
46
47
  }>();
47
48
 
48
- const { report, loading, fetchReport } = useNFCPatrolMonthlyReport(props.filters);
49
+ const { report, loading, fetchReport } =
50
+ useNFCPatrolMonthlyReport(props.filters, props.site);
49
51
 
50
52
  watch(
51
53
  () => [props.filters.route, props.filters.timeRange, props.filters.date, props.active],
@@ -22,6 +22,11 @@
22
22
  />
23
23
  </template>
24
24
 
25
+ <ReportEmptyState
26
+ v-else-if="!loading && notFound"
27
+ title="No Patrol Log"
28
+ message="No patrol log for this date"
29
+ />
25
30
  <ReportEmptyState v-else-if="!loading" />
26
31
 
27
32
  <RemarksDialog v-model="dialogRemarks" :remarks="selectedActivityRemarks" />
@@ -56,6 +61,7 @@ defineEmits<{
56
61
  const {
57
62
  report,
58
63
  loading,
64
+ notFound,
59
65
  selectedActivityRemarks,
60
66
  dialogRemarks,
61
67
  fetchReport,
@@ -1,19 +1,98 @@
1
1
  import type {
2
2
  NFCPatrolReportFilters,
3
3
  NFCPatrolSummaryReport,
4
+ NFCPatrolReportSummaryRow,
4
5
  } from "../types/nfc-patrol-report";
5
- import { useNFCPatrolReportService } from "../services/nfcPatrolReport";
6
+ import useNFCPatrolLog from "./useNFCPatrolLog";
7
+ import useSite from "./useSite";
8
+
9
+ interface NFCPatrolLogListResponse {
10
+ items?: Record<string, any>[];
11
+ pages?: number;
12
+ pageRange?: string;
13
+ }
14
+
15
+ function buildAddress(address?: Record<string, string>) {
16
+ if (!address) return undefined;
17
+ const parts = [
18
+ address.line1,
19
+ address.line2,
20
+ address.city,
21
+ address.state,
22
+ address.postalCode,
23
+ address.country,
24
+ ].filter((p) => p && p.trim() !== "");
25
+ return parts.length ? parts.join(", ") : undefined;
26
+ }
27
+
28
+ function mapLogToSummaryRow(log: Record<string, any>, index: number): NFCPatrolSummaryRow {
29
+ const checkpoints: Record<string, any>[] = log.checkPoints ?? [];
30
+ const checked = checkpoints.filter((cp) => cp.status === "Completed").length;
31
+ const missed = checkpoints.filter((cp) => cp.status === "Skipped").length;
32
+
33
+ return {
34
+ id: log._id,
35
+ routeName: `${index + 1}. ${log.route?.name ?? "-"}`,
36
+ securityCheckSchedule: log.route?.startTime ?? "-",
37
+ checked,
38
+ missed,
39
+ status: missed === 0 ? "Completed" : "Incomplete",
40
+ };
41
+ }
42
+
43
+ export function useNFCPatrolDailyReport(
44
+ filters: NFCPatrolReportFilters,
45
+ site: string,
46
+ ) {
47
+ const { getAll: getPatrolLogs } = useNFCPatrolLog();
48
+ const { getSiteById } = useSite();
6
49
 
7
- export function useNFCPatrolDailyReport(filters: NFCPatrolReportFilters) {
8
- const reportService = useNFCPatrolReportService();
9
50
  const report = ref<NFCPatrolSummaryReport | null>(null);
10
51
  const loading = ref(false);
52
+ const notFound = ref(false);
11
53
 
12
54
  async function fetchReport() {
55
+ if (!site || !filters.date || !filters.route) {
56
+ report.value = null;
57
+ notFound.value = false;
58
+ return;
59
+ }
60
+
13
61
  loading.value = true;
62
+ notFound.value = false;
14
63
 
15
64
  try {
16
- report.value = await reportService.getDailyReport(filters);
65
+ const [logsRes, siteInfo] = await Promise.all([
66
+ getPatrolLogs<NFCPatrolLogListResponse>({
67
+ page: 1,
68
+ limit: 100,
69
+ site,
70
+ date: filters.date,
71
+ routeId: filters.route,
72
+ routeStartTime: filters.timeRange
73
+ ? filters.timeRange.split(" - ")[0]
74
+ : undefined,
75
+ }),
76
+ getSiteById(site),
77
+ ]);
78
+
79
+ const logs = logsRes.items ?? [];
80
+
81
+ if (logs.length) {
82
+ const routeName = logs[0]?.route?.name ?? "-";
83
+
84
+ report.value = {
85
+ location: {
86
+ name: siteInfo.name,
87
+ address: buildAddress(siteInfo.address),
88
+ },
89
+ title: `Patrol Route Summary: ${routeName}`,
90
+ rows: logs.map(mapLogToSummaryRow),
91
+ };
92
+ } else {
93
+ report.value = null;
94
+ notFound.value = true;
95
+ }
17
96
  } finally {
18
97
  loading.value = false;
19
98
  }
@@ -22,6 +101,7 @@ export function useNFCPatrolDailyReport(filters: NFCPatrolReportFilters) {
22
101
  return {
23
102
  report,
24
103
  loading,
104
+ notFound,
25
105
  fetchReport,
26
106
  };
27
107
  }
@@ -3,8 +3,9 @@ interface NFCPatrolLogQuery {
3
3
  limit?: number;
4
4
  site: string;
5
5
  date: string;
6
+ type?: string;
6
7
  routeId: string;
7
- routeStartTime: string;
8
+ routeStartTime?: string;
8
9
  }
9
10
 
10
11
  export default function useNFCPatrolLog() {
@@ -13,6 +14,7 @@ export default function useNFCPatrolLog() {
13
14
  limit = 10,
14
15
  site,
15
16
  date,
17
+ type,
16
18
  routeId,
17
19
  routeStartTime,
18
20
  }: NFCPatrolLogQuery) {
@@ -23,6 +25,7 @@ export default function useNFCPatrolLog() {
23
25
  limit,
24
26
  site,
25
27
  date,
28
+ type,
26
29
  "route._id": routeId,
27
30
  "route.startTime": routeStartTime,
28
31
  },
@@ -1,19 +1,104 @@
1
1
  import type {
2
2
  NFCPatrolReportFilters,
3
3
  NFCPatrolSummaryReport,
4
+ NFCPatrolReportSummaryRow,
4
5
  } from "../types/nfc-patrol-report";
5
- import { useNFCPatrolReportService } from "../services/nfcPatrolReport";
6
+ import useNFCPatrolLog from "./useNFCPatrolLog";
7
+ import useSite from "./useSite";
8
+
9
+ interface NFCPatrolLogListResponse {
10
+ items?: Record<string, any>[];
11
+ pages?: number;
12
+ pageRange?: string;
13
+ }
14
+
15
+ function buildAddress(address?: Record<string, string>) {
16
+ if (!address) return undefined;
17
+
18
+ const parts = [
19
+ address.line1,
20
+ address.line2,
21
+ address.city,
22
+ address.state,
23
+ address.postalCode,
24
+ address.country,
25
+ ].filter((p) => p && p.trim() !== "");
26
+
27
+ return parts.length ? parts.join(", ") : undefined;
28
+ }
29
+
30
+ function mapLogToSummaryRow(
31
+ log: Record<string, any>,
32
+ index: number,
33
+ ): NFCPatrolReportSummaryRow {
34
+ const checkpoints: Record<string, any>[] = log.checkPoints ?? [];
35
+ const checked = checkpoints.filter((cp) => cp.status === "Completed").length;
36
+ const missed = checkpoints.filter((cp) => cp.status === "Skipped").length;
37
+
38
+ return {
39
+ id: log._id,
40
+ routeName: `${index + 1}. ${log.route?.name ?? "-"}`,
41
+ securityCheckSchedule: log.route?.startTime ?? "-",
42
+ checked,
43
+ missed,
44
+ status: missed === 0 ? "Completed" : "Incomplete",
45
+ };
46
+ }
47
+
48
+ export function useNFCPatrolMonthlyReport(
49
+ filters: NFCPatrolReportFilters,
50
+ site: string,
51
+ ) {
52
+ const { getAll: getPatrolLogs } = useNFCPatrolLog();
53
+ const { getSiteById } = useSite();
6
54
 
7
- export function useNFCPatrolMonthlyReport(filters: NFCPatrolReportFilters) {
8
- const reportService = useNFCPatrolReportService();
9
55
  const report = ref<NFCPatrolSummaryReport | null>(null);
10
56
  const loading = ref(false);
57
+ const notFound = ref(false);
11
58
 
12
59
  async function fetchReport() {
60
+ if (!site || !filters.date || !filters.route) {
61
+ report.value = null;
62
+ notFound.value = false;
63
+ return;
64
+ }
65
+
13
66
  loading.value = true;
67
+ notFound.value = false;
14
68
 
15
69
  try {
16
- report.value = await reportService.getMonthlyReport(filters);
70
+ const [logsRes, siteInfo] = await Promise.all([
71
+ getPatrolLogs<NFCPatrolLogListResponse>({
72
+ page: 1,
73
+ limit: 100,
74
+ site,
75
+ date: filters.date,
76
+ routeId: filters.route,
77
+ routeStartTime: filters.timeRange
78
+ ? filters.timeRange.split(" - ")[0]
79
+ : undefined,
80
+ type: "month",
81
+ }),
82
+ getSiteById(site),
83
+ ]);
84
+
85
+ const logs = logsRes.items ?? [];
86
+
87
+ if (logs.length) {
88
+ const routeName = logs[0]?.route?.name ?? "-";
89
+
90
+ report.value = {
91
+ location: {
92
+ name: siteInfo.name,
93
+ address: buildAddress(siteInfo.address),
94
+ },
95
+ title: `Patrol Route Summary: ${routeName}`,
96
+ rows: logs.map(mapLogToSummaryRow),
97
+ };
98
+ } else {
99
+ report.value = null;
100
+ notFound.value = true;
101
+ }
17
102
  } finally {
18
103
  loading.value = false;
19
104
  }
@@ -22,6 +107,7 @@ export function useNFCPatrolMonthlyReport(filters: NFCPatrolReportFilters) {
22
107
  return {
23
108
  report,
24
109
  loading,
110
+ notFound,
25
111
  fetchReport,
26
112
  };
27
113
  }
@@ -3,12 +3,80 @@ import type {
3
3
  NFCPatrolReport,
4
4
  NFCPatrolReportFilters,
5
5
  } from "../types/nfc-patrol-report";
6
+ import useNFCPatrolLog from "./useNFCPatrolLog";
7
+ import useSite from "./useSite";
6
8
 
7
- interface NFCPatrolLogResponse {
8
- data?: NFCPatrolLogResponse | NFCPatrolReport;
9
- item?: NFCPatrolReport;
10
- items?: NFCPatrolReport[];
11
- nfcPatrolLog?: NFCPatrolReport;
9
+ interface NFCPatrolLogListResponse {
10
+ items?: Record<string, any>[];
11
+ pages?: number;
12
+ pageRange?: string;
13
+ }
14
+
15
+ function formatTime(iso?: string) {
16
+ return iso ? iso.slice(11, 16) : "-";
17
+ }
18
+ function formatDuration(totalMinutes: number) {
19
+ if (totalMinutes < 60) return `${totalMinutes} mins`;
20
+
21
+ const hrs = Math.floor(totalMinutes / 60);
22
+ const mins = totalMinutes % 60;
23
+
24
+ if (mins === 0) return `${hrs} hrs`;
25
+ return `${hrs} hrs ${mins} mins`;
26
+ }
27
+
28
+ function diffMinutes(startIso?: string, endIso?: string) {
29
+ if (!startIso || !endIso) return "-";
30
+ const totalMinutes = Math.max(
31
+ 0,
32
+ Math.round((new Date(endIso).getTime() - new Date(startIso).getTime()) / 60000),
33
+ );
34
+ return formatDuration(totalMinutes);
35
+ }
36
+
37
+
38
+ function mapLogToPatrolReport(
39
+ log: Record<string, any>,
40
+ location: NFCPatrolReport["location"],
41
+ ): NFCPatrolReport {
42
+ const checkpoints: Record<string, any>[] = log.checkPoints ?? [];
43
+ const actualStart = checkpoints[0]?.startDateTime ?? log.startDateTime;
44
+ const actualEnd = checkpoints[checkpoints.length - 1]?.endDateTime ?? log.endDateTime;
45
+
46
+ const totalTravelMinutes = checkpoints.reduce(
47
+ (sum, cp) => sum + (Number(cp.travelTime) || 0),
48
+ 0,
49
+ );
50
+
51
+ const activities: NFCPatrolActivity[] = checkpoints.map((cp) => ({
52
+ id: cp.nfcTag_id,
53
+ checkpoint: cp.nfcTag_name,
54
+ startTime: formatTime(cp.startDateTime),
55
+ endTime: formatTime(cp.endDateTime),
56
+ actualTime: diffMinutes(cp.startDateTime, cp.endDateTime),
57
+ expectedTime: formatDuration(cp.travelTime),
58
+ status: cp.status,
59
+ remarks: cp.skippedRemarks && cp.skippedRemarks !== "N/A" ? cp.skippedRemarks : undefined,
60
+ }));
61
+
62
+ return {
63
+ location,
64
+ routeSummary: {
65
+ routeName: log.route?.name,
66
+ tolerance: totalTravelMinutes,
67
+ schedule: {
68
+ start: log.route?.startTime,
69
+ end: log.route?.endTime,
70
+ totalTime: diffMinutes(log.startDateTime, log.endDateTime),
71
+ },
72
+ actual: {
73
+ start: formatTime(actualStart),
74
+ end: formatTime(actualEnd),
75
+ totalTime: diffMinutes(actualStart, actualEnd),
76
+ },
77
+ },
78
+ activities,
79
+ };
12
80
  }
13
81
 
14
82
  export function useNFCPatrolReport(
@@ -16,43 +84,51 @@ export function useNFCPatrolReport(
16
84
  site: string,
17
85
  ) {
18
86
  const { getAll: getPatrolLogs } = useNFCPatrolLog();
87
+ const { getSiteById } = useSite();
88
+
19
89
  const report = ref<NFCPatrolReport | null>(null);
20
90
  const loading = ref(false);
91
+ const notFound = ref(false);
21
92
  const selectedActivityRemarks = ref("");
22
93
  const dialogRemarks = ref(false);
23
94
 
24
95
  async function fetchReport() {
25
96
  if (!site || !filters.date || !filters.route || !filters.timeRange) {
26
97
  report.value = null;
98
+ notFound.value = false;
27
99
  return;
28
100
  }
29
101
 
30
102
  loading.value = true;
103
+ notFound.value = false;
31
104
 
32
105
  try {
33
- const response = await getPatrolLogs<NFCPatrolLogResponse>({
34
- page: 1,
35
- limit: 10,
36
- site,
37
- date: filters.date,
38
- routeId: filters.route,
39
- routeStartTime: filters.timeRange.split(" - ")[0],
40
- });
41
-
42
- report.value = extractReport(response);
43
- } finally {
44
- loading.value = false;
45
- }
46
- }
106
+ const [logsRes, siteInfo] = await Promise.all([
107
+ getPatrolLogs<NFCPatrolLogListResponse>({
108
+ page: 1,
109
+ limit: 10,
110
+ site,
111
+ date: filters.date,
112
+ routeId: filters.route,
113
+ routeStartTime: filters.timeRange.split(" - ")[0],
114
+ }),
115
+ getSiteById(site),
116
+ ]);
47
117
 
48
- function extractReport(response: NFCPatrolLogResponse): NFCPatrolReport | null {
49
- const payload = response.data ?? response;
118
+ const log = logsRes.items?.[0];
50
119
 
51
- if ("location" in payload && "activities" in payload) {
52
- return payload;
120
+ if (log) {
121
+ report.value = mapLogToPatrolReport(log, {
122
+ name: siteInfo.name,
123
+ address: buildAddress(siteInfo.address),
124
+ });
125
+ } else {
126
+ report.value = null;
127
+ notFound.value = true;
128
+ }
129
+ } finally {
130
+ loading.value = false;
53
131
  }
54
-
55
- return payload.nfcPatrolLog ?? payload.item ?? payload.items?.[0] ?? null;
56
132
  }
57
133
 
58
134
  function openRemarksDialog(activity: NFCPatrolActivity) {
@@ -60,9 +136,33 @@ export function useNFCPatrolReport(
60
136
  dialogRemarks.value = true;
61
137
  }
62
138
 
139
+ function buildAddress(address?: {
140
+ line1?: string;
141
+ line2?: string;
142
+ city?: string;
143
+ state?: string;
144
+ country?: string;
145
+ postalCode?: string;
146
+ }) {
147
+ if (!address) return undefined;
148
+
149
+ const parts = [
150
+ address.line1,
151
+ address.line2,
152
+ address.city,
153
+ address.state,
154
+ address.postalCode,
155
+ address.country,
156
+ ].filter((part) => part && part.trim() !== "");
157
+
158
+ return parts.length ? parts.join(", ") : undefined;
159
+ }
160
+
161
+
63
162
  return {
64
163
  report,
65
164
  loading,
165
+ notFound,
66
166
  selectedActivityRemarks,
67
167
  dialogRemarks,
68
168
  fetchReport,
@@ -2,14 +2,22 @@ import type {
2
2
  NFCPatrolReportFilterOptions,
3
3
  NFCPatrolReportFilters,
4
4
  } from "../types/nfc-patrol-report";
5
+ import useNFCPatrolRoute from "./useNFCPatrolRoute";
5
6
 
7
+ function getTodayDate() {
8
+ const now = new Date();
9
+ const y = now.getFullYear();
10
+ const m = String(now.getMonth() + 1).padStart(2, "0");
11
+ const d = String(now.getDate()).padStart(2, "0");
12
+ return `${y}-${m}-${d}`;
13
+ }
6
14
  export function useNFCPatrolReportFilters(site: string) {
7
15
  const { getAll: getPatrolRoutes } = useNFCPatrolRoute();
8
16
 
9
17
  const filters = reactive<NFCPatrolReportFilters>({
10
18
  route: "",
11
19
  timeRange: "",
12
- date: "2025-11-27",
20
+ date: getTodayDate(),
13
21
  });
14
22
 
15
23
  const options = ref<NFCPatrolReportFilterOptions>({
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.30-staging.1",
5
+ "version": "3.0.30-staging.3",
6
6
  "author": "7365admin1",
7
7
  "main": "./nuxt.config.ts",
8
8
  "publishConfig": {
@@ -25,8 +25,8 @@ export interface NFCPatrolReportFilterOptions {
25
25
  }
26
26
 
27
27
  export interface NFCPatrolReportLocation {
28
- name: string;
29
- address: string;
28
+ name?: string;
29
+ address?: string;
30
30
  }
31
31
 
32
32
  export interface NFCPatrolRouteSummary {