Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
187 lines
6.2 KiB
TypeScript
187 lines
6.2 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { DatePicker } from '@/components/ui/date-picker';
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from '@/components/ui/select';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { apiFetch } from '@/lib/api/client';
|
||
import type { RequestReportInput } from '@/lib/validators/reports';
|
||
|
||
interface ReportTypeMeta {
|
||
label: string;
|
||
subtitle: string;
|
||
contents: string[];
|
||
}
|
||
|
||
const REPORT_TYPES: Record<string, ReportTypeMeta> = {
|
||
pipeline: {
|
||
label: 'Pipeline Summary',
|
||
subtitle: 'Interest counts by stage and conversion rates',
|
||
contents: [
|
||
'Active (non-archived) interests grouped by pipeline stage',
|
||
'Stage-to-stage drop-off counts',
|
||
'Open vs. won vs. lost roll-up at the bottom',
|
||
],
|
||
},
|
||
revenue: {
|
||
label: 'Revenue Report',
|
||
subtitle: 'Berth-price totals rolled up by pipeline stage',
|
||
contents: [
|
||
'Sum of primary-berth prices grouped by stage',
|
||
'Pulled from each interest’s primary berth link (non-primary junctions ignored)',
|
||
'Sold-stage total reflects realised revenue; earlier stages are forecast',
|
||
],
|
||
},
|
||
activity: {
|
||
label: 'Activity Log',
|
||
subtitle: 'Audit events across the port for a date range',
|
||
contents: [
|
||
'Audit log entries (create / update / delete) per entity',
|
||
'Filtered to the selected date range - defaults to last 30 days',
|
||
'Includes actor name, entity type, and action verb',
|
||
],
|
||
},
|
||
occupancy: {
|
||
label: 'Berth Occupancy',
|
||
subtitle: 'Berth counts by status',
|
||
contents: [
|
||
'Berths grouped by status: Available, Under Offer, Sold',
|
||
'Per-dock breakdown using the mooring-letter prefix',
|
||
'Total port utilisation percentage at the top',
|
||
],
|
||
},
|
||
};
|
||
|
||
export function GenerateReportForm() {
|
||
const queryClient = useQueryClient();
|
||
|
||
const [reportType, setReportType] = useState<string>('');
|
||
const [name, setName] = useState<string>('');
|
||
const [dateFrom, setDateFrom] = useState<string>('');
|
||
const [dateTo, setDateTo] = useState<string>('');
|
||
|
||
const mutation = useMutation({
|
||
mutationFn: (data: RequestReportInput) =>
|
||
apiFetch('/api/v1/reports', { method: 'POST', body: data }),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['reports'] });
|
||
setReportType('');
|
||
setName('');
|
||
setDateFrom('');
|
||
setDateTo('');
|
||
},
|
||
});
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!reportType || !name.trim()) return;
|
||
|
||
const payload: RequestReportInput = {
|
||
reportType: reportType as RequestReportInput['reportType'],
|
||
name: name.trim(),
|
||
parameters: {
|
||
...(dateFrom ? { dateFrom } : {}),
|
||
...(dateTo ? { dateTo } : {}),
|
||
},
|
||
};
|
||
|
||
mutation.mutate(payload);
|
||
};
|
||
|
||
return (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Generate Report</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<form onSubmit={handleSubmit} className="space-y-4">
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="reportType">Report Type</Label>
|
||
<Select value={reportType} onValueChange={setReportType}>
|
||
<SelectTrigger id="reportType">
|
||
<SelectValue placeholder="Select a report type..." />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{Object.entries(REPORT_TYPES).map(([value, meta]) => (
|
||
<SelectItem key={value} value={value} className="py-2">
|
||
<div className="flex flex-col">
|
||
<span className="font-medium">{meta.label}</span>
|
||
<span className="text-xs text-muted-foreground">{meta.subtitle}</span>
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{reportType && REPORT_TYPES[reportType] ? (
|
||
<div className="mt-1 rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||
<p className="font-medium text-foreground">{REPORT_TYPES[reportType].subtitle}</p>
|
||
<ul className="mt-1 list-disc space-y-0.5 pl-4">
|
||
{REPORT_TYPES[reportType].contents.map((line) => (
|
||
<li key={line}>{line}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="name">Report Name</Label>
|
||
<Input
|
||
id="name"
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
placeholder="e.g. Pipeline Summary Q1 2025"
|
||
maxLength={200}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-4">
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dateFrom">Date From (optional)</Label>
|
||
<DatePicker
|
||
id="dateFrom"
|
||
value={dateFrom}
|
||
onChange={setDateFrom}
|
||
className="w-auto"
|
||
/>
|
||
</div>
|
||
<div className="grid gap-2">
|
||
<Label htmlFor="dateTo">Date To (optional)</Label>
|
||
<DatePicker id="dateTo" value={dateTo} onChange={setDateTo} className="w-auto" />
|
||
</div>
|
||
</div>
|
||
|
||
{mutation.isError && (
|
||
<p className="text-sm text-destructive">
|
||
{mutation.error instanceof Error
|
||
? mutation.error.message
|
||
: 'Failed to queue report. Please try again.'}
|
||
</p>
|
||
)}
|
||
|
||
{mutation.isSuccess && (
|
||
<p className="text-sm text-green-600">
|
||
Report queued successfully. You will be notified when it is ready.
|
||
</p>
|
||
)}
|
||
|
||
<Button type="submit" disabled={!reportType || !name.trim() || mutation.isPending}>
|
||
{mutation.isPending ? 'Queuing...' : 'Generate Report'}
|
||
</Button>
|
||
</form>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|