Remove duration_minutes from event templates, auto-prefill event times

- Drop duration_minutes column from event_templates (model, schema, migration)
- Remove duration field from TemplateForm UI and TypeScript types
- EventForm now defaults start to current date/time and end to +1 hour
  when no initial values are provided (new events and template-based events)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-23 10:55:27 +08:00
parent 87708ae195
commit b1075d6ad4
6 changed files with 59 additions and 36 deletions

View File

@ -0,0 +1,27 @@
"""drop duration_minutes from event_templates
Revision ID: 013
Revises: 012
Create Date: 2026-02-23
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "013"
down_revision = "012"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.drop_column("event_templates", "duration_minutes")
def downgrade() -> None:
op.add_column(
"event_templates",
sa.Column("duration_minutes", sa.Integer(), server_default="60", nullable=False),
)

View File

@ -1,4 +1,4 @@
from sqlalchemy import String, Text, Integer, Boolean, ForeignKey from sqlalchemy import String, Text, Boolean, ForeignKey, Integer
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
@ -12,7 +12,6 @@ class EventTemplate(Base):
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
title: Mapped[str] = mapped_column(String(255), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
duration_minutes: Mapped[int] = mapped_column(Integer, default=60)
calendar_id: Mapped[Optional[int]] = mapped_column( calendar_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("calendars.id", ondelete="SET NULL"), nullable=True Integer, ForeignKey("calendars.id", ondelete="SET NULL"), nullable=True
) )

View File

@ -7,7 +7,6 @@ class EventTemplateCreate(BaseModel):
name: str name: str
title: str title: str
description: Optional[str] = None description: Optional[str] = None
duration_minutes: int = 60
calendar_id: Optional[int] = None calendar_id: Optional[int] = None
recurrence_rule: Optional[str] = None recurrence_rule: Optional[str] = None
all_day: bool = False all_day: bool = False
@ -19,7 +18,6 @@ class EventTemplateUpdate(BaseModel):
name: Optional[str] = None name: Optional[str] = None
title: Optional[str] = None title: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
duration_minutes: Optional[int] = None
calendar_id: Optional[int] = None calendar_id: Optional[int] = None
recurrence_rule: Optional[str] = None recurrence_rule: Optional[str] = None
all_day: Optional[bool] = None all_day: Optional[bool] = None
@ -32,7 +30,6 @@ class EventTemplateResponse(BaseModel):
name: str name: str
title: str title: str
description: Optional[str] description: Optional[str]
duration_minutes: int
calendar_id: Optional[int] calendar_id: Optional[int]
recurrence_rule: Optional[str] recurrence_rule: Optional[str]
all_day: bool all_day: bool

View File

@ -75,12 +75,29 @@ function parseRecurrenceRule(raw?: string): RecurrenceRule | null {
} }
} }
function nowLocal(): string {
const now = new Date();
const pad = (n: number) => n.toString().padStart(2, '0');
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
}
function plusOneHour(dt: string): string {
const d = new Date(dt);
d.setHours(d.getHours() + 1);
const pad = (n: number) => n.toString().padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
export default function EventForm({ event, initialStart, initialEnd, initialAllDay, editScope, onClose }: EventFormProps) { export default function EventForm({ event, initialStart, initialEnd, initialAllDay, editScope, onClose }: EventFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: calendars = [] } = useCalendars(); const { data: calendars = [] } = useCalendars();
const isAllDay = event?.all_day ?? initialAllDay ?? false; const isAllDay = event?.all_day ?? initialAllDay ?? false;
const rawStart = event?.start_datetime || initialStart || '';
const rawEnd = event?.end_datetime || initialEnd || ''; // Default to current time / +1 hour when creating a new event with no selection
const defaultStart = nowLocal();
const defaultEnd = plusOneHour(defaultStart);
const rawStart = event?.start_datetime || initialStart || defaultStart;
const rawEnd = event?.end_datetime || initialEnd || defaultEnd;
const defaultCalendar = calendars.find((c) => c.is_default); const defaultCalendar = calendars.find((c) => c.is_default);
const initialCalendarId = event?.calendar_id?.toString() || defaultCalendar?.id?.toString() || ''; const initialCalendarId = event?.calendar_id?.toString() || defaultCalendar?.id?.toString() || '';

View File

@ -45,7 +45,6 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
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',
calendar_id: template?.calendar_id?.toString() || '', calendar_id: template?.calendar_id?.toString() || '',
location_id: template?.location_id?.toString() || '', location_id: template?.location_id?.toString() || '',
all_day: template?.all_day || false, all_day: template?.all_day || false,
@ -58,7 +57,6 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
name: data.name, name: data.name,
title: data.title, title: data.title,
description: data.description || null, description: data.description || null,
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, location_id: data.location_id ? parseInt(data.location_id) : null,
all_day: data.all_day, all_day: data.all_day,
@ -128,19 +126,6 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="tmpl-duration">Duration (min)</Label>
<Input
id="tmpl-duration"
type="number"
min={5}
max={1440}
value={formData.duration_minutes}
onChange={(e) => setFormData({ ...formData, duration_minutes: e.target.value })}
/>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="tmpl-calendar">Calendar</Label> <Label htmlFor="tmpl-calendar">Calendar</Label>
<Select <Select
@ -154,7 +139,6 @@ export default function TemplateForm({ template, onClose }: TemplateFormProps) {
))} ))}
</Select> </Select>
</div> </div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="tmpl-location">Location</Label> <Label htmlFor="tmpl-location">Location</Label>

View File

@ -232,7 +232,6 @@ export interface EventTemplate {
name: string; name: string;
title: string; title: string;
description?: string; description?: string;
duration_minutes: number;
calendar_id?: number; calendar_id?: number;
recurrence_rule?: string; recurrence_rule?: string;
all_day: boolean; all_day: boolean;