Add timezone selector to Admin settings with full-stack support
- Add `timezone` column to User model (VARCHAR 64, default UTC) with idempotent startup migration - Expose and persist timezone via PATCH /api/users/me - Fix TimerEvent.occurred_at serialization to include UTC offset marker (+00:00) so JavaScript correctly parses timestamps as UTC - Add frontend utility (src/utils/time.js) with timezone-aware formatTime, getHHMM, getDateInTZ, tzDateTimeToUTC helpers and a curated IANA timezone list - Add Settings section to Admin page with timezone dropdown; saves to both the API and localStorage for the unauthenticated TV view - Update Activity Log to display and edit times in the user's timezone - Update TV dashboard clock to respect the saved timezone - Update README: features, setup steps, usage table, WebSocket events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,7 @@
|
||||
|
||||
<!-- Display row -->
|
||||
<div v-else class="event-row" :class="entry._type">
|
||||
<div class="event-time">{{ formatTime(entry.occurred_at) }}</div>
|
||||
<div class="event-time">{{ fmtTime(entry.occurred_at) }}</div>
|
||||
<div
|
||||
class="event-dot"
|
||||
:style="entry.subject_color ? { background: entry.subject_color } : {}"
|
||||
@@ -119,11 +119,14 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useChildrenStore } from '@/stores/children'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import api from '@/composables/useApi'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import ChildSelector from '@/components/ChildSelector.vue'
|
||||
import { formatTime, getHHMM, getDateInTZ, tzDateTimeToUTC } from '@/utils/time'
|
||||
|
||||
const childrenStore = useChildrenStore()
|
||||
const authStore = useAuthStore()
|
||||
const timeline = ref([])
|
||||
const manualLogs = ref([])
|
||||
const showForm = ref(false)
|
||||
@@ -175,10 +178,8 @@ function formatDate(dateStr) {
|
||||
return d.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })
|
||||
}
|
||||
|
||||
function formatTime(isoStr) {
|
||||
if (!isoStr) return ''
|
||||
const d = new Date(isoStr)
|
||||
return d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||
function fmtTime(isoStr) {
|
||||
return formatTime(isoStr, authStore.timezone)
|
||||
}
|
||||
|
||||
const EVENT_META = {
|
||||
@@ -205,10 +206,10 @@ function eventLabel(entry) {
|
||||
function startEdit(entry) {
|
||||
editingEntry.value = entry
|
||||
if (entry._type === 'event') {
|
||||
const d = new Date(entry.occurred_at)
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
editDraft.value = { event_type: entry.event_type, time: `${hh}:${mm}` }
|
||||
editDraft.value = {
|
||||
event_type: entry.event_type,
|
||||
time: getHHMM(entry.occurred_at, authStore.timezone),
|
||||
}
|
||||
} else {
|
||||
editDraft.value = { notes: entry.notes || '' }
|
||||
}
|
||||
@@ -216,12 +217,11 @@ function startEdit(entry) {
|
||||
|
||||
async function saveEdit(entry) {
|
||||
if (entry._type === 'event') {
|
||||
const original = new Date(entry.occurred_at)
|
||||
const [hh, mm] = editDraft.value.time.split(':').map(Number)
|
||||
original.setHours(hh, mm, 0, 0)
|
||||
const dateInTZ = getDateInTZ(entry.occurred_at, authStore.timezone)
|
||||
const utcDate = tzDateTimeToUTC(dateInTZ, editDraft.value.time, authStore.timezone)
|
||||
await api.patch(`/api/logs/timeline/${entry.id}`, {
|
||||
event_type: editDraft.value.event_type,
|
||||
occurred_at: original.toISOString(),
|
||||
occurred_at: utcDate.toISOString(),
|
||||
})
|
||||
} else {
|
||||
await api.patch(`/api/logs/${entry.id}`, { notes: editDraft.value.notes })
|
||||
|
||||
Reference in New Issue
Block a user