feat: rename bunch of files

This commit is contained in:
sudomarcma
2025-07-23 13:45:16 +08:00
parent ffe4c7228d
commit a7ba1bbda2
20 changed files with 388 additions and 1218 deletions
+21 -980
View File
File diff suppressed because it is too large Load Diff
-2
View File
@@ -13,8 +13,6 @@
"format": "prettier --write src/"
},
"dependencies": {
"@capacitor/cli": "^7.4.0",
"@capacitor/core": "^7.4.0",
"@heroicons/vue": "^2.2.0",
"@turf/turf": "^7.2.0",
"bcrypt": "^6.0.0",
+4 -4
View File
@@ -173,7 +173,7 @@ const displayGeofencesOnMap = () => {
});
};
const saveGeofence = async () => { // eslint-disable-line no-unused-vars
const saveGeofence = async () => {
if (!canSave.value) return;
try {
const newFence = await apiFetch('/api/managers/geofences', {
@@ -190,7 +190,7 @@ const saveGeofence = async () => { // eslint-disable-line no-unused-vars
}
};
const deleteGeofence = async (id) => { // eslint-disable-line no-unused-vars
const deleteGeofence = async (id) => {
const confirmed = await toast.showConfirm($t('confirmDeleteGeofence'))
if (!confirmed) return;
@@ -208,7 +208,7 @@ const deleteGeofence = async (id) => { // eslint-disable-line no-unused-vars
}
};
const toggleGeofenceStatus = async (fence) => { // eslint-disable-line no-unused-vars
const toggleGeofenceStatus = async (fence) => {
try {
const updatedFence = await apiFetch(`/api/managers/geofences/${fence.id}`, {
method: 'PUT',
@@ -228,7 +228,7 @@ const toggleGeofenceStatus = async (fence) => { // eslint-disable-line no-unused
}
};
const viewGeofenceOnMap = (fence) => { // eslint-disable-line no-unused-vars
const viewGeofenceOnMap = (fence) => {
if (fenceLayers[fence.id]) {
map.fitBounds(fenceLayers[fence.id].getBounds(), { padding: [50, 50] });
fenceLayers[fence.id].openPopup();
+12 -12
View File
@@ -79,14 +79,14 @@ const originalEnabledDates = ref(new Set());
const datesToEnable = ref(new Set());
const datesToDisable = ref(new Set());
const hasPendingChanges = computed(() => datesToEnable.value.size > 0 || datesToDisable.value.size > 0); // eslint-disable-line no-unused-vars
const sortedEnableList = computed(() => Array.from(datesToEnable.value).sort()); // eslint-disable-line no-unused-vars
const sortedDisableList = computed(() => Array.from(datesToDisable.value).sort()); // eslint-disable-line no-unused-vars
const hasPendingChanges = computed(() => datesToEnable.value.size > 0 || datesToDisable.value.size > 0);
const sortedEnableList = computed(() => Array.from(datesToEnable.value).sort());
const sortedDisableList = computed(() => Array.from(datesToDisable.value).sort());
const monthYear = computed(() => viewDate.value.toLocaleString('default', { month: 'long', year: 'numeric' })); // eslint-disable-line no-unused-vars
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; // eslint-disable-line no-unused-vars
const monthYear = computed(() => viewDate.value.toLocaleString('default', { month: 'long', year: 'numeric' }));
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const calendarGrid = computed(() => { // eslint-disable-line no-unused-vars
const calendarGrid = computed(() => {
const year = viewDate.value.getFullYear();
const month = viewDate.value.getMonth();
const firstDayOfMonth = new Date(year, month, 1).getDay();
@@ -105,7 +105,7 @@ const calendarGrid = computed(() => { // eslint-disable-line no-unused-vars
return grid;
});
const getDayClasses = (day) => { // eslint-disable-line no-unused-vars
const getDayClasses = (day) => {
if (!day.isCurrentMonth) return 'h-20';
const dateStr = day.id;
@@ -136,7 +136,7 @@ const getDayClasses = (day) => { // eslint-disable-line no-unused-vars
return classes;
};
function onDayClick(day) { // eslint-disable-line no-unused-vars
function onDayClick(day) {
const dateStr = day.id;
const isOriginallyEnabled = originalEnabledDates.value.has(dateStr);
@@ -151,7 +151,7 @@ function onDayClick(day) { // eslint-disable-line no-unused-vars
}
}
async function applyChanges() { // eslint-disable-line no-unused-vars
async function applyChanges() {
const confirmed = await toast.showConfirm($t('confirmApplyChanges'))
if (!confirmed) return;
@@ -177,10 +177,10 @@ function discardChanges() {
datesToDisable.value.clear();
}
const prevMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() - 1)); // eslint-disable-line no-unused-vars
const nextMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() + 1)); // eslint-disable-line no-unused-vars
const prevMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() - 1));
const nextMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() + 1));
const formatDate = (dateStr) => new Date(dateStr + 'T00:00:00').toLocaleDateString(undefined, { // eslint-disable-line no-unused-vars
const formatDate = (dateStr) => new Date(dateStr + 'T00:00:00').toLocaleDateString(undefined, {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
});
+42
View File
@@ -245,12 +245,25 @@ import { useToast } from '@/composables/useToast';
import { useRouter } from 'vue-router';
import { permissions } from '@/stores/permissions.js';
import { useI18n } from 'vue-i18n';
import { workerCache } from '@/utils/workerCache.js';
const { t: $t } = useI18n();
const router = useRouter();
const viewRecords = (workerId) => {
// Save current search state before navigating away
const searchState = {
searchQuery: searchQuery.value,
currentPage: currentPage.value,
pageSize: pageSize.value,
totalWorkers: totalWorkers.value,
workers: workers.value,
selectedWorkerIds: selectedWorkerIds.value,
exportFilters: exportFilters.value
};
sessionStorage.setItem('personnelSearchState', JSON.stringify(searchState));
router.push(`/manager/attendance/${workerId}`);
};
@@ -301,6 +314,14 @@ const fetchWorkers = async (page = currentPage.value) => {
const data = await apiFetch(`/api/managers/workers?search=${searchQuery.value}&page=${page}&limit=${pageSize.value}`);
workers.value = data.workers;
totalWorkers.value = data.totalCount;
// Cache worker data
if (data.workers && Array.isArray(data.workers)) {
data.workers.forEach(worker => {
workerCache.storeWorkerData(worker.id, worker);
});
}
// currentPage is already set to the requested page before fetch
} catch (_err) {
errorMessage.value = 'Failed to fetch workers.';
@@ -523,6 +544,27 @@ const exportWorkHours = async () => {
};
onMounted(() => {
// Check if there's saved search state
const savedSearchState = sessionStorage.getItem('personnelSearchState');
if (savedSearchState) {
try {
const searchState = JSON.parse(savedSearchState);
searchQuery.value = searchState.searchQuery || '';
currentPage.value = searchState.currentPage || 1;
pageSize.value = searchState.pageSize || 20;
totalWorkers.value = searchState.totalWorkers || 0;
workers.value = searchState.workers || [];
selectedWorkerIds.value = searchState.selectedWorkerIds || [];
exportFilters.value = searchState.exportFilters || { startDate: '', endDate: '' };
// Clear the saved search state after restoring it
sessionStorage.removeItem('personnelSearchState');
} catch (_e) {
// If there's an error parsing the saved state, fetch workers normally
fetchWorkers();
}
} else {
fetchWorkers();
}
});
</script>
+4 -4
View File
@@ -128,7 +128,7 @@ const fetchQrCodes = async () => {
}
}
const addQrCode = async () => { // eslint-disable-line no-unused-vars
const addQrCode = async () => {
if (!newQrName.value) return
try {
const newQr = await apiFetch('/api/managers/qr-codes', {
@@ -158,7 +158,7 @@ const addQrCode = async () => { // eslint-disable-line no-unused-vars
}
}
const toggleQrStatus = async (qr) => { // eslint-disable-line no-unused-vars
const toggleQrStatus = async (qr) => {
try {
await apiFetch(`/api/managers/qr-codes/${qr.id}`, {
method: 'PUT',
@@ -175,7 +175,7 @@ const toggleQrStatus = async (qr) => { // eslint-disable-line no-unused-vars
}
}
const deleteQrCode = async (id) => { // eslint-disable-line no-unused-vars
const deleteQrCode = async (id) => {
const confirmed = await toast.showConfirm($t('deleteQrConfirm'))
if (!confirmed) return
@@ -191,7 +191,7 @@ const deleteQrCode = async (id) => { // eslint-disable-line no-unused-vars
}
}
const downloadQrCode = async (qr) => { // eslint-disable-line no-unused-vars
const downloadQrCode = async (qr) => {
try {
const dataUrl = await QRCode.toDataURL(qr.id, {
type: 'image/png',
+5
View File
@@ -13,6 +13,11 @@
"english": "English",
"malay": "Behasa Melayu",
"setting": "Setting",
"settings": "Settings",
"appInformation": "App Information",
"version": "Version",
"platform": "Platform",
"web": "Web",
"yourStatus": "Your Status",
"clockedIn": "Clocked In",
+5
View File
@@ -13,6 +13,11 @@
"english": "English",
"malay": "Bahasa Melayu",
"setting": "Tetapan",
"settings": "Tetapan",
"appInformation": "Maklumat Aplikasi",
"version": "Versi",
"platform": "Platform",
"web": "Web",
"yourStatus": "Status Anda",
"clockedIn": "Sudah Masuk",
+14 -21
View File
@@ -1,58 +1,51 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import LoginView from '../views/LoginView.vue'
import WorkerDashboardView from '../views/WorkerDashboardView.vue'
import ManagerDashboardView from '../views/ManagerDashboardView.vue'
import WorkerHistoryView from '../views/WorkerHistoryView.vue'
import AttendanceRecordView from '../views/AttendanceRecordView.vue'
import Login from '../views/Login.vue'
import WorkerDashboard from '../views/WorkerDashboard.vue'
import ManagerDashboard from '../views/ManagerDashboard.vue'
import WorkerHistory from '../views/WorkerHistory.vue'
import ManagerAttendanceRecord from '../views/ManagerAttendanceRecord.vue'
import ManagerPermissions from '../components/ManagerPermissions.vue'
import ChangePasswordView from '../views/ChangePasswordView.vue'
import SettingsView from '../views/SettingsView.vue'
import NativeServicesStatus from '../views/NativeServicesStatus.vue'
import WorkerChangePassword from '../views/WorkerChangePassword.vue'
import WorkerSettings from '../views/WorkerSettings.vue'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/', name: 'login', component: LoginView },
{ path: '/', name: 'login', component: Login },
{
path: '/worker/dashboard',
name: 'worker-dashboard',
component: WorkerDashboardView,
component: WorkerDashboard,
meta: { requiresAuth: true, role: 'worker' },
},
{
path: '/worker/history',
name: 'worker-history',
component: WorkerHistoryView,
component: WorkerHistory,
meta: { requiresAuth: true, role: 'worker' },
},
{
path: '/worker/change-password',
name: 'worker-change-password',
component: ChangePasswordView,
component: WorkerChangePassword,
meta: { requiresAuth: true, role: 'worker' },
},
{
path: '/worker/settings',
name: 'worker-settings',
component: SettingsView,
meta: { requiresAuth: true, role: 'worker' },
},
{
path: '/worker/services-status',
name: 'worker-services-status',
component: NativeServicesStatus,
component: WorkerSettings,
meta: { requiresAuth: true, role: 'worker' },
},
{
path: '/manager/dashboard',
name: 'manager-dashboard',
component: ManagerDashboardView,
component: ManagerDashboard,
meta: { requiresAuth: true, role: 'manager' },
},
{
path: '/manager/attendance/:workerId',
name: 'manager-attendance-record',
component: AttendanceRecordView,
component: ManagerAttendanceRecord,
meta: { requiresAuth: true, role: 'manager' },
},
{
+50
View File
@@ -0,0 +1,50 @@
// Worker data caching utility
const CACHE_KEY_PREFIX = 'worker_data_';
const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds
export const workerCache = {
// Store worker data in session storage with timestamp
storeWorkerData(workerId, data) {
const cacheEntry = {
data: data,
timestamp: Date.now()
};
sessionStorage.setItem(`${CACHE_KEY_PREFIX}${workerId}`, JSON.stringify(cacheEntry));
},
// Retrieve worker data from session storage if not expired
getWorkerData(workerId) {
const cached = sessionStorage.getItem(`${CACHE_KEY_PREFIX}${workerId}`);
if (!cached) return null;
try {
const cacheEntry = JSON.parse(cached);
// Check if cache is still valid
if (Date.now() - cacheEntry.timestamp < CACHE_DURATION) {
return cacheEntry.data;
} else {
// Remove expired cache
sessionStorage.removeItem(`${CACHE_KEY_PREFIX}${workerId}`);
return null;
}
} catch (_e) {
// Remove invalid cache entry
sessionStorage.removeItem(`${CACHE_KEY_PREFIX}${workerId}`);
return null;
}
},
// Clear worker data cache
clearWorkerData(workerId) {
sessionStorage.removeItem(`${CACHE_KEY_PREFIX}${workerId}`);
},
// Clear all worker data cache
clearAllWorkerData() {
Object.keys(sessionStorage).forEach(key => {
if (key.startsWith(CACHE_KEY_PREFIX)) {
sessionStorage.removeItem(key);
}
});
}
};
@@ -81,6 +81,8 @@ const handleLogin = async () => {
const decodedToken = JSON.parse(atob(data.token.split('.')[1]))
sessionStorage.setItem('userId', decodedToken.userId)
sessionStorage.setItem('userRole', decodedToken.role)
// Store username in session storage
sessionStorage.setItem('username', username.value)
if (decodedToken.role === 'worker') {
router.push('/worker/dashboard')
@@ -2,9 +2,9 @@
<div class="max-w-4xl mx-auto px-4 py-8">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="mb-6">
<router-link to="/manager/dashboard" class="text-blue-600 hover:text-blue-800 font-medium">
<button @click="goBack" class="text-blue-600 hover:text-blue-800 font-medium">
{{ $t('backToDashboard') }}
</router-link>
</button>
<h2 class="text-2xl font-bold text-gray-800 dark:text-white mt-2">
{{ $t('attendanceLogFor') }} {{ workerName }}
</h2>
@@ -126,6 +126,7 @@ import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { apiFetch } from '@/api.js'
import { workerCache } from '@/utils/workerCache.js'
const { t } = useI18n()
@@ -156,6 +157,11 @@ const filters = ref({
const exportLoading = ref(false);
const goBack = () => {
// Navigate back to the manager dashboard (PersonnelManagement component)
window.history.back();
};
const fetchRecords = async () => {
let url = `/api/managers/attendance-records?workerIds=${workerId}`
if (filters.value.startDate && filters.value.endDate) {
@@ -168,7 +174,15 @@ const fetchRecords = async () => {
if (data && Array.isArray(data)) {
records.value = data
if (!workerName.value && data.length > 0) {
workerName.value = data[0].full_name
// Check if worker data is cached
const cachedWorkerData = workerCache.getWorkerData(workerId);
if (cachedWorkerData) {
workerName.value = cachedWorkerData.full_name;
} else {
workerName.value = data[0].full_name;
// Cache the worker data for future use
workerCache.storeWorkerData(workerId, { full_name: data[0].full_name });
}
}
} else {
records.value = []
-76
View File
@@ -1,76 +0,0 @@
<template>
<div class="mobile-viewport bg-gray-100 flex flex-col overflow-hidden">
<!-- Fixed Header -->
<header class="fixed left-0 right-0 top-0 z-50 bg-blue-600 text-white shadow-lg">
<div class="px-4 py-6" style="padding-top: calc(var(--safe-area-inset-top) + 1.5rem);">
<div class="flex items-center">
<button @click="goBack" class="mr-4 p-2 hover:bg-blue-700 rounded-lg transition-colors">
<ArrowLeftIcon class="w-6 h-6" />
</button>
<h1 class="text-3xl font-bold">{{ $t('servicesStatus') }}</h1>
</div>
</div>
</header>
<!-- Scrollable Main Content -->
<main class="px-4 py-8 space-y-8 mt-[calc(100px+env(safe-area-inset-top))]">
<div class="bg-white rounded-2xl shadow-lg p-6 mt-8">
<div class="space-y-4 text-center">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CogIcon class="w-8 h-8 text-blue-600" />
</div>
<h3 class="text-lg font-semibold text-gray-900">{{ $t('webVersionInfo') }}</h3>
<p class="text-gray-600">{{ $t('webVersionDescription') }}</p>
<div class="mt-6 p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-blue-700">
<strong>{{ $t('note') }}:</strong> {{ $t('webServicesNote') }}
</p>
</div>
</div>
<!-- Basic Web Status -->
<div class="mt-6 space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">{{ $t('browserSupport') }}</span>
<span class="text-green-500 text-sm">{{ $t('supported') }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">{{ $t('locationAccess') }}</span>
<span class="text-yellow-500 text-sm">{{ $t('browserBased') }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600">{{ $t('deviceId') }}</span>
<span class="text-xs text-gray-500 font-mono">{{ $t('webSession') }}</span>
</div>
</div>
<div class="mt-6 flex justify-end">
<button
@click="goBack"
class="bg-blue-600 text-white px-6 py-3 rounded-xl hover:bg-blue-700 font-medium"
>
{{ $t('back') }}
</button>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ArrowLeftIcon, CogIcon } from '@heroicons/vue/24/outline'
const { t: _t } = useI18n()
const router = useRouter()
const goBack = () => {
router.back()
}
</script>
<style scoped>
/* Component-specific styles if needed */
</style>
@@ -1,44 +1,45 @@
<template>
<div class="mobile-viewport bg-gray-100">
<!-- Header -->
<header class="fixed left-0 right-0 top-0 z-50 bg-blue-600 text-white shadow-lg">
<div class="px-4 py-6" style="padding-top: calc(var(--safe-area-inset-top) + 1.5rem);">
<div class="flex items-center">
<button @click="goBack" class="mr-4 p-2 hover:bg-blue-700 rounded-lg transition-colors">
<ArrowLeftIcon class="w-6 h-6" />
<div class="mobile-viewport bg-gray-100 dark:bg-gray-900 min-h-screen">
<!-- Back Button -->
<div class="fixed bottom-4 right-4 z-50">
<button
@click="goBack"
class="bg-white dark:bg-gray-800 shadow-lg rounded-full p-3 hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Return to Dashboard"
>
<svg class="w-6 h-6 text-gray-700 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</button>
<h1 class="text-3xl font-bold">{{ $t('changePasswordTitle') }}</h1>
</div>
</div>
</header>
<main class="main-with-fixed-header-and-nav px-4 py-8">
<div class="bg-white rounded-2xl shadow-lg p-8 w-full max-w-lg mx-auto mt-8">
<main class="px-4 py-8">
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-8 w-full max-w-lg mx-auto mt-8">
<form @submit.prevent="handleChangePassword" class="space-y-6">
<!-- Success Message -->
<div v-if="successMessage" class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded-lg">
<div v-if="successMessage" class="bg-green-100 dark:bg-green-900 border-l-4 border-green-500 text-green-700 dark:text-green-300 p-4 rounded-lg">
{{ $t(successMessage) }}
</div>
<!-- Error Message -->
<div v-if="errorMessage" class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg">
<div v-if="errorMessage" class="bg-red-100 dark:bg-red-900 border-l-4 border-red-500 text-red-700 dark:text-red-300 p-4 rounded-lg">
{{ $t(errorMessage) }}
</div>
<!-- Form Fields -->
<div>
<label for="currentPassword" class="block text-sm font-medium text-gray-700 mb-2">{{ $t('currentPassword') }}</label>
<label for="currentPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ $t('currentPassword') }}</label>
<input type="password" id="currentPassword" v-model="passwords.currentPassword" required
class="w-full px-4 py-3 text-gray-700 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
class="w-full px-4 py-3 text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<div>
<label for="newPassword" class="block text-sm font-medium text-gray-700 mb-2">{{ $t('newPassword') }}</label>
<label for="newPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ $t('newPassword') }}</label>
<input type="password" id="newPassword" v-model="passwords.newPassword" required
class="w-full px-4 py-3 text-gray-700 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
class="w-full px-4 py-3 text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<div>
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 mb-2">{{ $t('confirmNewPassword') }}</label>
<label for="confirmPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">{{ $t('confirmNewPassword') }}</label>
<input type="password" id="confirmPassword" v-model="passwords.confirmPassword" required
class="w-full px-4 py-3 text-gray-700 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
class="w-full px-4 py-3 text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
</div>
<!-- Submit Button -->
@@ -56,7 +57,6 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { apiFetch } from '@/api.js'
import { ArrowLeftIcon } from '@heroicons/vue/24/outline'
const router = useRouter()
@@ -14,8 +14,11 @@
</button>
</div>
<main class="px-4 py-8 space-y-8 flex flex-col justify-center min-h-screen">
<main class="px-4 pt-2 pb-8 space-y-8 flex flex-col justify-center">
<!-- Worker Name Display -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 text-center">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-2">{{ workerName }}</h1>
<p class="text-gray-600 dark:text-gray-400 mb-4">{{ username }}</p>
<div class="flex items-center justify-center gap-4 mb-4">
<component :is="isClockedIn ? CheckCircleIcon : ClockIcon"
:class="['w-16 h-16', isClockedIn ? 'text-green-500 dark:text-green-400' : 'text-red-500 dark:text-red-400']" />
@@ -89,6 +92,7 @@ import { useI18n } from 'vue-i18n'
import { CheckCircleIcon, ClockIcon, CameraIcon } from '@heroicons/vue/24/outline'
import { Html5Qrcode } from 'html5-qrcode'
import { apiFetch } from '@/api.js'
import { workerCache } from '@/utils/workerCache.js'
// Removed Capacitor Geolocation for web migration
const { t } = useI18n()
@@ -100,6 +104,7 @@ const router = useRouter()
const isClockedIn = ref(false)
const isScannerActive = ref(false)
const workerName = ref('')
const username = ref('')
// Unified overlay refs
const showOverlay = ref(false)
@@ -141,9 +146,29 @@ const dismissOverlay = () => {
};
const fetchWorkerDetails = async () => {
// Check if worker data is cached
const cachedData = workerCache.getWorkerData(userId);
if (cachedData) {
workerName.value = cachedData.full_name;
return;
}
// Check if worker name is already in session storage (fallback)
const storedWorkerName = sessionStorage.getItem('workerName');
if (storedWorkerName) {
workerName.value = storedWorkerName;
return;
}
try {
const data = await apiFetch(`/api/workers/${userId}`)
if (data) workerName.value = data.full_name
if (data) {
workerName.value = data.full_name
// Cache the worker data
workerCache.storeWorkerData(userId, data);
// Also store in session storage for compatibility
sessionStorage.setItem('workerName', data.full_name);
}
} catch (err) {
triggerOverlay(getLocalizedErrorMessage(err), 'error');
}
@@ -182,6 +207,16 @@ onMounted(async () => {
router.push('/')
return
}
// Get username from session storage
const storedUsername = sessionStorage.getItem('username');
if (storedUsername) {
username.value = storedUsername;
} else {
// Fallback to placeholder if not found
username.value = 'username'
}
fetchWorkerDetails()
fetchCurrentStatus()
})
+81
View File
@@ -0,0 +1,81 @@
<template>
<div class="mobile-viewport bg-gray-100 dark:bg-gray-900 min-h-screen">
<!-- Back Button -->
<div class="fixed bottom-4 right-4 z-50">
<button
@click="goBack"
class="bg-white dark:bg-gray-800 shadow-lg rounded-full p-3 hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Return to Dashboard"
>
<svg class="w-6 h-6 text-gray-700 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</button>
</div>
<main class="px-4 py-8">
<!-- Empty State -->
<div v-if="!clockHistory.length" class="text-center py-16 mt-8">
<ChartBarIcon class="w-16 h-16 text-gray-400 dark:text-gray-500 mx-auto mb-4" />
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-300">{{ $t('noClockHistory') }}</h2>
<p class="text-gray-500 dark:text-gray-400 mt-2">{{ $t('clockHistoryEmptyState') }}</p>
</div>
<!-- History List -->
<div v-else class="space-y-4 mt-8 mb-10">
<div v-for="event in clockHistory" :key="event.id"
class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-5 flex items-center space-x-4">
<div class="w-12 h-12 rounded-full flex items-center justify-center"
:class="event.event_type === 'clock_in' ? 'bg-green-100 dark:bg-green-900/50' : 'bg-red-100 dark:bg-red-900/50'">
<component :is="event.event_type === 'clock_in' ? ArrowDownCircleIcon : ArrowUpCircleIcon"
:class="['w-8 h-8', event.event_type === 'clock_in' ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400']" />
</div>
<div class="flex-grow">
<div class="font-bold text-lg text-gray-900 dark:text-gray-100">{{ $t(event.event_type) }}</div>
<div class="text-sm text-gray-600 dark:text-gray-400">{{ event.qrCodeUsedName }}</div>
</div>
<div class="text-right">
<div class="font-medium text-gray-800 dark:text-gray-200">{{ new Date(event.timestamp).toLocaleDateString() }}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">{{ new Date(event.timestamp).toLocaleTimeString() }}</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ChartBarIcon, ArrowDownCircleIcon, ArrowUpCircleIcon } from '@heroicons/vue/24/outline'
import { apiFetch } from '@/api.js'
const { t } = useI18n()
const router = useRouter()
const clockHistory = ref([])
const userId = sessionStorage.getItem('userId')
onMounted(async () => {
if (!userId) {
router.push('/')
return
}
try {
const data = await apiFetch(`/api/worker/clock-history/${userId}`)
if (data) {
clockHistory.value = data.filter(event => event.event_type !== 'failed');
}
} catch (error) {
console.error(t('clockHistoryFetchFail'), error)
}
})
const goBack = () => {
router.back()
}
</script>
<style scoped>
/* All styles are now handled by Tailwind CSS classes in the template. */
</style>
-80
View File
@@ -1,80 +0,0 @@
<template>
<div class="mobile-viewport bg-gray-100">
<!-- Header -->
<header class="fixed left-0 right-0 top-0 z-50 bg-blue-600 text-white shadow-lg">
<div class="px-4 py-6" style="padding-top: calc(var(--safe-area-inset-top) + 1.5rem);">
<div class="flex items-center">
<button @click="goBack" class="mr-4 p-2 hover:bg-blue-700 rounded-lg transition-colors">
<ArrowLeftIcon class="w-6 h-6" />
</button>
<h1 class="text-3xl font-bold">{{ $t('myClockHistory') }}</h1>
</div>
</div>
</header>
<main class="main-with-fixed-header-and-nav px-4 py-8">
<!-- Empty State -->
<div v-if="!clockHistory.length" class="text-center py-16 mt-8">
<ChartBarIcon class="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h2 class="text-2xl font-semibold text-gray-700">{{ $t('noClockHistory') }}</h2>
<p class="text-gray-500 mt-2">{{ $t('clockHistoryEmptyState') }}</p>
</div>
<!-- History List -->
<div v-else class="space-y-4 mt-8 mb-10">
<div v-for="event in clockHistory" :key="event.id"
class="bg-white rounded-2xl shadow-lg p-5 flex items-center space-x-4">
<div class="w-12 h-12 rounded-full flex items-center justify-center"
:class="event.event_type === 'clock_in' ? 'bg-green-100' : 'bg-red-100'">
<component :is="event.event_type === 'clock_in' ? ArrowDownCircleIcon : ArrowUpCircleIcon"
:class="['w-8 h-8', event.event_type === 'clock_in' ? 'text-green-600' : 'text-red-600']" />
</div>
<div class="flex-grow">
<div class="font-bold text-lg text-gray-900">{{ $t(event.event_type) }}</div>
<div class="text-sm text-gray-600">{{ event.qrCodeUsedName }}</div>
</div>
<div class="text-right">
<div class="font-medium text-gray-800">{{ new Date(event.timestamp).toLocaleDateString() }}</div>
<div class="text-sm text-gray-500">{{ new Date(event.timestamp).toLocaleTimeString() }}</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ChartBarIcon, ArrowDownCircleIcon, ArrowUpCircleIcon, ArrowLeftIcon } from '@heroicons/vue/24/outline'
import { apiFetch } from '@/api.js'
const { t } = useI18n()
const router = useRouter()
const clockHistory = ref([])
const userId = sessionStorage.getItem('userId')
onMounted(async () => {
if (!userId) {
router.push('/')
return
}
try {
const data = await apiFetch(`/api/worker/clock-history/${userId}`)
if (data) {
clockHistory.value = data.filter(event => event.event_type !== 'failed');
}
} catch (error) {
console.error(t('clockHistoryFetchFail'), error)
}
})
const goBack = () => {
router.back()
}
</script>
<style scoped>
/* All styles are now handled by Tailwind CSS classes in the template. */
</style>
@@ -1,15 +1,34 @@
<template>
<div class="mobile-viewport bg-gray-100 dark:bg-gray-900 min-h-screen p-4">
<!-- Back Button -->
<div class="mobile-viewport bg-gray-100 dark:bg-gray-900 min-h-screen">
<!-- Return Button (same position as settings button in dashboard) -->
<div class="fixed bottom-4 right-4 z-50">
<button @click="goBack" class="p-3 bg-white dark:bg-gray-700 rounded-full shadow-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
<ArrowLeftIcon class="w-6 h-6 text-gray-800 dark:text-gray-200" />
<button
@click="goBack"
class="bg-white dark:bg-gray-800 shadow-lg rounded-full p-3 hover:shadow-xl transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
aria-label="Return to Dashboard"
>
<svg class="w-6 h-6 text-gray-700 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</button>
</div>
<!-- Scrollable Main Content -->
<main class="space-y-4">
<!-- Menu Items -->
<main class="px-4 py-4 space-y-6 mt-2 mb-8">
<!-- Profile Section -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<div class="flex items-center space-x-4">
<div class="w-16 h-16 rounded-full bg-blue-100 dark:bg-blue-900/50 flex items-center justify-center">
<UserIcon class="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100">{{ workerName }}</h2>
<p class="text-gray-600 dark:text-gray-400">{{ username }}</p>
</div>
</div>
</div>
<!-- Settings Menu -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
<!-- Clock History -->
<router-link to="/worker/history" class="flex items-center p-5 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
@@ -50,6 +69,21 @@
</div>
</div>
<!-- App Information -->
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6">
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100 mb-4">{{ $t('appInformation') }}</h3>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ $t('version') }}</span>
<span class="font-medium text-gray-900 dark:text-gray-100">1.0.0</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600 dark:text-gray-400">{{ $t('platform') }}</span>
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $t('web') }}</span>
</div>
</div>
</div>
<!-- Logout Button -->
<button @click="logout" class="w-full flex items-center justify-center p-5 bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors">
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/50 rounded-xl flex items-center justify-center mr-5">
@@ -68,13 +102,16 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ChartBarIcon, LockClosedIcon, LanguageIcon, ArrowRightOnRectangleIcon, ChevronRightIcon, ArrowLeftIcon } from '@heroicons/vue/24/outline'
import { ChartBarIcon, LockClosedIcon, LanguageIcon, ArrowRightOnRectangleIcon, ChevronRightIcon, UserIcon } from '@heroicons/vue/24/outline'
// Removed authService dependency for web migration
const { locale } = useI18n()
const router = useRouter()
const currentLang = ref(locale.value)
const workerName = ref('')
const workerId = ref('')
const username = ref('')
const changeLang = () => {
locale.value = currentLang.value
@@ -87,6 +124,7 @@ const logout = async () => {
sessionStorage.removeItem('userId')
sessionStorage.removeItem('userRole')
sessionStorage.removeItem('token')
sessionStorage.removeItem('username')
router.push('/')
} catch (error) {
console.error('Logout error:', error)
@@ -104,5 +142,28 @@ onMounted(() => {
if (savedLang) {
currentLang.value = savedLang
}
// Get worker info from session storage
const userId = sessionStorage.getItem('userId')
if (userId) {
workerId.value = userId
// Get worker name from session storage
const storedWorkerName = sessionStorage.getItem('workerName');
if (storedWorkerName) {
workerName.value = storedWorkerName;
} else {
// Fallback to placeholder if not found
workerName.value = 'Worker'
}
// Get username from session storage
const storedUsername = sessionStorage.getItem('username');
if (storedUsername) {
username.value = storedUsername;
} else {
// Fallback to placeholder if not found
username.value = 'username'
}
}
})
</script>
-1
View File
@@ -1,5 +1,4 @@
import { fileURLToPath, URL } from 'node:url'
import fs from 'fs'
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'