added graph and potentialmining
This commit is contained in:
parent
0ada5c4735
commit
7835cd3630
@ -60,6 +60,7 @@ export interface CallDetailProps {
|
||||
placeOrderAction?: ReactNode;
|
||||
refreshKey?: number;
|
||||
onBack?: () => void;
|
||||
onDataLoad?: (data: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
export function CallDetail({
|
||||
@ -69,6 +70,7 @@ export function CallDetail({
|
||||
placeOrderAction,
|
||||
refreshKey,
|
||||
onBack,
|
||||
onDataLoad,
|
||||
}: CallDetailProps) {
|
||||
const [data, setData] = useState<Record<string, unknown> | 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);
|
||||
if (live) {
|
||||
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) {
|
||||
orderBookingClient.request<{ potential: { potential: any[] } }>(
|
||||
'POST',
|
||||
@ -107,7 +110,7 @@ export function CallDetail({
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [instanceId, refreshKey]);
|
||||
}, [instanceId, refreshKey, onDataLoad]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@ -41,7 +41,7 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card title={`Order #${instanceId}`}>
|
||||
@ -49,10 +49,10 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return (
|
||||
<Card title={`Order #${instanceId}`}>
|
||||
<Card title={`Order`}>
|
||||
<EmptyState title="No details found" />
|
||||
</Card>
|
||||
);
|
||||
@ -76,9 +76,9 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
|
||||
const orderId = extract('order_id', remainingData) || '-';
|
||||
let dateOfOrder = extract('date_of_order_3', remainingData);
|
||||
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 {
|
||||
dateOfOrder = '-';
|
||||
dateOfOrder = '-';
|
||||
}
|
||||
|
||||
// SO Info
|
||||
@ -92,7 +92,7 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
|
||||
soEmail = (soRaw as any).email || '-';
|
||||
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
|
||||
const storeRaw = extract('select_store', remainingData);
|
||||
@ -110,20 +110,13 @@ export function OrderDetail({ instanceId }: WiredDetailViewProps) {
|
||||
// Line Items
|
||||
const orderDetailsGrid = extract('order_details_3', remainingData);
|
||||
const itemsArray = Array.isArray(orderDetailsGrid) ? orderDetailsGrid : [];
|
||||
|
||||
|
||||
// Totals
|
||||
const totalBags = extract('total_bags_3', remainingData) || 0;
|
||||
const totalKgs = extract('total_kgs_3', remainingData) || 0;
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<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">
|
||||
|
||||
@ -47,7 +47,7 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
|
||||
: "grid grid-cols-1 lg:grid-cols-3 gap-6 w-full";
|
||||
|
||||
return (
|
||||
<div className={gridClass}>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 w-full">
|
||||
{charts.map((chart, idx) => {
|
||||
const title = (chart.key || `Chart ${idx + 1}`)
|
||||
.replace(/_/g, ' ')
|
||||
@ -93,10 +93,20 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
|
||||
chartType = 4; // Donut
|
||||
} else if (lowerKey.includes('brand') || lowerKey.includes('product')) {
|
||||
chartType = 5; // Table
|
||||
} else if (lowerKey.includes('monthly')) {
|
||||
} else if (lowerKey.includes('monthly') || lowerKey.includes('yearly')) {
|
||||
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 = () => {
|
||||
return (
|
||||
<>
|
||||
@ -177,8 +187,8 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={chart.chart_uid || idx} title={title} className="shadow-sm border border-gray-100 !bg-[var(--tiles-card-bg)]">
|
||||
<div className="h-[320px] w-full mt-4">
|
||||
<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 overflow-y-hidden ${minChartWidth > 100 ? 'overflow-x-auto scrollbar-slim' : ''}`}>
|
||||
{chartType === 5 ? (
|
||||
<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">
|
||||
@ -203,45 +213,47 @@ export function AnalyticsChart({ data, gridCols }: AnalyticsChartProps) {
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height="100%" className="focus:outline-none [&_.recharts-wrapper]:outline-none [&_.recharts-surface]:outline-none" style={{ outline: 'none' }}>
|
||||
{chartType === 3 || chartType === 4 ? (
|
||||
<PieChart margin={{ top: 10, right: 10, left: 10, bottom: 10 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
|
||||
<Tooltip
|
||||
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
|
||||
data={finalData}
|
||||
dataKey={seriesKeys[0] || "value"}
|
||||
nameKey="dimension"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={100}
|
||||
innerRadius={chartType === 4 ? 65 : 0}
|
||||
style={{ 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" />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
) : chartType === 1 ? (
|
||||
<LineChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
|
||||
{renderChartContent()}
|
||||
</LineChart>
|
||||
) : chartType === 2 ? (
|
||||
<AreaChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
|
||||
{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>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
<div style={{ minWidth: minChartWidth > 100 ? `${minChartWidth}px` : '100%', height: '100%' }}>
|
||||
<ResponsiveContainer width="100%" height="100%" className="focus:outline-none [&_.recharts-wrapper]:outline-none [&_.recharts-surface]:outline-none" style={{ outline: 'none' }}>
|
||||
{chartType === 3 || chartType === 4 ? (
|
||||
<PieChart margin={{ top: 10, right: 10, left: 10, bottom: 10 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
|
||||
<Tooltip
|
||||
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
|
||||
data={finalData}
|
||||
dataKey={seriesKeys[0] || "value"}
|
||||
nameKey="dimension"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={100}
|
||||
innerRadius={chartType === 4 ? 65 : 0}
|
||||
style={{ 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" />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
) : chartType === 1 ? (
|
||||
<LineChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
|
||||
{renderChartContent()}
|
||||
</LineChart>
|
||||
) : chartType === 2 ? (
|
||||
<AreaChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }} className="focus:outline-none outline-none" style={{ outline: 'none' }}>
|
||||
{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>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -15,7 +15,7 @@ export function CallsView({ onRowClick, pageSize, headerActions, rowActions, ref
|
||||
headerActions={headerActions}
|
||||
rowActions={rowActions}
|
||||
refreshKey={refreshKey}
|
||||
sortBy="instance_id"
|
||||
sortBy="visit_date"
|
||||
sortDir="desc"
|
||||
omitColumns={['instance_id']}
|
||||
initialFilters={initialFilters}
|
||||
|
||||
@ -98,21 +98,21 @@ export function RecordView({
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const initialFiltersStr = JSON.stringify(initialFilters || {});
|
||||
const initialFiltersNormalized = useMemo(() => {
|
||||
const res: Record<string, string[]> = {};
|
||||
if (initialFilters) {
|
||||
Object.entries(initialFilters).forEach(([k, v]) => {
|
||||
if (Array.isArray(v)) {
|
||||
res[k] = v;
|
||||
} else if (v) {
|
||||
res[k] = [v];
|
||||
}
|
||||
});
|
||||
}
|
||||
const parsed = JSON.parse(initialFiltersStr);
|
||||
Object.entries(parsed).forEach(([k, v]) => {
|
||||
if (Array.isArray(v)) {
|
||||
res[k] = v as string[];
|
||||
} else if (v) {
|
||||
res[k] = [v as string];
|
||||
}
|
||||
});
|
||||
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 [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -134,14 +134,23 @@ export function RecordView({
|
||||
|
||||
const filtersParam = useMemo(() => {
|
||||
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)
|
||||
.map(([key, vals]) => ({
|
||||
field_key: key,
|
||||
values: vals,
|
||||
data_type: 'character varying',
|
||||
}));
|
||||
}, [activeFiltersStr]);
|
||||
}, [activeFiltersStr, initialFiltersNormalized]);
|
||||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user