added graph and potentialmining

This commit is contained in:
suryac 2026-08-06 15:44:09 +05:30
parent 0ada5c4735
commit 7835cd3630
5 changed files with 90 additions and 73 deletions

View File

@ -60,6 +60,7 @@ export interface CallDetailProps {
placeOrderAction?: ReactNode; placeOrderAction?: ReactNode;
refreshKey?: number; refreshKey?: number;
onBack?: () => void; onBack?: () => void;
onDataLoad?: (data: Record<string, unknown>) => void;
} }
export function CallDetail({ export function CallDetail({
@ -69,6 +70,7 @@ export function CallDetail({
placeOrderAction, placeOrderAction,
refreshKey, refreshKey,
onBack, onBack,
onDataLoad,
}: CallDetailProps) { }: CallDetailProps) {
const [data, setData] = useState<Record<string, unknown> | null>(null); const [data, setData] = useState<Record<string, unknown> | null>(null);
const [potentialData, setPotentialData] = useState<any[] | null>(null); const [potentialData, setPotentialData] = useState<any[] | null>(null);
@ -84,8 +86,9 @@ export function CallDetail({
const r = await orderBookingClient.detailView(ORDER_BOOKING.detailViews.CALLS, instanceId); const r = await orderBookingClient.detailView(ORDER_BOOKING.detailViews.CALLS, instanceId);
if (live) { if (live) {
setData(r.data || {}); setData(r.data || {});
if (onDataLoad) onDataLoad(r.data || {});
const storeCode = (r.data?.select_store as any)?.store_code || (r.data?.select_store as any)?.code; const storeCode = (r.data?.select_store as any)?.store_code_2 || (r.data?.select_store as any)?.code || (r.data as any)?.store_code || (r.data as any)?.store?.store_code;
if (storeCode) { if (storeCode) {
orderBookingClient.request<{ potential: { potential: any[] } }>( orderBookingClient.request<{ potential: { potential: any[] } }>(
'POST', 'POST',
@ -107,7 +110,7 @@ export function CallDetail({
return () => { return () => {
live = false; live = false;
}; };
}, [instanceId, refreshKey]); }, [instanceId, refreshKey, onDataLoad]);
if (error) { if (error) {
return ( return (

View File

@ -52,7 +52,7 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
if (!data || Object.keys(data).length === 0) { if (!data || Object.keys(data).length === 0) {
return ( return (
<Card title={`Order #${instanceId}`}> <Card title={`Order`}>
<EmptyState title="No details found" /> <EmptyState title="No details found" />
</Card> </Card>
); );
@ -76,9 +76,9 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
const orderId = extract('order_id', remainingData) || '-'; const orderId = extract('order_id', remainingData) || '-';
let dateOfOrder = extract('date_of_order_3', remainingData); let dateOfOrder = extract('date_of_order_3', remainingData);
if (dateOfOrder) { if (dateOfOrder) {
dateOfOrder = new Date(dateOfOrder as string).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); dateOfOrder = new Date(dateOfOrder as string).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
} else { } else {
dateOfOrder = '-'; dateOfOrder = '-';
} }
// SO Info // SO Info
@ -92,7 +92,7 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
soEmail = (soRaw as any).email || '-'; soEmail = (soRaw as any).email || '-';
soId = (soRaw as any).user_id || (soRaw as any).id || (soRaw as any).uid || '-'; soId = (soRaw as any).user_id || (soRaw as any).id || (soRaw as any).uid || '-';
} }
const soInitials = soName.length > 2 ? soName.substring(0,2).toUpperCase() : 'SO'; const soInitials = soName.length > 2 ? soName.substring(0, 2).toUpperCase() : 'SO';
// Store Info // Store Info
const storeRaw = extract('select_store', remainingData); const storeRaw = extract('select_store', remainingData);
@ -117,13 +117,6 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
return ( return (
<div className="flex flex-col gap-6 bg-transparent"> <div className="flex flex-col gap-6 bg-transparent">
{/* Header */}
<div className="flex items-center gap-4">
<div>
<h1 className="text-xl font-semibold tracking-tight text-slate-900">Order Detail</h1>
<p className="text-sm text-slate-500">#{orderId}</p>
</div>
</div>
{/* Status + Date ribbon */} {/* Status + Date ribbon */}
<div className="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-slate-200 bg-[var(--tiles-card-bg)] p-4 shadow-sm"> <div className="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-slate-200 bg-[var(--tiles-card-bg)] p-4 shadow-sm">

View File

@ -47,7 +47,7 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
: "grid grid-cols-1 lg:grid-cols-3 gap-6 w-full"; : "grid grid-cols-1 lg:grid-cols-3 gap-6 w-full";
return ( return (
<div className={gridClass}> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full">
{charts.map((chart, idx) => { {charts.map((chart, idx) => {
const title = (chart.key || `Chart ${idx + 1}`) const title = (chart.key || `Chart ${idx + 1}`)
.replace(/_/g, ' ') .replace(/_/g, ' ')
@ -93,10 +93,20 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
chartType = 4; // Donut chartType = 4; // Donut
} else if (lowerKey.includes('brand') || lowerKey.includes('product')) { } else if (lowerKey.includes('brand') || lowerKey.includes('product')) {
chartType = 5; // Table chartType = 5; // Table
} else if (lowerKey.includes('monthly')) { } else if (lowerKey.includes('monthly') || lowerKey.includes('yearly')) {
chartType = 0; // Bar chartType = 0; // Bar
} }
// Determine if it should span full width
const isFullWidth = (gridCols === 1) || (!gridCols && charts.length <= 2) || hasSeries || lowerKey.includes('monthly') || lowerKey.includes('yearly') || finalData.length > 8;
const fullSpanClass = gridCols === 2 ? 'lg:col-span-2' : 'lg:col-span-3';
// Calculate a dynamic width if there's a lot of data to allow horizontal scrolling
const minChartWidth = chartType === 0 || chartType === 1 || chartType === 2
? Math.max(100, finalData.length * (hasSeries ? seriesKeys.length * 15 + 40 : 60))
: 100;
const renderChartContent = () => { const renderChartContent = () => {
return ( return (
<> <>
@ -177,8 +187,8 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
}; };
return ( return (
<Card key={chart.chart_uid || idx} title={title} className="shadow-sm border border-gray-100 !bg-[var(--tiles-card-bg)]"> <Card key={chart.chart_uid || idx} title={title} className={`shadow-sm border border-gray-100 !bg-[var(--tiles-card-bg)] ${isFullWidth ? fullSpanClass : ''}`}>
<div className="h-[320px] w-full mt-4"> <div className={`h-[320px] w-full mt-4 overflow-y-hidden ${minChartWidth > 100 ? 'overflow-x-auto scrollbar-slim' : ''}`}>
{chartType === 5 ? ( {chartType === 5 ? (
<div className="w-full h-full overflow-y-auto pr-2 scrollbar-slim"> <div className="w-full h-full overflow-y-auto pr-2 scrollbar-slim">
<table className="w-full text-left text-sm text-gray-600 border-collapse"> <table className="w-full text-left text-sm text-gray-600 border-collapse">
@ -203,45 +213,47 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
</table> </table>
</div> </div>
) : ( ) : (
<ResponsiveContainer width="100%" height="100%" className="focus:outline-none [&_.recharts-wrapper]:outline-none [&_.recharts-surface]:outline-none" style={{ outline: 'none' }}> <div style={{ minWidth: minChartWidth > 100 ? `${minChartWidth}px` : '100%', height: '100%' }}>
{chartType === 3 || chartType === 4 ? ( <ResponsiveContainer width="100%" height="100%" className="focus:outline-none [&_.recharts-wrapper]:outline-none [&_.recharts-surface]:outline-none" style={{ outline: 'none' }}>
<PieChart margin={{ top: 10, right: 10, left: 10, bottom: 10 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}> {chartType === 3 || chartType === 4 ? (
<Tooltip <PieChart margin={{ top: 10, right: 10, left: 10, bottom: 10 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
contentStyle={{ borderRadius: '8px', border: '1px solid #E2E8F0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '14px', fontFamily: 'inherit' }} <Tooltip
itemStyle={{ color: '#0F172A', fontWeight: '500' }} contentStyle={{ borderRadius: '8px', border: '1px solid #E2E8F0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '14px', fontFamily: 'inherit' }}
/> itemStyle={{ color: '#0F172A', fontWeight: '500' }}
<Legend wrapperStyle={{ paddingTop: '20px' }} /> />
<Pie <Legend wrapperStyle={{ paddingTop: '20px' }} />
data={finalData} <Pie
dataKey={seriesKeys[0] || "value"} data={finalData}
nameKey="dimension" dataKey={seriesKeys[0] || "value"}
cx="50%" nameKey="dimension"
cy="50%" cx="50%"
outerRadius={100} cy="50%"
innerRadius={chartType === 4 ? 65 : 0} outerRadius={100}
style={{ outline: 'none' }} innerRadius={chartType === 4 ? 65 : 0}
activeShape={false} style={{ outline: 'none' }}
className="focus:outline-none outline-none" activeShape={false}
> className="focus:outline-none outline-none"
{finalData.map((_, index) => ( >
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} style={{ outline: 'none' }} className="focus:outline-none outline-none" /> {finalData.map((_, index) => (
))} <Cell key={`cell-${index}`} fill={colors[index % colors.length]} style={{ outline: 'none' }} className="focus:outline-none outline-none" />
</Pie> ))}
</PieChart> </Pie>
) : chartType === 1 ? ( </PieChart>
<LineChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}> ) : chartType === 1 ? (
{renderChartContent()} <LineChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
</LineChart> {renderChartContent()}
) : chartType === 2 ? ( </LineChart>
<AreaChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}> ) : chartType === 2 ? (
{renderChartContent()} <AreaChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
</AreaChart> {renderChartContent()}
) : ( </AreaChart>
<BarChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}> ) : (
{renderChartContent()} <BarChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
</BarChart> {renderChartContent()}
)} </BarChart>
</ResponsiveContainer> )}
</ResponsiveContainer>
</div>
)} )}
</div> </div>
</Card> </Card>

View File

@ -15,7 +15,7 @@ export function CallsView({ onRowClick, pageSize, headerActions, rowActions, ref
headerActions={headerActions} headerActions={headerActions}
rowActions={rowActions} rowActions={rowActions}
refreshKey={refreshKey} refreshKey={refreshKey}
sortBy="instance_id" sortBy="visit_date"
sortDir="desc" sortDir="desc"
omitColumns={['instance_id']} omitColumns={['instance_id']}
initialFilters={initialFilters} initialFilters={initialFilters}

View File

@ -98,21 +98,21 @@ export function RecordView({
setPage(1); setPage(1);
}; };
const initialFiltersStr = JSON.stringify(initialFilters || {});
const initialFiltersNormalized = useMemo(() => { const initialFiltersNormalized = useMemo(() => {
const res: Record<string, string[]> = {}; const res: Record<string, string[]> = {};
if (initialFilters) { const parsed = JSON.parse(initialFiltersStr);
Object.entries(initialFilters).forEach(([k, v]) => { Object.entries(parsed).forEach(([k, v]) => {
if (Array.isArray(v)) { if (Array.isArray(v)) {
res[k] = v; res[k] = v as string[];
} else if (v) { } else if (v) {
res[k] = [v]; res[k] = [v as string];
} }
}); });
}
return res; return res;
}, [initialFilters]); }, [initialFiltersStr]);
const [activeFilters, setActiveFilters] = useState<Record<string, string[]>>(initialFiltersNormalized); const [activeFilters, setActiveFilters] = useState<Record<string, string[]>>({});
const [resp, setResp] = useState<RecordViewResponse | null>(null); const [resp, setResp] = useState<RecordViewResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -134,14 +134,23 @@ export function RecordView({
const filtersParam = useMemo(() => { const filtersParam = useMemo(() => {
const parsed = JSON.parse(activeFiltersStr) as Record<string, string[]>; const parsed = JSON.parse(activeFiltersStr) as Record<string, string[]>;
return Object.entries(parsed)
// Merge user active filters on top of initial prefilters
const merged = { ...initialFiltersNormalized };
Object.entries(parsed).forEach(([k, v]) => {
if (v && v.length > 0) {
merged[k] = v;
}
});
return Object.entries(merged)
.filter(([_, vals]) => vals && vals.length > 0) .filter(([_, vals]) => vals && vals.length > 0)
.map(([key, vals]) => ({ .map(([key, vals]) => ({
field_key: key, field_key: key,
values: vals, values: vals,
data_type: 'character varying', data_type: 'character varying',
})); }));
}, [activeFiltersStr]); }, [activeFiltersStr, initialFiltersNormalized]);
useEffect(() => { useEffect(() => {
let live = true; let live = true;