feat: rename bunch of files
This commit is contained in:
Generated
+21
-980
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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' },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 = []
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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,5 +1,4 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import fs from 'fs'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
Reference in New Issue
Block a user