added current time(yellow ring) enhancement
follow server time instead of user computer local time.
This commit is contained in:
@@ -59,6 +59,22 @@ async function startServer() {
|
|||||||
|
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
// --- Public server time endpoints (no auth, no cache) ---
|
||||||
|
const timeHandler = (req, res) => {
|
||||||
|
const now = new Date();
|
||||||
|
const ymdKL = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Kuala_Lumpur',
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit'
|
||||||
|
}).format(now); // "YYYY-MM-DD"
|
||||||
|
|
||||||
|
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||||
|
res.set('Pragma', 'no-cache');
|
||||||
|
res.set('Expires', '0');
|
||||||
|
|
||||||
|
res.json({ nowIso: now.toISOString(), tz: 'Asia/Kuala_Lumpur', ymdKL });
|
||||||
|
};
|
||||||
|
app.get('/time', timeHandler); // public path
|
||||||
|
app.get('/api/time', timeHandler); // also under /api
|
||||||
|
|
||||||
app.use('/api/managers', managerRoutes(db));
|
app.use('/api/managers', managerRoutes(db));
|
||||||
app.use('/api', workerRoutes(db));
|
app.use('/api', workerRoutes(db));
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed, onUnmounted } 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';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -73,20 +73,59 @@ const { t: $t } = useI18n();
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const viewDate = ref(new Date());
|
const viewDate = ref(new Date());
|
||||||
const todayStr = new Date().toISOString().slice(0, 10);
|
// Server-driven KL date for the yellow ring (updates every 60s)
|
||||||
|
const todayStr = ref(null);
|
||||||
|
|
||||||
|
const TZ = 'Asia/Kuala_Lumpur';
|
||||||
|
|
||||||
|
// Helper: format YYYY-MM-DD in a given TZ
|
||||||
|
const ymdInTZ = (tz, d = new Date()) =>
|
||||||
|
new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit'
|
||||||
|
}).format(d);
|
||||||
|
|
||||||
|
// Pull today from server; try /api/time then /time; fallback to client KL
|
||||||
|
async function getServerDate() {
|
||||||
|
const parse = (data) => {
|
||||||
|
if (typeof data?.ymdKL === 'string') return data.ymdKL;
|
||||||
|
if (data?.nowIso) return ymdInTZ(TZ, new Date(data.nowIso));
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const path of ['/api/time', '/time']) {
|
||||||
|
try {
|
||||||
|
const d = await apiFetch(`${path}?_t=${Date.now()}`);
|
||||||
|
const y = parse(d);
|
||||||
|
if (y) return y;
|
||||||
|
} catch (_err) {
|
||||||
|
continue; // try next endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Server time unavailable; using client KL time.');
|
||||||
|
return ymdInTZ(TZ, new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _intervalId;
|
||||||
|
onMounted(async () => {
|
||||||
|
const update = async () => { todayStr.value = await getServerDate(); };
|
||||||
|
await update();
|
||||||
|
_intervalId = setInterval(update, 60_000);
|
||||||
|
});
|
||||||
|
onUnmounted(() => { if (_intervalId) clearInterval(_intervalId); });
|
||||||
|
|
||||||
const originalEnabledDates = ref(new Set());
|
const originalEnabledDates = ref(new Set());
|
||||||
const datesToEnable = ref(new Set());
|
const datesToEnable = ref(new Set());
|
||||||
const datesToDisable = ref(new Set());
|
const datesToDisable = ref(new Set());
|
||||||
|
|
||||||
const hasPendingChanges = computed(() => datesToEnable.value.size > 0 || datesToDisable.value.size > 0);
|
const hasPendingChanges = computed(() => datesToEnable.value.size > 0 || datesToDisable.value.size > 0);
|
||||||
const sortedEnableList = computed(() => Array.from(datesToEnable.value).sort());
|
const sortedEnableList = computed(() => Array.from(datesToEnable.value).sort());
|
||||||
const sortedDisableList = computed(() => Array.from(datesToDisable.value).sort());
|
const sortedDisableList = computed(() => Array.from(datesToDisable.value).sort());
|
||||||
|
|
||||||
const monthYear = computed(() => viewDate.value.toLocaleString('default', { month: 'long', year: 'numeric' }));
|
const monthYear = computed(() => viewDate.value.toLocaleString('default', { month: 'long', year: 'numeric' }));
|
||||||
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
|
||||||
const calendarGrid = computed(() => {
|
const calendarGrid = computed(() => {
|
||||||
const year = viewDate.value.getFullYear();
|
const year = viewDate.value.getFullYear();
|
||||||
const month = viewDate.value.getMonth();
|
const month = viewDate.value.getMonth();
|
||||||
const firstDayOfMonth = new Date(year, month, 1).getDay();
|
const firstDayOfMonth = new Date(year, month, 1).getDay();
|
||||||
@@ -105,7 +144,7 @@ const calendarGrid = computed(() => {
|
|||||||
return grid;
|
return grid;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getDayClasses = (day) => {
|
const getDayClasses = (day) => {
|
||||||
if (!day.isCurrentMonth) return 'h-20';
|
if (!day.isCurrentMonth) return 'h-20';
|
||||||
|
|
||||||
const dateStr = day.id;
|
const dateStr = day.id;
|
||||||
@@ -129,14 +168,14 @@ const getDayClasses = (day) => {
|
|||||||
classes.push('bg-white', 'dark:bg-gray-800', 'hover:bg-gray-100', 'dark:hover:bg-gray-700');
|
classes.push('bg-white', 'dark:bg-gray-800', 'hover:bg-gray-100', 'dark:hover:bg-gray-700');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateStr === todayStr) {
|
if (todayStr.value && dateStr === todayStr.value) {
|
||||||
classes.push('ring-2', 'ring-yellow-400', 'dark:ring-yellow-500');
|
classes.push('ring-2', 'ring-yellow-400', 'dark:ring-yellow-500');
|
||||||
}
|
}
|
||||||
|
|
||||||
return classes;
|
return classes;
|
||||||
};
|
};
|
||||||
|
|
||||||
function onDayClick(day) {
|
function onDayClick(day) {
|
||||||
const dateStr = day.id;
|
const dateStr = day.id;
|
||||||
const isOriginallyEnabled = originalEnabledDates.value.has(dateStr);
|
const isOriginallyEnabled = originalEnabledDates.value.has(dateStr);
|
||||||
|
|
||||||
@@ -151,7 +190,7 @@ function onDayClick(day) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyChanges() {
|
async function applyChanges() {
|
||||||
const confirmed = await toast.showConfirm($t('confirmApplyChanges'))
|
const confirmed = await toast.showConfirm($t('confirmApplyChanges'))
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
@@ -177,10 +216,10 @@ function discardChanges() {
|
|||||||
datesToDisable.value.clear();
|
datesToDisable.value.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() - 1));
|
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 nextMonth = () => viewDate.value = new Date(viewDate.value.setMonth(viewDate.value.getMonth() + 1));
|
||||||
|
|
||||||
const formatDate = (dateStr) => new Date(dateStr + 'T00:00:00').toLocaleDateString(undefined, {
|
const formatDate = (dateStr) => new Date(dateStr + 'T00:00:00').toLocaleDateString(undefined, {
|
||||||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user