Files
pn-new-crm/src/components/reports/generate-report-form.tsx
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
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
2026-05-23 00:52:59 +02:00

187 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 interests 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>
);
}