diff --git a/src/frontend/routes/apps.$appId.villages.$villageId.tsx b/src/frontend/routes/apps.$appId.villages.$villageId.tsx index c695c99..c39d3d8 100644 --- a/src/frontend/routes/apps.$appId.villages.$villageId.tsx +++ b/src/frontend/routes/apps.$appId.villages.$villageId.tsx @@ -36,6 +36,7 @@ import { TbChartBar, TbClock, TbEdit, + TbFileText, TbHome2, TbLayoutKanban, TbMapPin, @@ -450,6 +451,7 @@ function VillageDetailPage() { const [editModalOpened, { open: openEditModal, close: closeEditModal }] = useDisclosure(false) const [isUpdating, setIsUpdating] = useState(false) const [isEditing, setIsEditing] = useState(false) + const [isExporting, setIsExporting] = useState(false) const [editForm, setEditForm] = useState({ name: '', desc: '', isDummy: false }) const village = infoRes?.data @@ -523,6 +525,216 @@ function VillageDetailPage() { } } + const handleDownloadPDF = async () => { + if (!village || !stats) return + setIsExporting(true) + try { + const [activityRes, peakRes, logsRes, inactiveRes] = await Promise.all([ + fetch(API_URLS.graphLogVillages(villageId, 'daily')).then(r => r.json()), + fetch(API_URLS.getPeakHours(villageId)).then(r => r.json()), + fetch(API_URLS.getRecentVillageLogs(villageId)).then(r => r.json()), + fetch(API_URLS.getInactiveUsers(7, villageId, 1)).then(r => r.json()), + ]) + + const activityData: { label: string; aktivitas: number }[] = activityRes?.data || [] + const peakHours: { hour: number; label: string; count: number }[] = peakRes?.data?.hours || [] + const peak: { label: string; count: number } | null = peakRes?.data?.peak || null + const recentLogs: { timestamp: string; userName: string; action: string; desc: string }[] = logsRes?.data || [] + const inactiveUsers: any[] = inactiveRes?.data?.users || [] + const totalInactive: number = inactiveRes?.data?.total ?? 0 + + const generatedAt = dayjs().format('DD MMM YYYY HH:mm') + + const maxActivity = Math.max(...activityData.map(d => d.aktivitas), 1) + const activityRows = activityData.map((d, i) => ` +
Village Head (Perbekel): ${village.perbekel || '-'}
+Status: ${village.isActive ? 'Active' : 'Inactive'}${village.isDummy ? ' · Dummy Data' : ''}
+Created: ${village.createdAt} · Last Updated: ${village.updatedAt || '-'}
+Generated: ${generatedAt}
+No activity data available.
' + : `| Date | Distribution | Count |
|---|
Busiest hour: ${peak.label} (${peak.count.toLocaleString()} activities)
` + : 'No peak data available.
'} +| Hour | Distribution | Count |
|---|---|---|
| No data | ||
No recent activity recorded.
' + : `| Time | +User | +Action | +Description | +
|---|
| Name / Email | +Role | +Group / Position | +Last Activity | +
|---|