feat(report): form component with all config fields
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import React, { useRef } from 'react';
|
||||
import AltMultiSelect from '../shared/AltMultiSelect';
|
||||
import type { ReportConfig } from './reportHelpers';
|
||||
|
||||
interface Props {
|
||||
config: ReportConfig;
|
||||
onChange: (patch: Partial<ReportConfig>) => void;
|
||||
allMuseums: string[];
|
||||
allChannels: string[];
|
||||
}
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return <div className="rf-section-title">{children}</div>;
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<label className="rf-field">
|
||||
<span className="rf-label">{label}</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function Toggle({ left, right, value, onChange }: {
|
||||
left: string; right: string; value: boolean; onChange: (v: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="rf-toggle">
|
||||
<button type="button" className={`rf-toggle-opt${!value ? ' rf-toggle-opt--on' : ''}`} onClick={() => onChange(false)}>{left}</button>
|
||||
<button type="button" className={`rf-toggle-opt${value ? ' rf-toggle-opt--on' : ''}`} onClick={() => onChange(true)}>{right}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckRow({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
|
||||
return (
|
||||
<label className="rf-check-row">
|
||||
<input type="checkbox" checked={checked} onChange={e => onChange(e.target.checked)} className="rf-checkbox" />
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReportForm({ config: cfg, onChange, allMuseums, allChannels }: Props) {
|
||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
if (file.size > 2 * 1024 * 1024) { alert('Logo must be under 2 MB'); return; }
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => onChange({ clientLogoBase64: reader.result as string });
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="report-form">
|
||||
|
||||
<SectionTitle>Client Info</SectionTitle>
|
||||
|
||||
<Field label="Report title">
|
||||
<input className="rf-input" type="text" value={cfg.title}
|
||||
onChange={e => onChange({ title: e.target.value })}
|
||||
placeholder="Q1 2025 Visitor Performance" />
|
||||
</Field>
|
||||
|
||||
<Field label="Prepared for (company)">
|
||||
<input className="rf-input" type="text" value={cfg.clientName}
|
||||
onChange={e => onChange({ clientName: e.target.value })}
|
||||
placeholder="Acme Group" />
|
||||
</Field>
|
||||
|
||||
<Field label="Contact name (optional)">
|
||||
<input className="rf-input" type="text" value={cfg.contactName}
|
||||
onChange={e => onChange({ contactName: e.target.value })}
|
||||
placeholder="Mohammed Al-..." />
|
||||
</Field>
|
||||
|
||||
<Field label="Client logo (PNG/JPG, max 2 MB)">
|
||||
<div className="rf-logo-row">
|
||||
<button type="button" className="rf-upload-btn" onClick={() => logoInputRef.current?.click()}>
|
||||
{cfg.clientLogoBase64 ? 'Change logo' : 'Upload logo'}
|
||||
</button>
|
||||
{cfg.clientLogoBase64 && (
|
||||
<>
|
||||
<img src={cfg.clientLogoBase64} alt="preview" className="rf-logo-preview" />
|
||||
<button type="button" className="rf-remove-btn" onClick={() => onChange({ clientLogoBase64: null })}>✕</button>
|
||||
</>
|
||||
)}
|
||||
<input ref={logoInputRef} type="file" accept="image/png,image/jpeg,image/svg+xml"
|
||||
style={{ display: 'none' }} onChange={handleLogoUpload} />
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field label="Accent color">
|
||||
<div className="rf-color-row">
|
||||
<input type="color" value={cfg.accentColor}
|
||||
onChange={e => onChange({ accentColor: e.target.value })}
|
||||
className="rf-color-input" />
|
||||
<span className="rf-color-val">{cfg.accentColor}</span>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<SectionTitle>Data Selection</SectionTitle>
|
||||
|
||||
<div className="rf-date-row">
|
||||
<Field label="Start date">
|
||||
<input className="rf-input" type="date" value={cfg.startDate}
|
||||
onChange={e => onChange({ startDate: e.target.value })} />
|
||||
</Field>
|
||||
<Field label="End date">
|
||||
<input className="rf-input" type="date" value={cfg.endDate}
|
||||
onChange={e => onChange({ endDate: e.target.value })} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field label="Museums">
|
||||
<AltMultiSelect value={cfg.selectedMuseums} options={allMuseums}
|
||||
onChange={v => onChange({ selectedMuseums: v })}
|
||||
allLabel="All museums" countLabel={n => `${n} museums`} clearLabel="Clear" />
|
||||
</Field>
|
||||
|
||||
<Field label="Channels">
|
||||
<AltMultiSelect value={cfg.selectedChannels} options={allChannels}
|
||||
onChange={v => onChange({ selectedChannels: v })}
|
||||
allLabel="All channels" countLabel={n => `${n} channels`} clearLabel="Clear" />
|
||||
</Field>
|
||||
|
||||
<Field label="VAT">
|
||||
<Toggle left="Excl. VAT" right="Incl. VAT" value={cfg.includeVAT}
|
||||
onChange={v => onChange({ includeVAT: v })} />
|
||||
</Field>
|
||||
|
||||
<CheckRow label="Include previous year comparison"
|
||||
checked={cfg.includeComparison} onChange={v => onChange({ includeComparison: v })} />
|
||||
|
||||
<SectionTitle>Content Sections</SectionTitle>
|
||||
|
||||
<CheckRow label="Executive summary" checked={cfg.showExecutiveSummary} onChange={v => onChange({ showExecutiveSummary: v })} />
|
||||
<CheckRow label="Key metrics table" checked={cfg.showMetricsTable} onChange={v => onChange({ showMetricsTable: v })} />
|
||||
<CheckRow label="Revenue trend chart" checked={cfg.showTrendChart} onChange={v => onChange({ showTrendChart: v })} />
|
||||
<CheckRow label="Breakdown by museum" checked={cfg.showMuseumBreakdown} onChange={v => onChange({ showMuseumBreakdown: v })} />
|
||||
<CheckRow label="Breakdown by channel" checked={cfg.showChannelBreakdown} onChange={v => onChange({ showChannelBreakdown: v })} />
|
||||
<CheckRow label="Pilgrim capture rate" checked={cfg.showPilgrimCapture} onChange={v => onChange({ showPilgrimCapture: v })} />
|
||||
|
||||
<SectionTitle>Presentation</SectionTitle>
|
||||
|
||||
<Field label="Language">
|
||||
<Toggle left="English" right="العربية" value={cfg.language === 'ar'}
|
||||
onChange={v => onChange({ language: v ? 'ar' : 'en' })} />
|
||||
</Field>
|
||||
|
||||
<Field label="Orientation">
|
||||
<Toggle left="Portrait" right="Landscape" value={cfg.orientation === 'landscape'}
|
||||
onChange={v => onChange({ orientation: v ? 'landscape' : 'portrait' })} />
|
||||
</Field>
|
||||
|
||||
<Field label="Confidentiality">
|
||||
<select className="rf-input" value={cfg.confidentiality}
|
||||
onChange={e => onChange({ confidentiality: e.target.value as ReportConfig['confidentiality'] })}>
|
||||
<option value="Confidential">Confidential</option>
|
||||
<option value="Internal">Internal</option>
|
||||
<option value="Public">Public</option>
|
||||
</select>
|
||||
</Field>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user