feat: add time-of-day controls to scheduled reports UI

Add send hour, day of week/month selectors to report schedule modal.
Schedule cards now show descriptive delivery times like
"Every Monday at 9:00 AM (UTC)". Timezone picker moved into modal.
This commit is contained in:
Usman Baig
2026-03-12 15:17:46 +01:00
parent c6ec4671a4
commit 27a9836d5a
2 changed files with 106 additions and 1 deletions

View File

@@ -107,6 +107,8 @@ export default function SiteSettingsPage() {
frequency: 'weekly' as string,
reportType: 'summary' as string,
timezone: '',
sendHour: 9,
sendDay: 1,
})
useEffect(() => {
@@ -235,6 +237,8 @@ export default function SiteSettingsPage() {
frequency: 'weekly',
reportType: 'summary',
timezone: site?.timezone || '',
sendHour: 9,
sendDay: 1,
})
}
@@ -248,6 +252,8 @@ export default function SiteSettingsPage() {
frequency: schedule.frequency,
reportType: schedule.report_type,
timezone: schedule.timezone || site?.timezone || '',
sendHour: schedule.send_hour ?? 9,
sendDay: schedule.send_day ?? (schedule.frequency === 'monthly' ? 1 : 0),
})
setReportModalOpen(true)
}
@@ -277,6 +283,8 @@ export default function SiteSettingsPage() {
frequency: reportForm.frequency,
timezone: reportForm.timezone || undefined,
report_type: reportForm.reportType,
send_hour: reportForm.sendHour,
...(reportForm.frequency !== 'daily' ? { send_day: reportForm.sendDay } : {}),
}
setReportSaving(true)
@@ -349,6 +357,34 @@ export default function SiteSettingsPage() {
}
}
const WEEKDAY_NAMES = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
const formatHour = (hour: number) => {
if (hour === 0) return '12:00 AM'
if (hour === 12) return '12:00 PM'
return hour < 12 ? `${hour}:00 AM` : `${hour - 12}:00 PM`
}
const getScheduleDescription = (schedule: ReportSchedule) => {
const hour = formatHour(schedule.send_hour ?? 9)
const tz = schedule.timezone || 'UTC'
switch (schedule.frequency) {
case 'daily':
return `Every day at ${hour} (${tz})`
case 'weekly': {
const day = WEEKDAY_NAMES[schedule.send_day ?? 0] || 'Monday'
return `Every ${day} at ${hour} (${tz})`
}
case 'monthly': {
const d = schedule.send_day ?? 1
const suffix = d === 1 ? 'st' : d === 2 ? 'nd' : d === 3 ? 'rd' : 'th'
return `${d}${suffix} of each month at ${hour} (${tz})`
}
default:
return schedule.frequency
}
}
const getReportTypeLabel = (type: string) => {
switch (type) {
case 'summary': return 'Summary'
@@ -1367,7 +1403,10 @@ export default function SiteSettingsPage() {
? (schedule.channel_config as EmailConfig).recipients.join(', ')
: (schedule.channel_config as WebhookConfig).url}
</p>
<div className="flex items-center gap-3 mt-1.5 text-xs text-neutral-400 dark:text-neutral-500">
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
{getScheduleDescription(schedule)}
</p>
<div className="flex items-center gap-3 mt-1 text-xs text-neutral-400 dark:text-neutral-500">
<span>
Last sent: {schedule.last_sent_at
? new Date(schedule.last_sent_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' })
@@ -1556,6 +1595,65 @@ export default function SiteSettingsPage() {
/>
</div>
{reportForm.frequency === 'weekly' && (
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Day of week</label>
<Select
value={String(reportForm.sendDay)}
onChange={(v) => setReportForm({ ...reportForm, sendDay: parseInt(v) })}
options={WEEKDAY_NAMES.map((name, i) => ({ value: String(i), label: name }))}
variant="input"
fullWidth
align="left"
/>
</div>
)}
{reportForm.frequency === 'monthly' && (
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Day of month</label>
<Select
value={String(reportForm.sendDay)}
onChange={(v) => setReportForm({ ...reportForm, sendDay: parseInt(v) })}
options={Array.from({ length: 28 }, (_, i) => {
const d = i + 1
const suffix = d === 1 ? 'st' : d === 2 ? 'nd' : d === 3 ? 'rd' : 'th'
return { value: String(d), label: `${d}${suffix}` }
})}
variant="input"
fullWidth
align="left"
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Time</label>
<Select
value={String(reportForm.sendHour)}
onChange={(v) => setReportForm({ ...reportForm, sendHour: parseInt(v) })}
options={Array.from({ length: 24 }, (_, i) => ({
value: String(i),
label: formatHour(i),
}))}
variant="input"
fullWidth
align="left"
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Timezone</label>
<Select
value={reportForm.timezone || 'UTC'}
onChange={(v) => setReportForm({ ...reportForm, timezone: v })}
options={TIMEZONES.map((tz) => ({ value: tz, label: tz }))}
variant="input"
fullWidth
align="left"
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Report Type</label>
<Select

View File

@@ -10,6 +10,9 @@ export interface ReportSchedule {
timezone: string
enabled: boolean
report_type: 'summary' | 'pages' | 'sources' | 'goals'
send_hour: number
send_day: number | null
next_send_at: string | null
last_sent_at: string | null
last_error: string | null
created_at: string
@@ -30,6 +33,8 @@ export interface CreateReportScheduleRequest {
frequency: string
timezone?: string
report_type?: string
send_hour?: number
send_day?: number
}
export interface UpdateReportScheduleRequest {
@@ -39,6 +44,8 @@ export interface UpdateReportScheduleRequest {
timezone?: string
report_type?: string
enabled?: boolean
send_hour?: number
send_day?: number
}
export async function listReportSchedules(siteId: string): Promise<ReportSchedule[]> {