UMBRA/frontend/src/components/calendar/TemplateForm.tsx
Kyle Pope b21343601b Convert TemplateForm from Dialog to Sheet side panel
Matches the EventForm UI pattern for consistency — same slide-in panel,
same layout structure with scrollable content area and pinned footer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 11:58:55 +08:00

210 lines
7.8 KiB
TypeScript

import { useState, FormEvent } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api';
import type { EventTemplate, Location } from '@/types';
import { useCalendars } from '@/hooks/useCalendars';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetFooter,
SheetClose,
} from '@/components/ui/sheet';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Select } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import LocationPicker from '@/components/ui/location-picker';
interface TemplateFormProps {
template: EventTemplate | null;
onClose: () => void;
}
export default function TemplateForm({ template, onClose }: TemplateFormProps) {
const queryClient = useQueryClient();
const { data: calendars = [] } = useCalendars();
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({
name: template?.name || '',
title: template?.title || '',
description: template?.description || '',
calendar_id: template?.calendar_id?.toString() || '',
location_id: template?.location_id?.toString() || '',
all_day: template?.all_day || false,
is_starred: template?.is_starred || false,
});
const mutation = useMutation({
mutationFn: async (data: typeof formData) => {
const payload = {
name: data.name,
title: data.title,
description: data.description || 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,
is_starred: data.is_starred,
};
if (template) {
const { data: res } = await api.put(`/event-templates/${template.id}`, payload);
return res;
} else {
const { data: res } = await api.post('/event-templates', payload);
return res;
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['event-templates'] });
toast.success(template ? 'Template updated' : 'Template created');
onClose();
},
onError: (error) => {
toast.error(getErrorMessage(error, 'Failed to save template'));
},
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!formData.name.trim() || !formData.title.trim()) return;
mutation.mutate(formData);
};
return (
<Sheet open={true} onOpenChange={onClose}>
<SheetContent>
<SheetClose onClick={onClose} />
<SheetHeader>
<SheetTitle>{template ? 'Edit Template' : 'New Template'}</SheetTitle>
</SheetHeader>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-y-auto">
<div className="px-6 py-5 space-y-4 flex-1">
<div className="space-y-2">
<Label htmlFor="tmpl-name" required>Template Name</Label>
<Input
id="tmpl-name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="e.g., Weekly standup"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="tmpl-title" required>Event Title</Label>
<Input
id="tmpl-title"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="Title for created events"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="tmpl-desc">Description</Label>
<Textarea
id="tmpl-desc"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="min-h-[80px] flex-1"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="tmpl-calendar">Calendar</Label>
<Select
id="tmpl-calendar"
value={formData.calendar_id}
onChange={(e) => setFormData({ ...formData, calendar_id: e.target.value })}
>
<option value="">Default</option>
{selectableCalendars.map((cal) => (
<option key={cal.id} value={cal.id}>{cal.name}</option>
))}
</Select>
</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>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Checkbox
id="tmpl-allday"
checked={formData.all_day}
onChange={(e) => setFormData({ ...formData, all_day: (e.target as HTMLInputElement).checked })}
/>
<Label htmlFor="tmpl-allday">All day</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox
id="tmpl-starred"
checked={formData.is_starred}
onChange={(e) => setFormData({ ...formData, is_starred: (e.target as HTMLInputElement).checked })}
/>
<Label htmlFor="tmpl-starred">Starred</Label>
</div>
</div>
</div>
<SheetFooter>
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : template ? 'Update' : 'Create'}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}