diff --git a/backend/package.json b/backend/package.json
index ce1923c..df650ae 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "jama-backend",
- "version": "0.11.4",
+ "version": "0.11.5",
"description": "TeamChat backend server",
"main": "src/index.js",
"scripts": {
diff --git a/build.sh b/build.sh
index 06c90c0..ec94744 100644
--- a/build.sh
+++ b/build.sh
@@ -13,7 +13,7 @@
# ─────────────────────────────────────────────────────────────
set -euo pipefail
-VERSION="${1:-0.11.4}"
+VERSION="${1:-0.11.5}"
ACTION="${2:-}"
REGISTRY="${REGISTRY:-}"
IMAGE_NAME="jama"
diff --git a/frontend/package.json b/frontend/package.json
index 83df088..dad2013 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "jama-frontend",
- "version": "0.11.4",
+ "version": "0.11.5",
"private": true,
"scripts": {
"dev": "vite",
diff --git a/frontend/src/components/MobileEventForm.jsx b/frontend/src/components/MobileEventForm.jsx
index 6b643ae..a25f2b6 100644
--- a/frontend/src/components/MobileEventForm.jsx
+++ b/frontend/src/components/MobileEventForm.jsx
@@ -204,7 +204,10 @@ function MobileRow({ icon, label, children, onPress, border=true }) {
// ── Main Mobile Event Form ────────────────────────────────────────────────────
export default function MobileEventForm({ event, eventTypes, userGroups, selectedDate, onSave, onCancel, onDelete, isToolManager }) {
const toast = useToast();
- const def = selectedDate ? selectedDate.toISOString().slice(0,10) : new Date().toISOString().slice(0,10);
+ // Use local date for default, not UTC slice (avoids off-by-one for UTC- timezones)
+ const defDate = selectedDate || new Date();
+ const _pad = n => String(n).padStart(2,'0');
+ const def = `${defDate.getFullYear()}-${_pad(defDate.getMonth()+1)}-${_pad(defDate.getDate())}`;
const [title, setTitle] = useState(event?.title||'');
const [typeId, setTypeId] = useState(event?.event_type_id ? String(event.event_type_id) : '');
const [localTypes, setLocalTypes] = useState(eventTypes);
@@ -217,6 +220,12 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
const [st, setSt] = useState(event ? toTimeIn(event.start_at) : '09:00');
const [ed, setEd] = useState(event ? toDateIn(event.end_at) : def);
const [et, setEt] = useState(event ? toTimeIn(event.end_at) : '10:00');
+ // Track the saved event duration (minutes) so editing preserves it
+ const savedDurMins = event
+ ? (new Date(event.end_at) - new Date(event.start_at)) / 60000
+ : null;
+ // Track previous typeId so we can detect a type change vs start time change
+ const prevTypeIdRef = useRef(event?.event_type_id ? String(event.event_type_id) : '');
const [allDay, setAllDay] = useState(!!event?.all_day);
const [track, setTrack] = useState(!!event?.track_availability);
const [isPrivate, setIsPrivate] = useState(event ? !event.is_public : false);
@@ -253,25 +262,42 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
finally { setSavingType(false); }
};
- // When start date or start time changes, update end date/time to maintain duration
+ // Auto-calculate end date/time when start date, start time, or event type changes.
+ // Rules:
+ // - New event: use eventType duration (default 1hr)
+ // - Editing + type changed: use new eventType duration
+ // - Editing + type same: use saved event duration (preserve original length)
+ // - Always: if end < start, advance end date by 1 day (overnight events)
useEffect(() => {
if(!sd||!st) return;
- const typ = localTypes.find(t=>t.id===Number(typeId));
- const dur = typ?.default_duration_hrs||1;
const start = buildISO(sd,st);
if(!start) return;
- if(event) {
- // Editing: only sync end date when start date changes, preserve manual end time
- setEd(toDateIn(addHours(start, 0)));
+
+ const typeChanged = typeId !== prevTypeIdRef.current;
+ prevTypeIdRef.current = typeId;
+
+ let durMins;
+ if(!event || typeChanged) {
+ // New event or type change: use eventType duration
+ const typ = localTypes.find(t=>t.id===Number(typeId));
+ durMins = (typ?.default_duration_hrs||1) * 60;
} else {
- // New event: always auto-set end to start + duration
- setEd(toDateIn(addHours(start,dur)));
- setEt(toTimeIn(addHours(start,dur)));
+ // Editing with same type: preserve the saved event duration
+ durMins = savedDurMins || 60;
}
+
+ const endIso = addHours(start, durMins/60);
+ setEd(toDateIn(endIso));
+ setEt(toTimeIn(endIso));
}, [sd, st, typeId]);
const handle = async () => {
if(!title.trim()) return toast('Title required','error');
+ // Validation rules
+ const startMs = new Date(buildISO(sd, allDay?'00:00':st)).getTime();
+ const endMs = new Date(buildISO(ed, allDay?'23:59':et)).getTime();
+ if(ed < sd) return toast('End date cannot be before start date','error');
+ if(!allDay && endMs <= startMs && ed === sd) return toast('End time must be after start time, or set a later end date','error');
setSaving(true);
try {
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 };
@@ -342,7 +368,17 @@ export default function MobileEventForm({ event, eventTypes, userGroups, selecte
setShowEndDate(true)} style={{ display:'flex',alignItems:'center',padding:'6px 20px 14px 56px',cursor:'pointer',borderBottom:'1px solid var(--border)' }}>
{fmtDateDisplay(ed)}
{!allDay && (
-