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:
@@ -107,6 +107,8 @@ export default function SiteSettingsPage() {
|
|||||||
frequency: 'weekly' as string,
|
frequency: 'weekly' as string,
|
||||||
reportType: 'summary' as string,
|
reportType: 'summary' as string,
|
||||||
timezone: '',
|
timezone: '',
|
||||||
|
sendHour: 9,
|
||||||
|
sendDay: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -235,6 +237,8 @@ export default function SiteSettingsPage() {
|
|||||||
frequency: 'weekly',
|
frequency: 'weekly',
|
||||||
reportType: 'summary',
|
reportType: 'summary',
|
||||||
timezone: site?.timezone || '',
|
timezone: site?.timezone || '',
|
||||||
|
sendHour: 9,
|
||||||
|
sendDay: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +252,8 @@ export default function SiteSettingsPage() {
|
|||||||
frequency: schedule.frequency,
|
frequency: schedule.frequency,
|
||||||
reportType: schedule.report_type,
|
reportType: schedule.report_type,
|
||||||
timezone: schedule.timezone || site?.timezone || '',
|
timezone: schedule.timezone || site?.timezone || '',
|
||||||
|
sendHour: schedule.send_hour ?? 9,
|
||||||
|
sendDay: schedule.send_day ?? (schedule.frequency === 'monthly' ? 1 : 0),
|
||||||
})
|
})
|
||||||
setReportModalOpen(true)
|
setReportModalOpen(true)
|
||||||
}
|
}
|
||||||
@@ -277,6 +283,8 @@ export default function SiteSettingsPage() {
|
|||||||
frequency: reportForm.frequency,
|
frequency: reportForm.frequency,
|
||||||
timezone: reportForm.timezone || undefined,
|
timezone: reportForm.timezone || undefined,
|
||||||
report_type: reportForm.reportType,
|
report_type: reportForm.reportType,
|
||||||
|
send_hour: reportForm.sendHour,
|
||||||
|
...(reportForm.frequency !== 'daily' ? { send_day: reportForm.sendDay } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
setReportSaving(true)
|
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) => {
|
const getReportTypeLabel = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'summary': return 'Summary'
|
case 'summary': return 'Summary'
|
||||||
@@ -1367,7 +1403,10 @@ export default function SiteSettingsPage() {
|
|||||||
? (schedule.channel_config as EmailConfig).recipients.join(', ')
|
? (schedule.channel_config as EmailConfig).recipients.join(', ')
|
||||||
: (schedule.channel_config as WebhookConfig).url}
|
: (schedule.channel_config as WebhookConfig).url}
|
||||||
</p>
|
</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>
|
<span>
|
||||||
Last sent: {schedule.last_sent_at
|
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' })
|
? 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>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Report Type</label>
|
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">Report Type</label>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export interface ReportSchedule {
|
|||||||
timezone: string
|
timezone: string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
report_type: 'summary' | 'pages' | 'sources' | 'goals'
|
report_type: 'summary' | 'pages' | 'sources' | 'goals'
|
||||||
|
send_hour: number
|
||||||
|
send_day: number | null
|
||||||
|
next_send_at: string | null
|
||||||
last_sent_at: string | null
|
last_sent_at: string | null
|
||||||
last_error: string | null
|
last_error: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -30,6 +33,8 @@ export interface CreateReportScheduleRequest {
|
|||||||
frequency: string
|
frequency: string
|
||||||
timezone?: string
|
timezone?: string
|
||||||
report_type?: string
|
report_type?: string
|
||||||
|
send_hour?: number
|
||||||
|
send_day?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateReportScheduleRequest {
|
export interface UpdateReportScheduleRequest {
|
||||||
@@ -39,6 +44,8 @@ export interface UpdateReportScheduleRequest {
|
|||||||
timezone?: string
|
timezone?: string
|
||||||
report_type?: string
|
report_type?: string
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
|
send_hour?: number
|
||||||
|
send_day?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listReportSchedules(siteId: string): Promise<ReportSchedule[]> {
|
export async function listReportSchedules(siteId: string): Promise<ReportSchedule[]> {
|
||||||
|
|||||||
Reference in New Issue
Block a user