feat: Update manager permissions and enhance toast notifications with internationalization support

This commit is contained in:
sudomarcma
2025-07-18 18:07:20 +08:00
parent c78539bab8
commit 2855214fae
8 changed files with 176 additions and 61 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ export default function(db) {
}); });
// Definitive version using a dedicated database connection // Definitive version using a dedicated database connection
router.post('/enabled-dates/update', checkPermission('view_all'), async (req, res) => { router.post('/enabled-dates/update', checkPermission('manage_resources'), async (req, res) => {
let connection; // Define connection here to ensure it's accessible in the 'finally' block let connection; // Define connection here to ensure it's accessible in the 'finally' block
try { try {
const { datesToEnable, datesToDisable } = req.body; const { datesToEnable, datesToDisable } = req.body;
+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div <div
class="min-h-screen bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100 transition-colors duration-300"> class="min-h-screen bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100 transition-colors duration-300">
<div class="fixed bottom-4 right-4 space-y-2 z-50"> <div class="fixed bottom-4 right-4 space-y-2 z-50 z-9999">
<component v-for="toast in renderToasts()" :is="toast" :key="toast.key" /> <component v-for="toast in renderToasts()" :is="toast" :key="toast.key" />
</div> </div>
<header <header
+58 -31
View File
@@ -198,9 +198,10 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed, watch } from 'vue'; import { ref, onMounted, computed, watch, nextTick } from 'vue';
import { apiFetch } from '@/api.js'; import { apiFetch } from '@/api.js';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useI18n } from 'vue-i18n';
const managers = ref([]); const managers = ref([]);
const loading = ref(false); const loading = ref(false);
@@ -229,6 +230,7 @@ const newManager = ref({
}); });
const toast = useToast(); const toast = useToast();
const { t: $t } = useI18n();
const isManagerFormValid = computed(() => const isManagerFormValid = computed(() =>
newManager.value.fullName && newManager.value.fullName &&
@@ -243,6 +245,8 @@ const permissionsList = ref([
{ key: 'manager_permissions', label: 'Manage Managers (Add/Edit/Delete & Permissions)' }, { key: 'manager_permissions', label: 'Manage Managers (Add/Edit/Delete & Permissions)' },
]); ]);
const initialLoad = ref(true);
const totalPages = computed(() => { const totalPages = computed(() => {
const pages = Math.ceil(totalManagers.value / pageSize.value); const pages = Math.ceil(totalManagers.value / pageSize.value);
return pages < 1 ? 1 : pages; return pages < 1 ? 1 : pages;
@@ -274,7 +278,7 @@ const fetchManagers = async (page = currentPage.value) => {
errorMessage.value = 'Failed to fetch managers.'; errorMessage.value = 'Failed to fetch managers.';
managers.value = []; managers.value = [];
totalManagers.value = 0; totalManagers.value = 0;
toast.showToast('Failed to fetch managers.', 'error'); toast.showToast($t('fetchManagersFailed'), 'error');
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -296,8 +300,12 @@ const jumpToPage = () => {
}; };
const openSettingsModal = (manager) => { const openSettingsModal = (manager) => {
initialLoad.value = true;
editingManager.value = { ...manager }; editingManager.value = { ...manager };
isSettingsModalVisible.value = true; isSettingsModalVisible.value = true;
nextTick(() => {
initialLoad.value = false;
});
}; };
const closeSettingsModal = () => { const closeSettingsModal = () => {
@@ -315,49 +323,69 @@ const saveManagerSettings = async () => {
saving.value = true; saving.value = true;
passwordErrorMessage.value = ''; passwordErrorMessage.value = '';
passwordSuccessMessage.value = ''; passwordSuccessMessage.value = '';
console.log('Starting saveManagerSettings...');
try { try {
// Save permissions - convert to new structure // Save all changes in parallel for better performance
const permissionsToSave = { const results = await Promise.all([
// Save permissions
apiFetch(`/api/managers/permissions/${editingManager.value.id}`, {
method: 'PUT',
body: JSON.stringify({
view_all: editingManager.value.view_all || false, view_all: editingManager.value.view_all || false,
edit_workers: editingManager.value.edit_workers || false, edit_workers: editingManager.value.edit_workers || false,
manage_resources: editingManager.value.manage_resources || false, manage_resources: editingManager.value.manage_resources || false,
manager_permissions: editingManager.value.manager_permissions || false manager_permissions: editingManager.value.manager_permissions || false
}; }),
const permissionsResponse = await apiFetch(`/api/managers/permissions/${editingManager.value.id}`, { }),
method: 'PUT',
body: JSON.stringify(permissionsToSave),
});
if (!permissionsResponse.success) {
throw new Error(permissionsResponse.message || 'Failed to update permissions.');
}
// Save manager details // Save manager details
await apiFetch(`/api/managers/managers/${editingManager.value.id}`, { apiFetch(`/api/managers/managers/${editingManager.value.id}`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ body: JSON.stringify({
department: editingManager.value.department, department: editingManager.value.department,
position: editingManager.value.position, position: editingManager.value.position,
status: editingManager.value.isActive ? 'active' : 'inactive', status: editingManager.value.isActive ? 'active' : 'inactive',
}), }),
}); }),
// Save password if changed // Save password if changed
if (newPassword.value) { newPassword.value && newPassword.value === confirmNewPassword.value
if (newPassword.value !== confirmNewPassword.value) { ? apiFetch(`/api/managers/managers/${editingManager.value.id}/password`, {
throw new Error('Passwords do not match.');
}
await apiFetch(`/api/managers/managers/${editingManager.value.id}/password`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ newPassword: newPassword.value }), body: JSON.stringify({ newPassword: newPassword.value }),
}); })
: Promise.resolve()
]);
const [permissionsResponse] = results;
console.log('API responses:', results);
// Check for explicit error responses
if (permissionsResponse && permissionsResponse.error) {
console.log('Permissions update failed:', permissionsResponse.error);
throw new Error(permissionsResponse.error || 'Failed to update permissions.');
} }
if (newPassword.value && newPassword.value !== confirmNewPassword.value) {
console.log('Password mismatch detected');
throw new Error('Passwords do not match.');
}
// Refresh data
await fetchManagers(currentPage.value); await fetchManagers(currentPage.value);
closeSettingsModal(); toast.showToast($t('managerSettingsSaved'), 'success');
toast.showToast('Manager settings saved successfully!', 'success');
// Wait for toast to appear before closing modal
await new Promise(resolve => setTimeout(resolve, 1000));
// Reset form and close modal - ensure all state is cleared
editingManager.value = null;
newPassword.value = '';
confirmNewPassword.value = '';
passwordErrorMessage.value = '';
passwordSuccessMessage.value = '';
showDeleteConfirm.value = false;
isSettingsModalVisible.value = false;
} catch (err) { } catch (err) {
console.error('Error in saveManagerSettings:', err);
passwordErrorMessage.value = err.message || 'Failed to save settings.'; passwordErrorMessage.value = err.message || 'Failed to save settings.';
toast.showToast(err.message || 'Failed to save settings.', 'error'); toast.showToast(err.message || 'Failed to save settings.', 'error');
} finally { } finally {
@@ -413,10 +441,9 @@ const closeAddManagerModal = () => {
await fetchManagers(1); // Auto-refetch the manager list after adding await fetchManagers(1); // Auto-refetch the manager list after adding
closeAddManagerModal(); // Auto-close the modal after successful addition closeAddManagerModal(); // Auto-close the modal after successful addition
toast.showToast('Manager added successfully!', 'success'); toast.showToast($t('managerAddedSuccess'), 'success');
} catch (err) { } catch (_err) {
const displayMessage = err.message || err.sqlMessage || 'Error adding manager'; toast.showToast($t('addManagerError'), 'error');
toast.showToast(displayMessage, 'error');
} finally { } finally {
addingManager.value = false; addingManager.value = false;
} }
@@ -425,12 +452,12 @@ const closeAddManagerModal = () => {
const deleteManager = async (id) => { const deleteManager = async (id) => {
try { try {
await apiFetch(`/api/managers/managers/${id}`, { method: 'DELETE' }); await apiFetch(`/api/managers/managers/${id}`, { method: 'DELETE' });
toast.showToast('Manager Deleted successfully.', 'success'); toast.showToast($t('managerDeletedSuccess'), 'success');
// Adjust page number if the last manager on a page was deleted // Adjust page number if the last manager on a page was deleted
fetchManagers(managers.value.length === 1 && currentPage.value > 1 ? currentPage.value - 1 : currentPage.value); fetchManagers(managers.value.length === 1 && currentPage.value > 1 ? currentPage.value - 1 : currentPage.value);
closeSettingsModal(); closeSettingsModal();
} catch (_err) { } catch (_err) {
toast.showToast('Failed to delete manager.', 'error'); toast.showToast($t('deleteManagerFailed'), 'error');
errorMessage.value = 'Failed to Delete manager.'; // This could also be removed if toast is sufficient errorMessage.value = 'Failed to Delete manager.'; // This could also be removed if toast is sufficient
} }
}; };
+8 -8
View File
@@ -340,9 +340,9 @@ const addWorker = async () => {
}); });
await fetchWorkers(1); await fetchWorkers(1);
newWorker.value = { fullName: '', username: '', password: '', department: '', position: '' }; newWorker.value = { fullName: '', username: '', password: '', department: '', position: '' };
toast.showToast('Worker added successfully', 'success'); toast.showToast($t('workerAdded'), 'success');
} catch (_err) { } catch (_err) {
toast.showToast(_err.message || 'Error adding user.', 'error'); toast.showToast(_err.message || $t('addUserError'), 'error');
} finally { } finally {
loading.value = false; loading.value = false;
} }
@@ -354,7 +354,7 @@ const deleteWorker = async (id) => {
if (!confirmed) return; if (!confirmed) return;
try { try {
await apiFetch(`/api/managers/workers/${id}`, { method: 'DELETE' }); await apiFetch(`/api/managers/workers/${id}`, { method: 'DELETE' });
toast.showToast('Worker soft-deleted successfully.', 'success'); toast.showToast($t('workerSoftDeleted'), 'success');
fetchWorkers(workers.value.length === 1 && currentPage.value > 1 ? currentPage.value - 1 : currentPage.value); fetchWorkers(workers.value.length === 1 && currentPage.value > 1 ? currentPage.value - 1 : currentPage.value);
} catch (_err) { } catch (_err) {
errorMessage.value = 'Failed to soft-delete worker.'; errorMessage.value = 'Failed to soft-delete worker.';
@@ -365,9 +365,9 @@ const clearDevice = async (workerId) => {
const toast = useToast(); const toast = useToast();
try { try {
await apiFetch(`/api/managers/workers/${workerId}/reset-device`, { method: 'PUT' }); await apiFetch(`/api/managers/workers/${workerId}/reset-device`, { method: 'PUT' });
toast.showToast('Worker device cleared successfully.', 'success'); toast.showToast($t('deviceCleared'), 'success');
} catch (_err) { } catch (_err) {
toast.showToast(_err.message || 'Failed to clear device.', 'error'); toast.showToast(_err.message || $t('clearDeviceFailed'), 'error');
} }
}; };
@@ -378,7 +378,7 @@ const saveWorkerSettings = async () => {
passwordSuccessMessage.value = ''; passwordSuccessMessage.value = '';
let passwordUpdated = false; let passwordUpdated = false;
let detailsUpdated = false; let detailsUpdated = false;
toast.showToast('Saving settings...', 'info'); toast.showToast($t('savingSettings'), 'info');
// Handle password change // Handle password change
if (newPassword.value || confirmNewPassword.value) { if (newPassword.value || confirmNewPassword.value) {
@@ -495,7 +495,7 @@ const toggleSelectAll = (event) => {
const exportWorkHours = async () => { const exportWorkHours = async () => {
const toast = useToast(); const toast = useToast();
exportLoading.value = true; exportLoading.value = true;
toast.showToast('Exporting records...', 'info'); toast.showToast($t('exportingRecords'), 'info');
const { startDate, endDate } = exportFilters.value; const { startDate, endDate } = exportFilters.value;
let workerIds = selectedWorkerIds.value.join(','); let workerIds = selectedWorkerIds.value.join(',');
@@ -516,7 +516,7 @@ const exportWorkHours = async () => {
a.remove(); a.remove();
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
} catch (_err) { } catch (_err) {
toast.showToast('Failed to export records.', 'error'); toast.showToast($t('exportRecordsFailed'), 'error');
} finally { } finally {
exportLoading.value = false; exportLoading.value = false;
} }
+1 -1
View File
@@ -2,7 +2,7 @@
<transition name="app-toast"> <transition name="app-toast">
<div <div
v-if="visible" v-if="visible"
class="fixed z-[9999] bottom-4 right-4 p-4 rounded-lg shadow-lg max-w-xs" class="fixed z-9999 bottom-4 right-4 p-4 rounded-lg shadow-lg max-w-xs"
:class="{ :class="{
'bg-green-100 text-green-800 border border-green-200': type === 'success', 'bg-green-100 text-green-800 border border-green-200': type === 'success',
'bg-red-100 text-red-800 border border-red-200': type === 'error', 'bg-red-100 text-red-800 border border-red-200': type === 'error',
+1
View File
@@ -60,6 +60,7 @@ export function useToast() {
key: toast.id, key: toast.id,
message: toast.message, message: toast.message,
type: toast.type, type: toast.type,
class: "z-9999",
duration: toast.duration, duration: toast.duration,
persistent: !!toast.actions, persistent: !!toast.actions,
onClose: () => removeToast(toast.id) onClose: () => removeToast(toast.id)
+27 -1
View File
@@ -265,10 +265,36 @@
"managerSettings": "Manager Settings", "managerSettings": "Manager Settings",
"managerStatus": "Manager Status", "managerStatus": "Manager Status",
"confirmDeleteWorker": "Are you sure you want to delete this worker? This will soft-delete their account.", "confirmDeleteWorker": "Are you sure you want to delete this worker? This will soft-delete their account.",
"confirmClearDevice": "Are you sure you want to clear this device? The worker will need to re-login.",
"view_all": "View All", "view_all": "View All",
"edit_workers": "Edit Workers", "edit_workers": "Edit Workers",
"manage_resources": "Manage Resources", "manage_resources": "Manage Resources",
"manager_permissions": "Manager Permissions", "manager_permissions": "Manager Permissions",
"confirmDelete": "Are you sure you want to delete this?", "confirmDelete": "Are you sure you want to delete this?",
"confirm": "Confirm" "confirm": "Confirm",
"scheduleUpdateFailed": "Failed to update schedule. Please try again.",
"confirmApplyChanges": "Are you sure you want to apply these schedule changes?",
"scheduleUpdateSuccess": "Schedule updated successfully",
"fetchRecordsFailed": "Failed to fetch records",
"loadDetailsFailed": "Failed to load details",
"fetchQRCodesFailed": "Failed to fetch QR codes",
"generateQRCodeFailed": "Failed to generate QR code image",
"createQRCodeFailed": "Failed to create QR code",
"updateQRStatusFailed": "Failed to update QR code status",
"qrCodeDeleted": "QR code deleted successfully",
"deleteQRCodeFailed": "Failed to delete QR code",
"workerAdded": "Worker added successfully",
"addUserError": "Error adding user",
"workerSoftDeleted": "Worker soft-deleted successfully",
"deviceCleared": "Worker device cleared successfully",
"clearDeviceFailed": "Failed to clear device",
"savingSettings": "Saving settings...",
"exportingRecords": "Exporting records...",
"exportRecordsFailed": "Failed to export records",
"fetchManagersFailed": "Failed to fetch managers",
"managerSettingsSaved": "Manager settings saved successfully",
"saveSettingsFailed": "Failed to save settings",
"managerAdded": "Manager added successfully",
"managerDeleted": "Manager deleted successfully",
"deleteManagerFailed": "Failed to delete manager"
} }
+66 -5
View File
@@ -237,13 +237,13 @@
"error.criticalServer": "Ralat kritikal pada pelayan telah berlaku. Sila hubungi sokongan.", "error.criticalServer": "Ralat kritikal pada pelayan telah berlaku. Sila hubungi sokongan.",
"dangerZone": "Zon Bahaya", "dangerZone": "Zon Bahaya",
"clearDeviceDescription": "Ini akan memadam semua data pekerja dari peranti. Gunakan dengan berhati-hati.", "clearDeviceDescription": "Nyahpaut Akaun dengan Peranti.",
"settings": "Tetapan", "settings": "Tetapan",
"employeeSettings": "Tetapan Pekerja", "employeeSettings": "Tetapan Pekerja",
"accountSettings": "Tetapan Akaun", "accountSettings": "Tetapan Akaun",
"workerStatus": "Status Pekerja", "workerStatus": "Status Akaun",
"activeAccount": "Akaun Aktif", "activeAccount": "Benarkan Log Masuk",
"deleteDescription": "Tindakan ini tidak boleh diundur. Semua data akan dipadam secara kekal.", "deleteDescription": "Pengguna akan dipadam.",
"saveChanges": "Simpan Perubahan", "saveChanges": "Simpan Perubahan",
"confirmDeleteWorker": "Adakah anda pasti mahu memadam pekerja ini? Ini akan memadam akaun mereka secara lembut.", "confirmDeleteWorker": "Adakah anda pasti mahu memadam pekerja ini? Ini akan memadam akaun mereka secara lembut.",
"managerPermissions": "Pengurus", "managerPermissions": "Pengurus",
@@ -252,8 +252,69 @@
"loadingManagers": "Memuatkan pengurus...", "loadingManagers": "Memuatkan pengurus...",
"managerSettings": "Tetapan Pengurus", "managerSettings": "Tetapan Pengurus",
"managerStatus": "Status Pengurus", "managerStatus": "Status Pengurus",
"confirmClearDevice": "Adakah anda pasti mahu mengosongkan peranti ini? Pekerja perlu log masuk semula.",
"view_all": "Lihat Semua", "view_all": "Lihat Semua",
"edit_workers": "Sunting Pekerja", "edit_workers": "Sunting Pekerja",
"manage_resources": "Urus Sumber", "manage_resources": "Urus Sumber",
"manager_permissions": "Kebenaran Pengurus" "manager_permissions": "Kebenaran Pengurus",
"confirmDelete": "Adakah anda pasti mahu memadam ini?",
"confirm": "Sahkan",
"can_view_workers": "Lihat Pekerja",
"can_edit_workers": "Urus Pekerja",
"can_view_alerts": "Lihat Amaran",
"can_view_geofences": "Lihat Geofences",
"can_manage_geofences": "Urus Geofences",
"can_view_qrcodes": "Lihat Kod QR",
"can_manage_qrcodes": "Urus Kod QR",
"can_view_reports": "Lihat Laporan",
"can_manage_killswitch": "Urus Jadual",
"can_manage_permissions": "Urus Kebenaran",
"can_edit_managers": "Sunting Pengurus",
"can_delete_managers": "Padam Pengurus",
"Worker added successfully": "Pekerja berjaya ditambah",
"Worker soft-deleted successfully.": "Pekerja berjaya dipadam (soft delete)",
"Worker device cleared successfully.": "Peranti pekerja berjaya dikosongkan",
"Manager settings saved successfully!": "Tetapan pengurus berjaya disimpan!",
"Manager added successfully!": "Pengurus berjaya ditambah!",
"Manager Deleted successfully.": "Pengurus berjaya dipadam.",
"QR code deleted successfully": "Kod QR berjaya dipadam",
"Failed to fetch records.": "Gagal mengambil rekod.",
"Failed to load details.": "Gagal memuatkan butiran.",
"Failed to fetch QR codes": "Gagal mengambil kod QR",
"Failed to generate QR code image": "Gagal menjana imej kod QR",
"Failed to create QR code": "Gagal mencipta kod QR",
"Failed to update QR code status": "Gagal mengemas kini status kod QR",
"Failed to delete QR code": "Gagal memadam kod QR",
"Failed to export records.": "Gagal mengeksport rekod.",
"Failed to fetch managers.": "Gagal mengambil senarai pengurus.",
"Failed to delete manager.": "Gagal memadam pengurus.",
"Saving settings...": "Menyimpan tetapan...",
"Exporting records...": "Mengeksport rekod...",
"scheduleUpdateFailed": "Gagal mengemas kini jadual. Sila cuba lagi.",
"confirmApplyChanges": "Adakah anda pasti mahu mengguna pakai perubahan jadual ini?",
"scheduleUpdateSuccess": "Jadual berjaya dikemas kini",
"fetchRecordsFailed": "Gagal mengambil rekod",
"loadDetailsFailed": "Gagal memuatkan butiran",
"fetchQRCodesFailed": "Gagal mengambil kod QR",
"generateQRCodeFailed": "Gagal menjana imej kod QR",
"createQRCodeFailed": "Gagal mencipta kod QR",
"updateQRStatusFailed": "Gagal mengemas kini status kod QR",
"qrCodeDeleted": "Kod QR berjaya dipadam",
"deleteQRCodeFailed": "Gagal memadam kod QR",
"workerAdded": "Pekerja berjaya ditambah",
"addUserError": "Ralat menambah pengguna",
"workerSoftDeleted": "Pekerja berjaya dipadam (soft delete)",
"deviceCleared": "Peranti pekerja berjaya dikosongkan",
"clearDeviceFailed": "Gagal mengosongkan peranti",
"savingSettings": "Menyimpan tetapan...",
"exportingRecords": "Mengeksport rekod...",
"exportRecordsFailed": "Gagal mengeksport rekod",
"fetchManagersFailed": "Gagal mengambil senarai pengurus",
"managerSettingsSaved": "Tetapan pengurus berjaya disimpan",
"saveSettingsFailed": "Gagal menyimpan tetapan",
"managerAdded": "Pengurus berjaya ditambah",
"managerDeleted": "Pengurus berjaya dipadam",
"deleteManagerFailed": "Gagal memadam pengurus"
} }