Add per-user notification time preferences

Allow users to customize when they receive notification reminders:
- Add task_due_soon_hour, task_overdue_hour, warranty_expiring_hour fields
- Store times in UTC, clients convert to/from local timezone
- Worker runs hourly, queries only users scheduled for that hour
- Early exit optimization when no users need notifications
- Admin UI displays custom notification times

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-07 00:23:57 -06:00
parent af87bd943e
commit dd16019ce2
7 changed files with 267 additions and 113 deletions
@@ -4,9 +4,17 @@ import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ColumnDef } from '@tanstack/react-table';
import Link from 'next/link';
import { MoreHorizontal, Trash2 } from 'lucide-react';
import { MoreHorizontal, Trash2, Clock } from 'lucide-react';
import { notificationPrefsApi, type NotificationPreference } from '@/lib/api';
// Helper function to format UTC hour for display
function formatHour(hour: number | null): string {
if (hour === null) return '-';
const h = hour % 12 || 12;
const ampm = hour < 12 ? 'AM' : 'PM';
return `${h}:00 ${ampm}`;
}
import { DataTable } from '@/components/data-table';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
@@ -166,6 +174,43 @@ export default function NotificationPrefsPage() {
/>
),
},
{
id: 'notification_times',
header: () => (
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span>Custom Times (UTC)</span>
</div>
),
cell: ({ row }) => {
const { task_due_soon_hour, task_overdue_hour, warranty_expiring_hour } = row.original;
const hasCustomTimes = task_due_soon_hour !== null || task_overdue_hour !== null || warranty_expiring_hour !== null;
if (!hasCustomTimes) {
return <span className="text-muted-foreground text-sm">Default</span>;
}
return (
<div className="text-sm space-y-0.5">
{task_due_soon_hour !== null && (
<div className="text-xs">
<span className="text-muted-foreground">Due Soon:</span> {formatHour(task_due_soon_hour)}
</div>
)}
{task_overdue_hour !== null && (
<div className="text-xs">
<span className="text-muted-foreground">Overdue:</span> {formatHour(task_overdue_hour)}
</div>
)}
{warranty_expiring_hour !== null && (
<div className="text-xs">
<span className="text-muted-foreground">Warranty:</span> {formatHour(warranty_expiring_hour)}
</div>
)}
</div>
);
},
},
{
id: 'actions',
cell: ({ row }) => {
+4
View File
@@ -599,6 +599,10 @@ export interface NotificationPreference {
warranty_expiring: boolean;
// Email preferences
email_task_completed: boolean;
// Custom notification times (UTC hour 0-23, null means use system default)
task_due_soon_hour: number | null;
task_overdue_hour: number | null;
warranty_expiring_hour: number | null;
created_at: string;
updated_at: string;
}