From 596fd0f969faaabee1cea414bed450c781e73426 Mon Sep 17 00:00:00 2001 From: Ricky Stretch Date: Sat, 21 Mar 2026 00:23:29 -0400 Subject: [PATCH] v0.11.3 fixed timezone issue --- backend/package.json | 2 +- build.sh | 2 +- frontend/package.json | 2 +- frontend/src/components/MobileEventForm.jsx | 31 +++++++++++---- .../src/components/ScheduleManagerModal.jsx | 32 +++++++++++++--- frontend/src/components/SchedulePage.jsx | 38 +++++++++++++++---- 6 files changed, 83 insertions(+), 24 deletions(-) diff --git a/backend/package.json b/backend/package.json index 707c2c2..8ef3706 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.11.2", + "version": "0.11.3", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/build.sh b/build.sh index 926c51e..5378f43 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.11.2}" +VERSION="${1:-0.11.3}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index 6a013b7..8d7356a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.11.2", + "version": "0.11.3", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/MobileEventForm.jsx b/frontend/src/components/MobileEventForm.jsx index 86b795b..d0eb272 100644 --- a/frontend/src/components/MobileEventForm.jsx +++ b/frontend/src/components/MobileEventForm.jsx @@ -20,15 +20,32 @@ const TIME_SLOTS = (() => { return s; })(); -function toDateIn(iso) { return iso ? iso.slice(0,10) : ''; } +function toDateIn(iso) { + if (!iso) return ''; + const d = new Date(iso); + const pad = n => String(n).padStart(2,'0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; +} function toTimeIn(iso) { - if(!iso) return ''; - const d=new Date(iso); - const h=String(d.getHours()).padStart(2,'0'), m=d.getMinutes()<30?'00':'30'; + if (!iso) return ''; + const d = new Date(iso); + const h = String(d.getHours()).padStart(2,'0'), m = d.getMinutes() < 30 ? '00' : '30'; return `${h}:${m}`; } -function buildISO(d,t) { return d&&t?`${d}T${t}:00`:''; } -function addHours(iso,h){ const d=new Date(iso); d.setMinutes(d.getMinutes()+h*60); const pad=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`; } +function buildISO(date, time) { + if (!date || !time) return ''; + const d = new Date(`${date}T${time}:00`); + const pad = n => String(n).padStart(2,'0'); + const off = -d.getTimezoneOffset(); + const sign = off >= 0 ? '+' : '-'; + const abs = Math.abs(off); + return `${date}T${time}:00${sign}${pad(Math.floor(abs/60))}:${pad(abs%60)}`; +} +function addHours(iso, h) { + const d = new Date(iso); d.setMinutes(d.getMinutes() + h * 60); + const pad = n => String(n).padStart(2,'0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`; +} function fmtDateDisplay(iso) { if(!iso) return ''; const d=new Date(iso); return `${DAYS[d.getDay()]}, ${SHORT_MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; } function fmtTimeDisplay(slot) { const f=TIME_SLOTS.find(s=>s.value===slot); return f?f.label:slot; } @@ -252,7 +269,7 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte if(!title.trim()) return toast('Title required','error'); setSaving(true); try { - const body = { title:title.trim(), eventTypeId:typeId||null, startAt:allDay?`${sd}T00:00:00`:buildISO(sd,st), endAt:allDay?`${ed}T23:59:59`:buildISO(ed,et), allDay, location, description, isPublic:!isPrivate, trackAvailability:track, userGroupIds:[...groups], recurrenceRule:recRule||null }; + const body = { title:title.trim(), eventTypeId:typeId||null, startAt:allDay?buildISO(sd,'00:00'):buildISO(sd,st), endAt:allDay?buildISO(ed,'23:59'):buildISO(ed,et), allDay, location, description, isPublic:!isPrivate, trackAvailability:track, userGroupIds:[...groups], recurrenceRule:recRule||null }; let scope = 'this'; if(event && event.recurrence_rule?.freq) { const choice = window.confirm('This is a recurring event.\n\nOK = Update this and all future occurrences\nCancel = Update this event only'); diff --git a/frontend/src/components/ScheduleManagerModal.jsx b/frontend/src/components/ScheduleManagerModal.jsx index 36235bb..6d81976 100644 --- a/frontend/src/components/ScheduleManagerModal.jsx +++ b/frontend/src/components/ScheduleManagerModal.jsx @@ -16,13 +16,33 @@ function fmtTime(isoStr) { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function fmtTimeRange(start, end) { return `${fmtTime(start)} – ${fmtTime(end)}`; } -function toLocalDateInput(isoStr) { return isoStr ? isoStr.slice(0,10) : ''; } -function toLocalTimeInput(isoStr) { return isoStr ? isoStr.slice(11,16) : ''; } -function buildISO(date, time) { return date && time ? `${date}T${time}:00` : ''; } +function toLocalDateInput(isoStr) { + if (!isoStr) return ''; + const d = new Date(isoStr); + const pad = n => String(n).padStart(2,'0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; +} +function toLocalTimeInput(isoStr) { + if (!isoStr) return ''; + const d = new Date(isoStr); + const pad = n => String(n).padStart(2,'0'); + return `${pad(d.getHours())}:${pad(d.getMinutes())}`; +} +function buildISO(date, time) { + if (!date || !time) return ''; + const d = new Date(`${date}T${time}:00`); + const pad = n => String(n).padStart(2,'0'); + const off = -d.getTimezoneOffset(); + const sign = off >= 0 ? '+' : '-'; + const abs = Math.abs(off); + return `${date}T${time}:00${sign}${pad(Math.floor(abs/60))}:${pad(abs%60)}`; +} function addHours(isoStr, hrs) { const d = new Date(isoStr); d.setMinutes(d.getMinutes() + hrs * 60); - return d.toISOString().slice(0,19); + const pad = n => String(n).padStart(2,'0'); + // Return local datetime string — do NOT use toISOString() which shifts to UTC + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`; } function sameDay(a, b) { return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); @@ -174,8 +194,8 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc try { const body = { title: title.trim(), eventTypeId: eventTypeId || null, - startAt: allDay ? `${startDate}T00:00:00` : buildISO(startDate, startTime), - endAt: allDay ? `${endDate}T23:59:59` : buildISO(endDate, endTime), + startAt: allDay ? buildISO(startDate, '00:00') : buildISO(startDate, startTime), + endAt: allDay ? buildISO(endDate, '23:59') : buildISO(endDate, endTime), allDay, location, description, isPublic, trackAvailability: trackAvail, userGroupIds: [...selectedGroups], }; diff --git a/frontend/src/components/SchedulePage.jsx b/frontend/src/components/SchedulePage.jsx index 4656c6e..7b07e74 100644 --- a/frontend/src/components/SchedulePage.jsx +++ b/frontend/src/components/SchedulePage.jsx @@ -16,13 +16,35 @@ const SHORT_MONTHS= ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct' function fmtDate(d) { return `${d.getDate()} ${SHORT_MONTHS[d.getMonth()]} ${d.getFullYear()}`; } function fmtTime(iso) { if(!iso) return ''; const d=new Date(iso); return d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); } function fmtRange(s,e) { return `${fmtTime(s)} – ${fmtTime(e)}`; } -function toDateIn(iso) { return iso?iso.slice(0,10):''; } -function toTimeIn(iso) { if(!iso) return ''; const d=new Date(iso); const h=String(d.getHours()).padStart(2,'0'), m=d.getMinutes()<30?'00':'30'; return `${h}:${m}`; } -function buildISO(d,t) { return d&&t?`${d}T${t}:00`:''; } -function addHours(iso,h){ - const d=new Date(iso); d.setMinutes(d.getMinutes()+h*60); - // Return local datetime string (YYYY-MM-DDTHH:MM:SS) — NOT toISOString() which shifts to UTC - const pad=n=>String(n).padStart(2,'0'); +// Convert a UTC ISO string (from Postgres TIMESTAMPTZ) to local YYYY-MM-DD for +function toDateIn(iso) { + if (!iso) return ''; + const d = new Date(iso); + const pad = n => String(n).padStart(2,'0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; +} +// Convert a UTC ISO string to local HH:MM for , snapped to :00 or :30 +function toTimeIn(iso) { + if (!iso) return ''; + const d = new Date(iso); + const h = String(d.getHours()).padStart(2,'0'); + const m = d.getMinutes() < 30 ? '00' : '30'; + return `${h}:${m}`; +} +// Build an ISO string with local timezone offset so Postgres stores the right UTC value +function buildISO(date, time) { + if (!date || !time) return ''; + // Parse as local datetime then get offset-aware ISO string + const d = new Date(`${date}T${time}:00`); + const pad = n => String(n).padStart(2,'0'); + const off = -d.getTimezoneOffset(); + const sign = off >= 0 ? '+' : '-'; + const abs = Math.abs(off); + return `${date}T${time}:00${sign}${pad(Math.floor(abs/60))}:${pad(abs%60)}`; +} +function addHours(iso, h) { + const d = new Date(iso); d.setMinutes(d.getMinutes() + h * 60); + const pad = n => String(n).padStart(2,'0'); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`; } function sameDay(a,b) { return a.getFullYear()===b.getFullYear()&&a.getMonth()===b.getMonth()&&a.getDate()===b.getDate(); } @@ -396,7 +418,7 @@ function EventForm({ event, userGroups, eventTypes, selectedDate, onSave, onCanc if(groupsRequired&&grps.size===0) return toast('Select at least one group for availability tracking','error'); setSaving(true); try{ - const body={title:title.trim(),eventTypeId:typeId||null,startAt:allDay?`${sd}T00:00:00`:buildISO(sd,st),endAt:allDay?`${ed}T23:59:59`:buildISO(ed,et),allDay,location:loc,description:desc,isPublic:pub,trackAvailability:track,userGroupIds:[...grps],recurrenceRule:recRule||null}; + const body={title:title.trim(),eventTypeId:typeId||null,startAt:allDay?buildISO(sd,'00:00'):buildISO(sd,st),endAt:allDay?buildISO(ed,'23:59'):buildISO(ed,et),allDay,location:loc,description:desc,isPublic:pub,trackAvailability:track,userGroupIds:[...grps],recurrenceRule:recRule||null}; let scope='this'; if(event && event.recurrence_rule?.freq) { const choice = window.confirm('This is a recurring event.\n\nOK = Update this and all future occurrences\nCancel = Update this event only');