Fix reminder alerts not firing and add AM/PM time picker

- Backend: /due endpoint now matches both NULL and empty string for
  recurrence_rule (form was sending '' not null, excluding all reminders)
- Form: sends null instead of '' for empty recurrence_rule
- ReminderForm: replaced datetime-local with date + hour/minute/AM-PM
  selects for 12-hour time format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-24 00:50:08 +08:00
parent 5080e23256
commit b2e336ab4a
2 changed files with 95 additions and 31 deletions

View File

@ -50,7 +50,10 @@ async def get_due_reminders(
Reminder.remind_at <= now, Reminder.remind_at <= now,
Reminder.is_dismissed == False, Reminder.is_dismissed == False,
Reminder.is_active == True, Reminder.is_active == True,
or_(
Reminder.recurrence_rule.is_(None), Reminder.recurrence_rule.is_(None),
Reminder.recurrence_rule == '',
),
or_( or_(
Reminder.snoozed_until.is_(None), Reminder.snoozed_until.is_(None),
Reminder.snoozed_until <= now, Reminder.snoozed_until <= now,

View File

@ -22,17 +22,48 @@ interface ReminderFormProps {
onClose: () => void; onClose: () => void;
} }
function parseRemindAt(remindAt?: string) {
if (!remindAt) return { date: '', hour: '12', minute: '00', period: 'PM' as const };
const d = new Date(remindAt);
const date = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
let hours = d.getHours();
const period = hours >= 12 ? 'PM' as const : 'AM' as const;
hours = hours % 12 || 12;
return {
date,
hour: String(hours),
minute: String(d.getMinutes()).padStart(2, '0'),
period,
};
}
function buildRemindAt(date: string, hour: string, minute: string, period: 'AM' | 'PM'): string {
if (!date) return '';
let h = parseInt(hour, 10);
if (period === 'AM' && h === 12) h = 0;
else if (period === 'PM' && h !== 12) h += 12;
return `${date}T${String(h).padStart(2, '0')}:${minute}`;
}
export default function ReminderForm({ reminder, onClose }: ReminderFormProps) { export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [formData, setFormData] = useState({ const parsed = parseRemindAt(reminder?.remind_at);
title: reminder?.title || '', const [title, setTitle] = useState(reminder?.title || '');
description: reminder?.description || '', const [description, setDescription] = useState(reminder?.description || '');
remind_at: reminder?.remind_at ? reminder.remind_at.slice(0, 16) : '', const [date, setDate] = useState(parsed.date);
recurrence_rule: reminder?.recurrence_rule || '', const [hour, setHour] = useState(parsed.hour);
}); const [minute, setMinute] = useState(parsed.minute);
const [period, setPeriod] = useState<'AM' | 'PM'>(parsed.period);
const [recurrenceRule, setRecurrenceRule] = useState(reminder?.recurrence_rule || '');
const mutation = useMutation({ const mutation = useMutation({
mutationFn: async (data: typeof formData) => { mutationFn: async () => {
const data = {
title,
description: description || null,
remind_at: buildRemindAt(date, hour, minute, period) || null,
recurrence_rule: recurrenceRule || null,
};
if (reminder) { if (reminder) {
const response = await api.put(`/reminders/${reminder.id}`, data); const response = await api.put(`/reminders/${reminder.id}`, data);
return response.data; return response.data;
@ -55,7 +86,7 @@ export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
const handleSubmit = (e: FormEvent) => { const handleSubmit = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
mutation.mutate(formData); mutation.mutate();
}; };
return ( return (
@ -71,8 +102,8 @@ export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
<Label htmlFor="title" required>Title</Label> <Label htmlFor="title" required>Title</Label>
<Input <Input
id="title" id="title"
value={formData.title} value={title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })} onChange={(e) => setTitle(e.target.value)}
required required
/> />
</div> </div>
@ -81,29 +112,60 @@ export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
<Label htmlFor="description">Description</Label> <Label htmlFor="description">Description</Label>
<Textarea <Textarea
id="description" id="description"
value={formData.description} value={description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(e) => setDescription(e.target.value)}
className="min-h-[80px] flex-1" className="min-h-[80px] flex-1"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="remind_at">Remind At</Label> <Label htmlFor="remind_date">Remind At</Label>
<div className="flex items-center gap-2">
<Input <Input
id="remind_at" id="remind_date"
type="datetime-local" type="date"
value={formData.remind_at} value={date}
onChange={(e) => setFormData({ ...formData, remind_at: e.target.value })} onChange={(e) => setDate(e.target.value)}
className="flex-1"
/> />
<Select
value={hour}
onChange={(e) => setHour(e.target.value)}
className="w-[4.5rem]"
>
{Array.from({ length: 12 }, (_, i) => i + 1).map((h) => (
<option key={h} value={String(h)}>{h}</option>
))}
</Select>
<span className="text-muted-foreground">:</span>
<Select
value={minute}
onChange={(e) => setMinute(e.target.value)}
className="w-[4.5rem]"
>
{Array.from({ length: 12 }, (_, i) => i * 5).map((m) => (
<option key={m} value={String(m).padStart(2, '0')}>
{String(m).padStart(2, '0')}
</option>
))}
</Select>
<Select
value={period}
onChange={(e) => setPeriod(e.target.value as 'AM' | 'PM')}
className="w-[4.5rem]"
>
<option value="AM">AM</option>
<option value="PM">PM</option>
</Select>
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="recurrence">Recurrence</Label> <Label htmlFor="recurrence">Recurrence</Label>
<Select <Select
id="recurrence" id="recurrence"
value={formData.recurrence_rule} value={recurrenceRule}
onChange={(e) => setFormData({ ...formData, recurrence_rule: e.target.value })} onChange={(e) => setRecurrenceRule(e.target.value)}
> >
<option value="">None</option> <option value="">None</option>
<option value="daily">Daily</option> <option value="daily">Daily</option>
@ -112,7 +174,6 @@ export default function ReminderForm({ reminder, onClose }: ReminderFormProps) {
</Select> </Select>
</div> </div>
</div> </div>
</div>
<SheetFooter> <SheetFooter>
<Button type="button" variant="outline" onClick={onClose}> <Button type="button" variant="outline" onClick={onClose}>