From 2d7ddbb96a0f1e75565005f03baeef64c1d2e974 Mon Sep 17 00:00:00 2001 From: Edison Date: Fri, 2 Jan 2026 14:51:36 +0800 Subject: [PATCH] department filter and excel support --- backend/managerRoutes.js | 44 ++++++++++++-- src/components/PersonnelManagement.vue | 81 +++++++++++++++++++------- src/locales/en.json | 5 ++ src/locales/ms.json | 5 ++ src/locales/tm.json | 6 +- 5 files changed, 114 insertions(+), 27 deletions(-) diff --git a/backend/managerRoutes.js b/backend/managerRoutes.js index 4af93bb..abdf0a7 100644 --- a/backend/managerRoutes.js +++ b/backend/managerRoutes.js @@ -394,6 +394,7 @@ export default function () { const wantXlsx = String(req.query.format || 'csv').toLowerCase() === 'xlsx' let workerIdClause = '' + let departmentClause = '' const params = [`${startDate} 00:00:00`, `${endDate} 23:59:59`] if (workerIds) { @@ -406,6 +407,12 @@ export default function () { } } + const { department } = req.query + if (department) { + departmentClause = ` AND LOWER(w.department) = LOWER(?)` + params.push(department) + } + const query = ` SELECT cr.worker_id, @@ -418,7 +425,7 @@ export default function () { FROM clock_records cr JOIN workers w ON cr.worker_id = w.id LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id - WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause} + WHERE cr.timestamp BETWEEN ? AND ? ${workerIdClause}${departmentClause} AND cr.event_type IN ('clock_in','clock_out') ORDER BY cr.worker_id, cr.timestamp ASC ` @@ -850,9 +857,16 @@ export default function () { let whereClauses = ["w.role = 'worker'", "w.status != 'deleted'"] // Filter out soft-deleted workers if (search) { - whereClauses.push(`(w.full_name LIKE ? OR w.department LIKE ?)`) - params.push(searchTerm, searchTerm) - countParams.push(searchTerm, searchTerm) + whereClauses.push(`w.full_name LIKE ?`) + params.push(searchTerm) + countParams.push(searchTerm) + } + + const { department } = req.query + if (department) { + whereClauses.push(`LOWER(w.department) = LOWER(?)`) + params.push(department) + countParams.push(department) } if (whereClauses.length > 0) { @@ -1479,6 +1493,28 @@ export default function () { db.release() } }) + // GET distinct departments for filter tabs + router.get('/departments', checkPermission('view_all'), async (req, res) => { + const db = await getConnection() + try { + const [rows] = await db.execute(` + SELECT DISTINCT department + FROM workers + WHERE role = 'worker' + AND status != 'deleted' + AND department IS NOT NULL + AND department != '' + ORDER BY department ASC + `) + const departments = rows.map((r) => r.department) + res.json(departments) + } catch (error) { + console.error('Get departments error:', error) + res.status(500).json({ message: 'Database error fetching departments.', details: error.message }) + } finally { + db.release() + } + }) return router } diff --git a/src/components/PersonnelManagement.vue b/src/components/PersonnelManagement.vue index 0460495..c55980b 100644 --- a/src/components/PersonnelManagement.vue +++ b/src/components/PersonnelManagement.vue @@ -51,25 +51,39 @@

{{ $t('workerRoster') }}

-
-
- +
+
- -
-
- - -
-
- - -
+
+ + +
+
+ + +
+
+ + +
+
+
@@ -415,6 +430,7 @@ const viewRecords = (workerId) => { workers: workers.value, selectedWorkerIds: selectedWorkerIds.value, exportFilters: exportFilters.value, + selectedDepartment: selectedDepartment.value, }; sessionStorage.setItem('personnelSearchState', JSON.stringify(searchState)); @@ -452,6 +468,8 @@ const exportFilters = ref({ startDate: '', endDate: '' }); const exportLoading = ref(false); const showClearDeviceConfirm = ref(false); const showDeleteConfirm = ref(false); +const departments = ref([]); +const selectedDepartment = ref(''); // --- COMPUTED --- const isFormValid = computed( @@ -467,6 +485,10 @@ const isAllSelected = computed( // --- WATCHERS --- watch(searchQuery, () => fetchWorkers(1)); +watch(selectedDepartment, () => { + currentPage.value = 1; + fetchWorkers(1); +}); watch(currentPage, (newPage) => { selectedWorkerIds.value = []; jumpToPageInput.value = newPage; @@ -476,9 +498,11 @@ watch(currentPage, (newPage) => { const fetchWorkers = async (page = currentPage.value) => { loading.value = true; try { - const data = await apiFetch( - `/api/managers/workers?search=${searchQuery.value}&page=${page}&limit=${pageSize.value}` - ); + let url = `/api/managers/workers?search=${encodeURIComponent(searchQuery.value)}&page=${page}&limit=${pageSize.value}`; + if (selectedDepartment.value) { + url += `&department=${encodeURIComponent(selectedDepartment.value)}`; + } + const data = await apiFetch(url); workers.value = data.workers; totalWorkers.value = data.totalCount; @@ -496,6 +520,14 @@ const fetchWorkers = async (page = currentPage.value) => { loading.value = false; } }; +const fetchDepartments = async () => { + try { + const data = await apiFetch('/api/managers/departments'); + departments.value = data; + } catch (_err) { + console.error('Failed to fetch departments'); + } +}; const changePage = (page) => { if (page > 0 && page <= totalPages.value) { @@ -693,9 +725,14 @@ const exportWorkHours = async () => { const { startDate, endDate } = exportFilters.value; const workerIds = selectedWorkerIds.value.join(','); + let exportUrl = `${import.meta.env.VITE_API_BASE_URL}/api/managers/attendance-records/export?format=xlsx&startDate=${startDate}&endDate=${endDate}&workerIds=${workerIds}`; + if (selectedDepartment.value) { + exportUrl += `&department=${encodeURIComponent(selectedDepartment.value)}`; + } + try { const response = await fetch( - `${import.meta.env.VITE_API_BASE_URL}/api/managers/attendance-records/export?format=xlsx&startDate=${startDate}&endDate=${endDate}&workerIds=${workerIds}`, + exportUrl, { headers: { Authorization: `Bearer ${sessionStorage.getItem('token')}`, @@ -721,6 +758,7 @@ const exportWorkHours = async () => { }; onMounted(() => { + fetchDepartments(); const savedSearchState = sessionStorage.getItem('personnelSearchState'); if (savedSearchState) { try { @@ -732,6 +770,7 @@ onMounted(() => { workers.value = searchState.workers || []; selectedWorkerIds.value = searchState.selectedWorkerIds || []; exportFilters.value = searchState.exportFilters || { startDate: '', endDate: '' }; + selectedDepartment.value = searchState.selectedDepartment || ''; sessionStorage.removeItem('personnelSearchState'); } catch (_e) { fetchWorkers(); diff --git a/src/locales/en.json b/src/locales/en.json index caf3ac7..edf61f7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -115,6 +115,7 @@ "chooseTag": "-- Choose a tag --", "addByTag": "Add by Tag", "selectedForReport": "Selected for Report ({count})", + "all": "All", "allWorkersSelected": "All Workers ({count}) Selected", "noWorkersSelected": "No workers selected.", "reportSettings": "2. Report Settings", @@ -139,6 +140,9 @@ "exportAll": "Export All", "export": "Export", + "filterByDepartment": "Filter by Department", + "departmentFilter": "Departments", + "allDepartments": "All Departments", "addNewUser": "Add New User", "fullName": "Full Name", "department": "Department", @@ -159,6 +163,7 @@ "tags": "Tags", "workerRoster": "Employee List", "searchByNameOrUsername": "Search by name/username", + "searchByName": "Search by Name", "searchByNameOrDepartment": "Search by name/department", "filterByTag": "Filter by tag", "clearFilter": "Clear filter", diff --git a/src/locales/ms.json b/src/locales/ms.json index 4939991..549b1b5 100644 --- a/src/locales/ms.json +++ b/src/locales/ms.json @@ -116,6 +116,7 @@ "chooseTag": "-- Pilih tag --", "addByTag": "Tambah melalui Tag", "selectedForReport": "Dipilih untuk Laporan ({count})", + "all": "Semua", "allWorkersSelected": "Semua Pekerja ({count}) Dipilih", "noWorkersSelected": "Tiada pekerja dipilih.", "reportSettings": "2. Tetapan Laporan", @@ -139,6 +140,9 @@ "reportGenerationError": "Ralat semasa menjana laporan.", "exportAll": "Eksport Semua", "export": "Eksport", + "filterByDepartment": "Tapis mengikut Jabatan", + "departmentFilter": "Jabatan:", + "allDepartments": "Semua Jabatan", "addNewUser": "Tambah Pengguna Baru", "fullName": "Nama Penuh", "department": "Jabatan", @@ -158,6 +162,7 @@ "createTag": "Cipta Tag", "tags": "Tag", "workerRoster": "Deftar Pekerja", + "searchByName": "Cari mengikut Nama", "searchByNameOrUsername": "Cari mengikut nama atau nama pengguna", "searchByNameOrDepartment": " Cari nama atau jabatan", "filterByTag": "Tapis mengikut tag", diff --git a/src/locales/tm.json b/src/locales/tm.json index 5f9231f..fd639ab 100644 --- a/src/locales/tm.json +++ b/src/locales/tm.json @@ -97,7 +97,8 @@ "chooseTag": "-- ஒரு டேக்கைத் தேர்ந்தெடுக்கவும் --", "addByTag": "டேக் மூலம் சேர்க்கவும்", "selectedForReport": "அறிக்கைக்காக தேர்ந்தெடுக்கப்பட்டவை ({count})", - "allWorkersSelected": "அனைத்து பணியாளர்கள் ({count}) தேர்ந்தெடுக்கப்பட்டனர்", + "all": "அனைத்தும்", + "allWorkersSelected": "அனைத்து பணியாளர்கள் ({count}) தேர்ந்தெடுக்கப்பட்டனர்", "noWorkersSelected": "பணியாளர்கள் எதுவும் தேர்ந்தெடுக்கப்படவில்லை.", "reportSettings": "2. அறிக்கை அமைப்புகள்", "setting": "அமைப்பு", @@ -133,7 +134,8 @@ "createTag": "டேக் உருவாக்கவும்", "tags": "டேக்குகள்", "workerRoster": "பணியாளர் பட்டியல்", - "searchByNameOrUsername": "பெயர் அல்லது பயனர் பெயர் மூலம் தேடவும்", + "searchByName": "பெயரால் தேடு", + "searchByNameOrUsername": "பெயர் அல்லது பயனர்பெயரால் தேடு", "filterByTag": "டேக் மூலம் வடிகட்டவும்", "clearFilter": "வடிகட்டியைத் துடைக்கவும்", "dateJoined": "சேர்ந்த தேதி",