Fix template event creation, add location to templates, fix timestamps

- EventForm: check event?.id instead of event to decide PUT vs POST,
  fixes "unable to parse string as integer" when creating from template
- TemplateForm: add LocationPicker for setting location on templates
- docker-compose: set TZ=Australia/Perth on backend and db containers
  so datetime.now() and PostgreSQL NOW() return local time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-22 19:11:48 +08:00
parent a144945077
commit 5c8b3f895d
3 changed files with 52 additions and 3 deletions

View File

@ -3,6 +3,8 @@ services:
image: postgres:16-alpine image: postgres:16-alpine
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
environment:
- TZ=Australia/Perth
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
healthcheck: healthcheck:
@ -17,6 +19,8 @@ services:
ports: ports:
- "8000:8000" - "8000:8000"
env_file: .env env_file: .env
environment:
- TZ=Australia/Perth
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy

View File

@ -159,7 +159,7 @@ export default function EventForm({ event, initialStart, initialEnd, initialAllD
recurrence_rule: rule, recurrence_rule: rule,
}; };
if (event) { if (event?.id) {
if (editScope) { if (editScope) {
payload.edit_scope = editScope; payload.edit_scope = editScope;
} }

View File

@ -1,8 +1,8 @@
import { useState, FormEvent } from 'react'; import { useState, FormEvent } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api'; import api, { getErrorMessage } from '@/lib/api';
import type { EventTemplate } from '@/types'; import type { EventTemplate, Location } from '@/types';
import { useCalendars } from '@/hooks/useCalendars'; import { useCalendars } from '@/hooks/useCalendars';
import { import {
Dialog, Dialog,
@ -18,6 +18,7 @@ import { Select } from '@/components/ui/select';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import LocationPicker from '@/components/ui/location-picker';
interface TemplateFormProps { interface TemplateFormProps {
template: EventTemplate | null; template: EventTemplate | null;
@ -29,12 +30,24 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
const { data: calendars = [] } = useCalendars(); const { data: calendars = [] } = useCalendars();
const selectableCalendars = calendars.filter((c) => !c.is_system); const selectableCalendars = calendars.filter((c) => !c.is_system);
const { data: locations = [] } = useQuery({
queryKey: ['locations'],
queryFn: async () => {
const { data } = await api.get<Location[]>('/locations');
return data;
},
});
const existingLocation = locations.find((l) => l.id === template?.location_id);
const [locationSearch, setLocationSearch] = useState(existingLocation?.name || '');
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: template?.name || '', name: template?.name || '',
title: template?.title || '', title: template?.title || '',
description: template?.description || '', description: template?.description || '',
duration_minutes: template?.duration_minutes?.toString() || '60', duration_minutes: template?.duration_minutes?.toString() || '60',
calendar_id: template?.calendar_id?.toString() || '', calendar_id: template?.calendar_id?.toString() || '',
location_id: template?.location_id?.toString() || '',
all_day: template?.all_day || false, all_day: template?.all_day || false,
is_starred: template?.is_starred || false, is_starred: template?.is_starred || false,
}); });
@ -47,6 +60,7 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
description: data.description || null, description: data.description || null,
duration_minutes: parseInt(data.duration_minutes) || 60, duration_minutes: parseInt(data.duration_minutes) || 60,
calendar_id: data.calendar_id ? parseInt(data.calendar_id) : null, calendar_id: data.calendar_id ? parseInt(data.calendar_id) : null,
location_id: data.location_id ? parseInt(data.location_id) : null,
all_day: data.all_day, all_day: data.all_day,
is_starred: data.is_starred, is_starred: data.is_starred,
}; };
@ -142,6 +156,37 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
</div> </div>
</div> </div>
<div className="space-y-2">
<Label htmlFor="tmpl-location">Location</Label>
<LocationPicker
id="tmpl-location"
value={locationSearch}
onChange={(val) => {
setLocationSearch(val);
if (!val) setFormData({ ...formData, location_id: '' });
}}
onSelect={async (result) => {
if (result.source === 'local' && result.location_id) {
setFormData({ ...formData, location_id: result.location_id.toString() });
} else if (result.source === 'nominatim') {
try {
const { data: newLoc } = await api.post('/locations', {
name: result.name,
address: result.address,
category: 'other',
});
queryClient.invalidateQueries({ queryKey: ['locations'] });
setFormData({ ...formData, location_id: newLoc.id.toString() });
toast.success(`Location "${result.name}" created`);
} catch {
toast.error('Failed to create location');
}
}
}}
placeholder="Search for a location..."
/>
</div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox