diff --git a/backend/middleware/withTzSession.js b/backend/middleware/withTzSession.js deleted file mode 100644 index cb71c66..0000000 --- a/backend/middleware/withTzSession.js +++ /dev/null @@ -1,21 +0,0 @@ -// backend/middleware/withTzSession.js -import { db } from '../server.js'; - -function tzToOffset(iana) { - // Stable offsets for two timezones - return iana === 'Asia/Jakarta' ? '+07:00' : '+08:00'; -} - -export async function withTzSession(req, res, next) { - const conn = await db.getConnection(); - req.db = conn; - try { - const iana = req.headers['x-user-timezone'] || 'Asia/Kuala_Lumpur'; - await conn.query('SET time_zone = ?', [tzToOffset(iana)]); - req.userTz = iana; - next(); - } catch (e) { - conn.release(); - res.status(500).json({ message: 'Failed to set session time zone' }); - } -} diff --git a/backend/workerRoutes.js b/backend/workerRoutes.js index 792fc31..1283106 100644 --- a/backend/workerRoutes.js +++ b/backend/workerRoutes.js @@ -101,10 +101,7 @@ export default function(db) { router.use(authenticateJWT); -// Definitive version with distance calculation and specific error messages - import { db } from './server.js'; // ensure this exists up top - -router.post('/clock', async (req, res) => { + router.post('/clock', async (req, res) => { // NEW: borrow a connection so we can set session time_zone const conn = await db.getConnection(); try { diff --git a/src/api.js b/src/api.js index 57f8b20..acd459b 100644 --- a/src/api.js +++ b/src/api.js @@ -1,11 +1,21 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +function getUserTimezone() { + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Kuala_Lumpur'; + } catch { + return 'Asia/Kuala_Lumpur'; + } +} + export async function apiFetch(endpoint, options = {}) { const token = sessionStorage.getItem('token'); const defaultHeaders = { 'ngrok-skip-browser-warning': 'true', 'Content-Type': 'application/json', + // Timezone header used by the backend to set session time_zone + 'X-User-Timezone': getUserTimezone(), ...options.headers, }; @@ -42,12 +52,18 @@ export async function apiFetch(endpoint, options = {}) { document.dispatchEvent(event); } // Use the 'details' from our backend error structure, or the message, or a default - throw new Error(errorData.details || errorData.message || `API call failed with status: ${response.status}`); + throw new Error( + errorData.details || + errorData.message || + `API call failed with status: ${response.status}` + ); } else { // If the server sends back HTML or plain text, use that as the error message. // This prevents the "Unexpected token '<'" error. const textError = await response.text(); - throw new Error(textError || `Server returned an unhandled error with status: ${response.status}`); + throw new Error( + textError || `Server returned an unhandled error with status: ${response.status}` + ); } } @@ -59,7 +75,7 @@ export async function apiFetch(endpoint, options = {}) { // Handle file downloads like CSV const disposition = response.headers.get('content-disposition'); if (disposition && disposition.includes('attachment')) { - return response.blob(); + return response.blob(); } return response.json(); diff --git a/src/components/KillSwitchManagement.vue b/src/components/KillSwitchManagement.vue index 6306d8c..784e506 100644 --- a/src/components/KillSwitchManagement.vue +++ b/src/components/KillSwitchManagement.vue @@ -6,11 +6,17 @@ {{ monthYear }}
{{ $t('activeAccount') }}
++ {{ $t('activeAccount') }} +
{{ $t('clearDeviceDescription') }}
++ {{ $t('clearDeviceDescription') }} +
{{ $t('confirmClearDevice') }}
++ {{ $t('confirmClearDevice') }} +
{{ $t('deleteDescription') }}
++ {{ $t('deleteDescription') }} +
{{ $t('confirmDelete') }}
++ {{ $t('confirmDelete') }} +
{{ passwordErrorMessage }}
-{{ passwordSuccessMessage }}
++ {{ passwordErrorMessage }} +
++ {{ passwordSuccessMessage }} +
| + | {{ $t('worker') }} - {{ sortDirection === 'asc' ? '↑' : '↓' }} + + {{ sortDirection === 'asc' ? '↑' : '↓' }} + | -+ | {{ $t('failedCount') }} - {{ sortDirection === 'asc' ? '↑' : '↓' }} + + {{ sortDirection === 'asc' ? '↑' : '↓' }} + + | ++ {{ $t('actions') }} | -{{ $t('actions') }} | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| {{ record.full_name }} | -{{ record.count }} | +||||||||||||||||
| + {{ record.full_name }} + | ++ {{ record.count }} + |
- |
@@ -53,9 +76,12 @@
|||||||||||||||
|
-
@@ -69,8 +95,11 @@
-
@@ -78,22 +107,37 @@
{{ detailModalTitle }}-+ {{ detailModalTitle }} ++
-
-
+
+
-
-
+ {{ $t('reason') }}
+
+
+ :placeholder="$t('enterBriefNote')"
+ />
-
-
-
+
+
-
-
+
+
- |
|||||||||||||||||
| + }" + > {{ record.event_type.replace('_', ' ') }} | - {{ new Date(record.timestamp).toLocaleString() }} + {{ formatLocalTimestamp(record.timestamp) }} + | ++ {{ record.qrCodeUsedName }} | -{{ record.qrCodeUsedName }} | - + {{ $t('showOnMap') }} - {{ $t('nA') }} + + {{ $t('nA') }} + + | ++ {{ record.notes || $t('nA') }} | -{{ record.notes || $t('nA') }} | |||||||||||