Compliance.jsx
1 import { useQuery } from '@tanstack/react-query'; 2 import { fetchCompliance } from '../api'; 3 import MetricCard from '../components/MetricCard'; 4 import DataTable from '../components/DataTable'; 5 import Tabs from '../components/Tabs'; 6 import { BarChart, LineChart, PieChart } from '../components/charts'; 7 8 const FEEDBACK_COLS = [ 9 { accessorKey: 'category', header: 'Category' }, 10 { accessorKey: 'count', header: 'Count' }, 11 { accessorKey: 'avg_score', header: 'Avg Score', cell: i => i.getValue()?.toFixed(2) ?? '—' }, 12 ]; 13 14 const OPTOUT_COLS = [ 15 { accessorKey: 'channel', header: 'Channel' }, 16 { accessorKey: 'count', header: 'Opt-outs' }, 17 { accessorKey: 'pct', header: '%', cell: i => `${(i.getValue() ?? 0).toFixed(1)}%` }, 18 ]; 19 20 const PLATFORM_COLS = [ 21 { accessorKey: 'platform', header: 'Platform' }, 22 { accessorKey: 'sent', header: 'Sent' }, 23 { accessorKey: 'delivered', header: 'Delivered' }, 24 { accessorKey: 'failed', header: 'Failed' }, 25 { 26 accessorKey: 'delivery_rate', 27 header: 'Delivery %', 28 cell: i => `${(i.getValue() ?? 0).toFixed(1)}%`, 29 }, 30 ]; 31 32 const VERSION_COLS = [ 33 { accessorKey: 'version', header: 'Version' }, 34 { accessorKey: 'approved', header: 'Approved' }, 35 { accessorKey: 'rejected', header: 'Rejected' }, 36 { accessorKey: 'pending', header: 'Pending' }, 37 { 38 accessorKey: 'approval_rate', 39 header: 'Approval %', 40 cell: i => `${(i.getValue() ?? 0).toFixed(1)}%`, 41 }, 42 ]; 43 44 export default function Compliance() { 45 const { data, isLoading, error } = useQuery({ 46 queryKey: ['compliance'], 47 queryFn: fetchCompliance, 48 }); 49 50 if (isLoading) return <div className="text-slate-400 p-8">Loading…</div>; 51 if (error) return <div className="text-red-400 p-8">Error: {error.message}</div>; 52 53 const d = data ?? {}; 54 const os = d.optout_stats ?? {}; 55 const ph = d.platform_health ?? []; 56 const as_ = d.approval_stats ?? {}; 57 const fbc = d.feedback_by_category ?? []; 58 const at = d.approval_trend ?? []; 59 const rf = d.recent_feedback ?? []; 60 const pv = d.prompt_versions ?? []; 61 62 return ( 63 <div className="space-y-6"> 64 <Tabs tabs={['Legal', 'Prompt Learning']}> 65 {active => 66 active === 'Legal' ? ( 67 <div className="space-y-6"> 68 {/* Opt-out metrics */} 69 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 70 <MetricCard label="Total Opt-outs" value={os.total ?? 0} /> 71 <MetricCard label="Email Opt-outs" value={os.email ?? 0} variant="warning" /> 72 <MetricCard label="SMS Opt-outs" value={os.sms ?? 0} variant="warning" /> 73 <MetricCard label="Form Opt-outs" value={os.form ?? 0} /> 74 </div> 75 76 {/* Opt-out breakdown */} 77 {(d.optout_breakdown ?? []).length > 0 && ( 78 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 79 <h3 className="text-sm font-medium text-slate-400 mb-3">Opt-outs by Channel</h3> 80 <DataTable columns={OPTOUT_COLS} data={d.optout_breakdown ?? []} /> 81 </div> 82 )} 83 84 {/* Platform delivery health */} 85 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 86 <h3 className="text-sm font-medium text-slate-400 mb-3"> 87 Platform Delivery Health 88 </h3> 89 <DataTable columns={PLATFORM_COLS} data={ph} /> 90 </div> 91 92 {/* CAN-SPAM / TCPA checklist */} 93 <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 94 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 95 <h3 className="text-sm font-medium text-slate-400 mb-3">CAN-SPAM Checklist</h3> 96 <ul className="space-y-2 text-sm"> 97 {[ 98 'Sender ID in all emails', 99 'Physical address included', 100 'Unsubscribe link in footer', 101 'Opt-outs honoured within 10 days', 102 'No deceptive subject lines', 103 ].map(item => ( 104 <li key={item} className="flex items-center gap-2 text-emerald-400"> 105 <span>✓</span> 106 <span className="text-slate-300">{item}</span> 107 </li> 108 ))} 109 </ul> 110 </div> 111 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 112 <h3 className="text-sm font-medium text-slate-400 mb-3">TCPA Checklist</h3> 113 <ul className="space-y-2 text-sm"> 114 {[ 115 'STOP opt-out instruction in SMS (US/CA only)', 116 'Business hours enforcement (8am–9pm)', 117 'Sender ID in all SMS', 118 'No automated calls to reassigned numbers', 119 'Rate: 1 SMS per 3 days per contact', 120 ].map(item => ( 121 <li key={item} className="flex items-center gap-2 text-emerald-400"> 122 <span>✓</span> 123 <span className="text-slate-300">{item}</span> 124 </li> 125 ))} 126 </ul> 127 </div> 128 </div> 129 </div> 130 ) : ( 131 <div className="space-y-6"> 132 {/* Approval metrics */} 133 <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 134 <MetricCard 135 label="Approval Rate" 136 value={`${(as_.approval_rate ?? 0).toFixed(1)}%`} 137 variant={(as_.approval_rate ?? 0) >= 80 ? 'success' : 'warning'} 138 /> 139 <MetricCard label="Approved" value={as_.approved ?? 0} variant="success" /> 140 <MetricCard label="Rejected" value={as_.rejected ?? 0} variant="error" /> 141 <MetricCard label="Pending Review" value={as_.pending ?? 0} variant="warning" /> 142 </div> 143 144 {/* Approval trend */} 145 {at.length > 0 && ( 146 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 147 <h3 className="text-sm font-medium text-slate-400 mb-3"> 148 Approval Trend (30 days) 149 </h3> 150 <LineChart 151 data={at} 152 xKey="date" 153 lines={[ 154 { key: 'approved', name: 'Approved', color: '#34d399' }, 155 { key: 'rejected', name: 'Rejected', color: '#f87171' }, 156 ]} 157 /> 158 </div> 159 )} 160 161 {/* Feedback by category */} 162 {fbc.length > 0 && ( 163 <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 164 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 165 <h3 className="text-sm font-medium text-slate-400 mb-3"> 166 Feedback by Category 167 </h3> 168 <PieChart data={fbc} nameKey="category" valueKey="count" /> 169 </div> 170 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 171 <h3 className="text-sm font-medium text-slate-400 mb-3">Category Breakdown</h3> 172 <DataTable columns={FEEDBACK_COLS} data={fbc} /> 173 </div> 174 </div> 175 )} 176 177 {/* Prompt version history */} 178 {pv.length > 0 && ( 179 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 180 <h3 className="text-sm font-medium text-slate-400 mb-3"> 181 Prompt Version History 182 </h3> 183 <DataTable columns={VERSION_COLS} data={pv} /> 184 </div> 185 )} 186 187 {/* Recent feedback */} 188 {rf.length > 0 && ( 189 <div className="bg-slate-800 rounded-xl p-4 border border-slate-700"> 190 <h3 className="text-sm font-medium text-slate-400 mb-3">Recent Feedback</h3> 191 <div className="space-y-2"> 192 {rf.slice(0, 10).map((f, i) => ( 193 <div 194 key={i} 195 className="text-sm text-slate-300 bg-slate-900 rounded p-2 border border-slate-700" 196 > 197 <span className="text-slate-500 text-xs mr-2"> 198 {f.created_at?.slice(0, 10)} 199 </span> 200 {f.feedback} 201 </div> 202 ))} 203 </div> 204 </div> 205 )} 206 </div> 207 ) 208 } 209 </Tabs> 210 </div> 211 ); 212 }