Browse documentation

Frontend Specs

Section 13 — Admin Reports (`/admin/reports`)

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard.

docs/specs/frontend/admin-spec-04c-admin-reports.md
On this page

Role access: ADMIN only. VIEWER/EDITOR/CLIENT redirected to /dashboard. Layout: 3-tab interface: Report Builder, Scheduled, Library. 1200px max-width. Sidebar: carbon (#12161E) background. Notes: Reports are platform-wide analytics and operational summaries. They can be one-time or scheduled (daily/weekly/monthly). All reports support export to PDF, CSV, and email delivery. Report generation is async and may take 1–5 minutes for large datasets.


13.1 Report Builder#

1. Purpose#

Create custom reports by selecting data sources, metrics, filters, and visualizations. Admins build one-time or reusable report templates for board meetings, investor updates, team reviews, or compliance documentation.

2. Visual Layout#

Section A: Report Configuration Form (top, 60% width)

  • Report name: Text input, required, max 100 chars. Placeholder: "Q2 Revenue Summary".
  • Description: Textarea, optional, max 500 chars. Placeholder: "Monthly report for board meeting."
  • Data sources: Multi-select checkboxes (grouped by category):
    • Business: Revenue, Subscriptions, Clients, Churn, ARPU
    • Content: Content generated, Content published, Approval queue, Content performance
    • Marketing: Leads, Lead sources, Conversion rate, Campaign performance
    • Operations: API usage, Worker throughput, Queue depth, System health
    • Support: Tickets (if support system exists), Response time, Resolution rate
  • Metrics per source: When a source is checked, metric selectors appear below it:
    • Revenue source: MRR, ARR, New MRR, Churned MRR, Expansion MRR, ARPU
    • Content source: Total content, Published content, Pending approval, Avg generation time
    • Each metric: checkbox + optional "Compare to previous period" toggle
  • Time range: Dropdown — "Last 7 days", "Last 30 days", "Last 90 days", "Last 12 months", "Quarter to date", "Year to date", "Custom" (date picker).
  • Filters: Dynamic filters based on selected sources:
    • Plan filter (if revenue/subscription selected)
    • Client filter (if any client-specific data selected)
    • Status filter (if applicable)
  • Visualization: Radio group — "Table only", "Charts + Table", "Charts only", "Executive Summary" (narrative with key highlights).
  • Comparison: Toggle "Compare to previous period" — adds prior period columns/chart series.
  • "Generate Report" button: Ember Orange CTA. Disabled until name and at least one source/metric selected.

Section B: Live Preview (top, 40% width)

  • As admin selects metrics and time range, a live preview updates:
    • Mini chart placeholder: "Select metrics to see preview"
    • Once metrics selected: sparkline or mini bar chart of the selected metric over the time range
    • Key stat: "{N} {metric} in selected period" with trend arrow
  • Preview is a low-fidelity approximation (uses cached data, not full report generation).

Section C: Report Templates (below form, full width)

  • Horizontal scrollable card row of pre-built templates:
    • "Monthly Revenue Report" (Revenue + Subscriptions + Churn, last 30 days, charts + table)
    • "Content Performance Report" (Content + Marketing, last 30 days, charts + table)
    • "System Health Report" (Operations only, last 7 days, executive summary)
    • "Board Meeting Deck" (All sources, last 90 days, executive summary, comparison enabled)
    • "GDPR Compliance Report" (Data Governance + Audit, last 90 days, table only)
  • Each template card: icon + name + description + "Use Template" button (secondary). Clicking pre-fills the form.

3. Data Source (tRPC endpoint)#

admin.getReportPreview.useQuery({
  sources: z.array(z.string()),
  metrics: z.array(z.string()),
  timeRange: z.object({ from: z.date(), to: z.date() }),
  filters: z.record(z.any()).optional(),
}, { enabled: false }); // manual trigger when preview requested

admin.generateReport.useMutation({
  name: z.string().min(1).max(100),
  description: z.string().max(500).optional(),
  sources: z.array(z.string()).min(1),
  metrics: z.array(z.string()).min(1),
  timeRange: z.object({ from: z.date(), to: z.date() }),
  filters: z.record(z.any()).optional(),
  visualization: z.enum(["TABLE", "CHARTS_TABLE", "CHARTS", "EXECUTIVE"]),
  compareToPrevious: z.boolean().default(false),
  schedule: z.object({
    enabled: z.boolean().default(false),
    frequency: z.enum(["DAILY", "WEEKLY", "MONTHLY"]).optional(),
    recipients: z.array(z.string().email()).optional(),
  }).optional(),
});

4. Zod Schema#

const ReportSourceSchema = z.enum([
  "REVENUE", "SUBSCRIPTIONS", "CLIENTS", "CHURN", "ARPU",
  "CONTENT", "CONTENT_PERFORMANCE", "APPROVAL_QUEUE",
  "LEADS", "LEAD_SOURCES", "CONVERSION_RATE", "CAMPAIGNS",
  "API_USAGE", "WORKER_THROUGHPUT", "QUEUE_DEPTH", "SYSTEM_HEALTH",
  "SUPPORT_TICKETS", "RESPONSE_TIME", "RESOLUTION_RATE",
  "AUDIT_TRAIL", "DATA_GOVERNANCE",
]);

const ReportMetricSchema = z.string(); // dynamic based on source, e.g., "MRR", "ARR", "TOTAL_CONTENT"

const ReportVisualizationSchema = z.enum(["TABLE", "CHARTS_TABLE", "CHARTS", "EXECUTIVE"]);

const ReportTemplateSchema = z.object({
  id: z.string(),
  name: z.string(),
  description: z.string(),
  icon: z.string(), // Lucide icon name
  sources: z.array(ReportSourceSchema),
  metrics: z.array(ReportMetricSchema),
  timeRange: z.string(), // e.g., "LAST_30_DAYS"
  visualization: ReportVisualizationSchema,
  compareToPrevious: z.boolean().default(false),
});

const ReportInputSchema = z.object({
  name: z.string().min(1).max(100),
  description: z.string().max(500).optional(),
  sources: z.array(ReportSourceSchema).min(1),
  metrics: z.array(ReportMetricSchema).min(1),
  timeRange: z.object({ from: z.date(), to: z.date() }),
  filters: z.record(z.any()).optional(),
  visualization: ReportVisualizationSchema,
  compareToPrevious: z.boolean().default(false),
  schedule: z.object({
    enabled: z.boolean().default(false),
    frequency: z.enum(["DAILY", "WEEKLY", "MONTHLY"]).optional(),
    recipients: z.array(z.string().email()).optional(),
  }).optional(),
});

const ReportPreviewSchema = z.object({
  metricName: z.string(),
  value: z.number(),
  trend: z.enum(["UP", "DOWN", "STABLE"]),
  trendPercent: z.number(),
  miniChart: z.array(z.object({
    label: z.string(),
    value: z.number(),
  })).optional(),
});

5. Fetch Frequency#

  • On-demand: Preview is manually triggered (debounced 500ms after metric selection). Report generation is on-demand mutation.
  • Static: Templates are static data (no API call needed).

6. Data Manipulations#

  • Preview aggregation: Uses pre-aggregated cached data (last 24h) for instant preview. Full report uses raw data (may take minutes).
  • Metric labels: "MRR" → "Monthly Recurring Revenue"; "TOTAL_CONTENT" → "Total Content Generated"; human-readable mapping.
  • Time range presets: "LAST_7_DAYS" → from: now-7d, to: now. "QUARTER_TO_DATE" → from: quarter start, to: now. Custom shows date pickers.
  • Filter dependency: Revenue metrics show plan filter. Content metrics show status filter. Dynamic filter UI based on selected sources.
  • Template pre-fill: Click template → form populated with all values. Admin can modify before generating.
  • Comparison: If enabled, all metrics show "vs previous period" column or dashed line in chart. Previous period = equal duration before from date.
  • Executive summary: Narrative generation using AI (optional). "MRR grew 12% to $12,450, driven by 3 new Pro subscriptions. Churn remained low at 2%. Content generation increased 34% due to new AI model."

7. Rationale#

  • Self-service reporting: Admins shouldn't need engineering to generate reports. Point-and-click report builder empowers ops, finance, and leadership teams.
  • Pre-built templates: 80% of reports are the same every month. Templates save time. "Monthly Revenue Report" is one click away.
  • Live preview: Prevents "generate and hope." Admin sees a preview of the data before committing to a full report generation (which may take 5 minutes and cost compute).
  • Multi-source reports: Board reports need revenue + content + operations in one PDF. Single-source reports are too narrow for executive audiences.
  • Comparison: "MRR is $12,450" is a number. "MRR is $12,450, up 12% from last month" is insight. Comparison is the default for executive reports.
  • Visualization options: Tables for detail-oriented analysts. Charts for executives who want trends. Executive summary for board members who want the narrative. One report, multiple outputs.
  • Scheduling: If a report is needed weekly, don't make the admin click "generate" every Monday. Schedule it. Email delivery to stakeholders.
  • Cached preview: Full report generation queries large datasets. Preview uses cached aggregates to be instant. Only the full report is expensive.
  • Filter dependency: Showing a "plan filter" when the admin selected "API usage" is confusing. Dynamic filters keep the UI clean and relevant.
  • Template icons: Visual identification of report type. Revenue = dollar icon, Content = file icon, System = server icon.

8. Interaction Flows#

  • Select template: Click "Use Template" on any template card → form pre-filled → modify as needed → "Generate Report".
  • Build from scratch: Type name → select sources → metrics appear → select metrics → configure time range → set filters → choose visualization → toggle comparison → "Generate".
  • Preview update: After selecting 1+ metrics, preview panel updates with cached data. If no data available, "No data for preview. Generate full report for complete data."
  • Generate report: Click "Generate Report" → mutation → report queued → navigates to Report Library with new report in "Processing" state → toast "Report '{name}' queued. Check library in 1–5 minutes."
  • Schedule toggle: If "Schedule this report" is toggled ON, additional fields appear: frequency (daily/weekly/monthly) + recipient emails (comma-separated, validated). Report is saved as a scheduled template and run automatically.
  • Form validation: Real-time validation. Name required, at least 1 source, at least 1 metric. Red borders + messages on invalid fields.
  • Impersonation: Read-only. Form viewable but all fields disabled. "Generate" button hidden. Cannot create or schedule reports.

9. Error States#

  • Loading: Skeleton template cards (5) + form fields disabled until loaded.
  • Preview fail: "Preview unavailable. Selected metrics may require full report generation."
  • Generate fail: Toast "Report generation failed. Dataset too large. Try narrowing time range or reducing metrics."
  • No data: If selected metrics have no data for the time range, preview shows "No data for this period."
  • Template fail: If template pre-fill fails, toast "Template data unavailable. Build report manually."
  • Schedule fail: If email recipients invalid, inline red text: "Invalid email: {email}".
  • Concurrent generation: If admin already has 3 reports processing, toast: "Maximum 3 concurrent reports. Wait for one to complete."
  • No permission: Non-ADMIN redirected.

10. Role-Based Variations#

  • ADMIN: Full access — build, generate, schedule, use templates, preview.
  • Other roles: No access.
  • Impersonation: Read-only. Form viewable but disabled. No generation. No scheduling. Templates viewable.

13.2 Scheduled Reports#

1. Purpose#

Manage recurring reports that run automatically on a schedule (daily, weekly, monthly). Admins create, edit, pause, and delete scheduled reports. View delivery history and failure logs.

2. Visual Layout#

Section A: Scheduled Report List (top, full width)

  • Table: Name | Schedule | Last Run | Next Run | Status | Recipients | Actions
  • Name: Report name + description tooltip on hover.
  • Schedule: Badge + frequency. "📅 Daily at 08:00 IST" or "📅 Weekly (Mon) at 09:00 IST" or "📅 Monthly (1st) at 07:00 IST".
  • Last Run: Relative time + status. "2 hr ago — Succeeded" (green) or "1 day ago — Failed" (red). Click → opens run history.
  • Next Run: "Tomorrow at 08:00 IST" or "In 2 days" or "—" (if paused).
  • Status: ACTIVE (green badge, toggle ON) or PAUSED (gray badge, toggle OFF). Toggleable inline.
  • Recipients: Email pills, truncated if > 3. "admin@rankflow.ai, board@rankflow.ai, +2 more". Hover tooltip shows full list.
  • Actions: 3-dot menu:
    • "Run Now" → triggers immediate execution
    • "Edit" → opens report builder pre-filled with this report's config
    • "Duplicate" → creates copy with "(Copy)" suffix
    • "View History" → navigates to run history for this report
    • "Delete" → confirmation → removes schedule

Section B: Run History (below, full width, collapsible)

  • Click "View History" on any row → expands below showing:
  • Table: Run Time | Status | Duration | Size | Download | Error
  • Run Time: Relative timestamp. "2 hr ago", "Yesterday at 08:00".
  • Status: SUCCEEDED (green), FAILED (red), CANCELLED (gray).
  • Duration: "{N} min {N} sec" or "{N} sec" for fast reports.
  • Size: "{N} MB" or "{N} KB" (PDF/CSV size).
  • Download: "Download PDF" / "Download CSV" links if SUCCEEDED. "—" if FAILED.
  • Error: Error message truncated to 50 chars. Full message on hover or in detail modal.
  • Pagination: 20 per page. Default shows last 10 runs.

Section C: Schedule Stats (above table)

  • Horizontal row: "Active Schedules: {N}" | "Paused: {N}" | "Succeeded Today: {N}" | "Failed Today: {N}" | "Avg Generation Time: {N} min"
  • "Failed Today" in red if > 0.

3. Data Source (tRPC endpoint)#

admin.getScheduledReports.useQuery(undefined, { refetchInterval: 60000 }); // 1min standard
admin.getReportRunHistory.useQuery({
  reportId: z.string().uuid(),
  page: z.number().min(1).default(1),
  pageSize: z.number().min(1).max(50).default(20),
}, { enabled: false }); // manual trigger when expanded

admin.toggleScheduledReport.useMutation({ reportId: z.string().uuid(), enabled: z.boolean() });
admin.runScheduledReportNow.useMutation({ reportId: z.string().uuid() });
admin.deleteScheduledReport.useMutation({ reportId: z.string().uuid() });

4. Zod Schema#

const ScheduledReportSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  description: z.string().optional(),
  schedule: z.object({
    frequency: z.enum(["DAILY", "WEEKLY", "MONTHLY"]),
    dayOfWeek: z.number().min(0).max(6).optional(), // 0=Sunday for weekly
    dayOfMonth: z.number().min(1).max(31).optional(), // for monthly
    time: z.string(), // "08:00" in UTC
    timezone: z.string().default("UTC"),
  }),
  lastRunAt: z.date().optional(),
  lastRunStatus: z.enum(["SUCCEEDED", "FAILED", "CANCELLED"]).optional(),
  lastRunError: z.string().optional(),
  nextRunAt: z.date().optional(),
  status: z.enum(["ACTIVE", "PAUSED"]),
  recipients: z.array(z.string().email()),
  createdAt: z.date(),
  updatedAt: z.date(),
});

const ReportRunHistoryItemSchema = z.object({
  id: z.string().uuid(),
  reportId: z.string().uuid(),
  runAt: z.date(),
  status: z.enum(["SUCCEEDED", "FAILED", "CANCELLED"]),
  durationSeconds: z.number(),
  sizeBytes: z.number().optional(),
  pdfUrl: z.string().url().optional(),
  csvUrl: z.string().url().optional(),
  errorMessage: z.string().optional(),
});

const ScheduledReportListResponseSchema = z.object({
  items: z.array(ScheduledReportSchema),
  total: z.number(),
  activeCount: z.number(),
  pausedCount: z.number(),
  succeededToday: z.number(),
  failedToday: z.number(),
  avgGenerationMinutes: z.number().optional(),
});

5. Fetch Frequency#

  • Standard (60000ms / 1min): Schedule status changes (active/paused) and last run times update. 1min polling.
  • On-demand: Toggle status, run now, delete, expand history, download.
  • SSE: When a scheduled report completes (success or fail), push update to table. Next run time updates automatically.

6. Data Manipulations#

  • Schedule formatting:
    • DAILY at 08:00 UTC → "Daily at 08:00 UTC (2:30 PM IST)"
    • WEEKLY on Monday at 09:00 UTC → "Weekly (Mon) at 09:00 UTC"
    • MONTHLY on 1st at 07:00 UTC → "Monthly (1st) at 07:00 UTC"
  • Last run: Relative time + status badge. If FAILED, red badge with error tooltip.
  • Next run: If ACTIVE, computed next occurrence. If PAUSED, "—".
  • Recipients: Email pills. Truncated to 3 + "+{N} more". Hover shows full list.
  • Status toggle: Inline toggle switch. Active = green. Paused = gray. Toggle → mutation. If failed, toggle reverts.
  • Run history: Expandable per row. Last 10 runs shown inline. Click "View All" → full history modal with pagination.
  • Download links: If run succeeded, PDF and CSV download links. Expire after 7 days. If expired, "Download expired. Run again."
  • Stats: "Succeeded Today" counts runs with status: SUCCEEDED and runAt today. "Failed Today" same for FAILED.
  • Avg generation time: (sum of durationSeconds) / count / 60 → minutes. Only for SUCCEEDED runs.

7. Rationale#

  • Scheduled reports: Automation. The admin sets it once, and the report generates and emails itself every Monday morning. Zero manual work.
  • Inline toggle: Pausing a schedule is common (e.g., "pause during holidays" or "pause while we fix a data bug"). One-click toggle without opening an edit form.
  • Run history: Visibility into report health. If a report has failed 3 times in a row, it's broken. Admin sees the pattern and investigates. Error messages in history help debug.
  • Run now: If a scheduled report is needed early (e.g., board meeting moved up), "Run Now" triggers it immediately without waiting for the schedule.
  • 1min polling: Next run times are computed (not real-time). 1min is sufficient. When a report actually runs, SSE pushes the completion status.
  • Recipient pills: Email list can be long. Pills with truncation keep the table readable. Hover shows full list.
  • Download expiry: Report files contain sensitive data. Auto-expiring after 7 days prevents indefinite exposure. Admin can re-run if needed.
  • Stats bar: "Failed Today" in red draws attention to broken reports. If it's 0, the system is healthy. If it's > 0, action needed.
  • Duplicate: If a report is almost right but needs a small tweak, duplicate and edit. Faster than rebuilding from scratch.

8. Interaction Flows#

  • Toggle schedule: Click status toggle → immediate UI change → mutation → toast "Schedule {enabled/paused}. Next run: {time} or '—'."
  • Run now: Click "Run Now" in menu → confirmation: "Run '{name}' immediately? This will queue a report generation." → "Run" → toast "Report queued. Check history in 1–5 minutes." → history table updates with new row (PROCESSING).
  • View history: Click "View History" → row expands with last 10 runs → click "View All" → modal with full pagination.
  • Download: Click "Download PDF" or "Download CSV" in history → browser downloads. If expired, "Download expired. Run again or wait for next scheduled run."
  • Edit: Click "Edit" → opens Report Builder with form pre-filled → modify → "Save Schedule" → updates existing schedule (doesn't create new).
  • Duplicate: Click "Duplicate" → new schedule created with "(Copy)" suffix → toast "Schedule duplicated. Edit as needed." → row added to table.
  • Delete: Click "Delete" → confirmation: "Delete schedule for '{name}'? This will stop all future reports. Past reports remain in history." → "Delete" → row removed → toast "Schedule deleted."
  • Impersonation: Read-only. Toggle, run now, edit, duplicate, delete all disabled. History viewable. Downloads disabled.

9. Error States#

  • Loading: Skeleton table (8 rows) + skeleton stats.
  • Empty: "No scheduled reports. Create a scheduled report from the Report Builder or use a template."
  • Toggle fail: Toast "Failed to toggle schedule. Report may be running." → toggle reverts.
  • Run now fail: Toast "Failed to queue report. Maximum concurrent reports reached. Wait for existing reports to complete."
  • History fail: If history can't load, "History unavailable. Report may have been deleted."
  • Download fail: Toast "Download failed. File may have expired. Re-run the report."
  • All failed: If all schedules failed today, red banner: "⚠️ All scheduled reports failed today. Check system health and report configuration."
  • No permission: Non-ADMIN redirected.

10. Role-Based Variations#

  • ADMIN: Full access — toggle, run now, edit, duplicate, delete, view history, download.
  • Other roles: No access.
  • Impersonation: Read-only. Table viewable. History viewable. No actions. No downloads.

13.3 Report Library#

1. Purpose#

Browse, search, and download all generated reports (one-time and scheduled). The archive of all past reports. Admins find old reports by name, date, type, or metric.

2. Visual Layout#

Section A: Search & Filters (top, full width)

  • Search: "Search by report name, metric, or client"
  • Type filter: Multi-select (One-time, Scheduled, Template)
  • Source filter: Multi-select (Revenue, Content, Marketing, Operations, etc.)
  • Date range: "Last 7 days", "Last 30 days", "Last 90 days", "Custom"
  • Status filter: Multi-select (READY, PROCESSING, FAILED)
  • Sort: "Date", "Name", "Size" (dropdown)
  • "Export List" button (secondary, CSV of report metadata)

Section B: Report Grid (below, full width)

  • Card grid (3 columns desktop, 2 tablet, 1 mobile). Each card:
    • Icon: Document icon (PDF) or spreadsheet icon (CSV) or chart icon (Charts + Table).
    • Name: Report name, truncated to 2 lines. text-base text-primary.
    • Meta: text-sm text-muted. "Generated {relative time} · {N} MB · {source count} sources"
    • Status badge: READY (green), PROCESSING (amber + spinner), FAILED (red).
    • Preview thumbnail: If report includes charts, a tiny thumbnail (200px wide) of the first chart. If table-only, a mini table preview (first 3 rows). If processing, skeleton thumbnail.
    • Actions: "Download PDF" (primary, Ember) + "Download CSV" (secondary) + "View" (opens report viewer modal) + "Delete" (trash icon, danger).
  • PROCESSING cards: Skeleton chart + amber badge + "Processing... {N}%" (if progress available). No download buttons. "Cancel" button if admin wants to abort.
  • FAILED cards: Red badge + "Failed: {error preview}" + "Retry" button.

Section C: Report Viewer Modal

  • Click "View" on any READY card → modal:
    • Full report rendered in modal (scrollable, max-height 80vh).
    • If executive summary: narrative text with highlighted stats.
    • If charts + table: charts first, then table below.
    • If table only: full data table with sorting and pagination (50 rows per page within modal).
    • "Download PDF" and "Download CSV" buttons at top (sticky).
    • Report metadata sidebar: generated by, generated at, data sources, time range, filters used.
    • "Share" button: generates a temporary shareable link (expires in 24h) for sending to non-admin stakeholders.

3. Data Source (tRPC endpoint)#

admin.getReportLibrary.useQuery({
  search: z.string().optional(),
  types: z.array(z.enum(["ONE_TIME", "SCHEDULED", "TEMPLATE"])).optional(),
  sources: z.array(ReportSourceSchema).optional(),
  dateRange: z.object({ from: z.date().optional(), to: z.date().optional() }).optional(),
  statuses: z.array(z.enum(["READY", "PROCESSING", "FAILED"])).optional(),
  sortBy: z.enum(["DATE", "NAME", "SIZE"]).default("DATE"),
  sortOrder: z.enum(["ASC", "DESC"]).default("DESC"),
  page: z.number().min(1).default(1),
  pageSize: z.number().min(1).max(50).default(24),
}, { refetchInterval: 30000 }); // 30s frequent

admin.getReportDetail.useQuery({
  reportId: z.string().uuid(),
}, { enabled: false }); // manual trigger

admin.deleteReport.useMutation({ reportId: z.string().uuid() });
admin.cancelReport.useMutation({ reportId: z.string().uuid() }); // for PROCESSING reports
admin.shareReport.useMutation({ reportId: z.string().uuid(), expiresInHours: z.number().min(1).max(72).default(24) });

4. Zod Schema#

const ReportTypeSchema = z.enum(["ONE_TIME", "SCHEDULED", "TEMPLATE"]);

const ReportStatusSchema = z.enum(["READY", "PROCESSING", "FAILED"]);

const ReportLibraryItemSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  type: ReportTypeSchema,
  sources: z.array(ReportSourceSchema),
  status: ReportStatusSchema,
  generatedAt: z.date().optional(),
  generatedBy: z.string().optional(),
  sizeBytes: z.number().optional(),
  pdfUrl: z.string().url().optional(),
  csvUrl: z.string().url().optional(),
  thumbnailUrl: z.string().url().optional(),
  errorMessage: z.string().optional(),
  progressPercent: z.number().min(0).max(100).optional(), // for PROCESSING
  timeRange: z.object({ from: z.date(), to: z.date() }).optional(),
});

const ReportDetailSchema = z.object({
  report: ReportLibraryItemSchema,
  content: z.object({
    executiveSummary: z.string().optional(),
    charts: z.array(z.object({
      title: z.string(),
      type: z.enum(["LINE", "BAR", "PIE", "AREA", "TABLE"]),
      data: z.record(z.any()),
    })).optional(),
    tables: z.array(z.object({
      title: z.string(),
      headers: z.array(z.string()),
      rows: z.array(z.array(z.any())),
    })).optional(),
  }),
  metadata: z.object({
    sources: z.array(ReportSourceSchema),
    metrics: z.array(z.string()),
    timeRange: z.object({ from: z.date(), to: z.date() }),
    filters: z.record(z.any()).optional(),
    generatedAt: z.date(),
    generatedBy: z.string(),
    durationSeconds: z.number(),
  }),
});

const ReportLibraryResponseSchema = z.object({
  items: z.array(ReportLibraryItemSchema),
  total: z.number(),
  hasMore: z.boolean(),
});

5. Fetch Frequency#

  • Frequent (30000ms / 30s): Report statuses change (PROCESSING → READY/FAILED). 30s polling catches transitions.
  • On-demand: Filter, search, sort, pagination, view detail, download, delete, share.
  • SSE: When a report completes, push to library (status change + download links). When a new report is generated, prepend to grid.

6. Data Manipulations#

  • Status badge colors: READY = green; PROCESSING = amber + spinner; FAILED = red.
  • Size formatting: < 1024 KB → "{N} KB"; < 1024 MB → "{N} MB"; >= 1024 MB → "{N} GB".
  • Thumbnail: Generated server-side during report creation. 200x150px PNG. For chart reports, shows first chart. For table reports, shows first 3 rows as a mini table. For processing, skeleton.
  • Relative time: "Generated 2 hr ago", "Yesterday", "3 days ago".
  • Card grid: 3 columns on desktop. Cards are fixed height (300px) with scrollable content if report name is long. Consistent grid prevents layout shifts.
  • PROCESSING progress: If progress data available, show progress bar in card: "Processing... 45%". If not, indeterminate spinner.
  • Source count: Meta line shows number of data sources. "3 sources" means Revenue + Content + Operations.
  • Type badge: Small badge on card: "One-time", "Scheduled", "Template". Different colors: One-time = blue, Scheduled = green, Template = gray.
  • Share link: Temporary URL with token. Expires in 24h (configurable). No auth required for link. Link leads to read-only report viewer. Admin can revoke link early.
  • Modal viewer: Report rendered in modal. Charts are interactive (Recharts). Tables are sortable and paginated. Sticky download buttons at top.
  • Metadata sidebar: Shows data provenance. "Generated by John Doe on Jun 15, 2026 at 14:32. Sources: Revenue, Content. Time range: May 1–May 31, 2026. Filters: Plan = Growth." Transparency and reproducibility.

7. Rationale#

  • Report library: Archive of all reports. If a board member asks "what was Q1 revenue?" and the admin already generated the report, it's in the library. No need to regenerate.
  • Card grid with thumbnails: Visual browsing. Thumbnails let admin spot the right report quickly. "That one with the green line chart — that's the revenue report."
  • 30s polling for processing: When an admin clicks "Generate Report," they want to know when it's done. 30s polling shows the status transition. The card changes from amber spinner to green download button.
  • Shareable links: Board members and investors don't have admin accounts. A shareable link lets them view the report without login. Auto-expires for security.
  • Modal viewer: Preview before downloading. If the report looks wrong, admin can delete it and regenerate without downloading a broken PDF.
  • Cancel processing: If a report was generated with wrong parameters, admin can cancel it while processing. Saves compute and storage.
  • Source count on card: Quick filter. If admin is looking for a revenue report, they can see at a glance which cards have revenue as a source.
  • Type differentiation: One-time reports are ad-hoc. Scheduled reports are recurring. Templates are blueprints. Knowing the type tells admin how the report was created.
  • Metadata sidebar: Data provenance is critical for trust. "This report says MRR is $12,450. Was that from all plans or just Pro? The metadata tells me."
  • Search: Report names + metric names + source names are all searchable. If admin can't remember the report name, they can search "churn" or "revenue."
  • Export list: CSV of all report metadata (not report content). Useful for auditing what reports were generated and when.

8. Interaction Flows#

  • Filter: Change status filter to "FAILED" → only failed reports shown. Change type to "Scheduled" → only scheduled reports shown. URL updates with filters.
  • Search: Type "revenue" → cards filter live (debounced 300ms). Only reports with "revenue" in name or sources shown.
  • View report: Click "View" → modal opens with full report content → scroll through charts and tables → click "Download PDF" or "Download CSV".
  • Download: Click "Download PDF" on card → browser downloads. Filename: {reportName}_{YYYYMMDD}.pdf.
  • Share: Click "Share" in modal → generates link → shows link in copyable field with "Copy" button + expiry timer "Expires in 24h" → "Revoke" button to invalidate early.
  • Delete: Click trash on card → confirmation: "Delete report '{name}'? PDF and CSV will be removed." → "Delete" → card removed with fade-out → toast "Report deleted."
  • Cancel processing: Click "Cancel" on PROCESSING card → confirmation: "Cancel report generation? Partial data will be discarded." → "Cancel" → status changes to FAILED → toast "Report cancelled."
  • Retry failed: Click "Retry" on FAILED card → re-queues report generation with same parameters → status changes to PROCESSING → toast "Report re-queued."
  • Impersonation: Read-only. Cards viewable. Modal viewer accessible. Downloads disabled. Share disabled. Delete and cancel disabled.

9. Error States#

  • Loading: Skeleton card grid (9 cards with shimmer).
  • Empty (filtered): "No reports match your filters. Try adjusting date range or status."
  • Empty (no reports): "No reports generated yet. Create your first report in the Report Builder."
  • View fail: If report content can't load, modal shows "Report content unavailable. File may have been corrupted or expired."
  • Download fail: Toast "Download failed. File may have expired. Re-generate the report."
  • Share fail: Toast "Share link generation failed. Report may be too large or processing."
  • Delete fail: Toast "Delete failed. Report may be in use by another process."
  • Cancel fail: Toast "Cancel failed. Report may have already completed."
  • No permission: Non-ADMIN redirected.

10. Role-Based Variations#

  • ADMIN: Full access — view, filter, search, download, share, delete, cancel, retry.
  • Other roles: No access.
  • Impersonation: Read-only. Cards viewable. Modal viewer accessible. No downloads, no sharing, no deletion, no cancellation.