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>
210 lines
7.8 KiB
TypeScript
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>
|
|
);
|
|
}
|