diff --git a/src/api/client.ts b/src/api/client.ts index 27b7927..840769d 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -12,6 +12,7 @@ import type { import { APP_ID } from './config'; const TOKEN_KEY = 'krishna_sales_token'; +const USER_KEY = 'krishna_sales_user'; /** * HTTP client for the Zino gateway (sandbox). @@ -23,13 +24,22 @@ export class ZinoClient { readonly baseUrl: string; readonly workflowUuid: string; private token: string | null = null; + private user: User | null = null; private onAuthError?: () => void; constructor(baseUrl: string, workflowUuid: string, onAuthError?: () => void) { this.baseUrl = baseUrl.replace(/\/+$/, ''); this.workflowUuid = workflowUuid; this.onAuthError = onAuthError; - if (typeof window !== 'undefined') this.token = localStorage.getItem(TOKEN_KEY); + if (typeof window !== 'undefined') { + this.token = localStorage.getItem(TOKEN_KEY); + try { + const u = localStorage.getItem(USER_KEY); + if (u) this.user = JSON.parse(u); + } catch { + this.user = null; + } + } } setAuthErrorHandler(fn: () => void): void { @@ -39,8 +49,13 @@ export class ZinoClient { setToken(token: string | null): void { this.token = token; if (typeof window === 'undefined') return; - if (token) localStorage.setItem(TOKEN_KEY, token); - else localStorage.removeItem(TOKEN_KEY); + if (token) { + localStorage.setItem(TOKEN_KEY, token); + } else { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(USER_KEY); + this.user = null; + } } getToken(): string | null { @@ -95,6 +110,10 @@ export class ZinoClient { ...(orgId ? { org_id: Number(orgId) } : {}), }); this.setToken(res.token); + if (typeof window !== 'undefined' && res.user) { + this.user = res.user; + localStorage.setItem(USER_KEY, JSON.stringify(res.user)); + } return res; } @@ -102,9 +121,10 @@ export class ZinoClient { this.setToken(null); } - /** Decode the persisted JWT into a User (no network). */ + /** Decode the persisted JWT into a User (no network) or use the saved user. */ currentUser(): User | null { if (!this.token) return null; + if (this.user) return this.user; try { const p = JSON.parse(atob(this.token.split('.')[1])); return { diff --git a/src/components/dv/CallDetail.tsx b/src/components/dv/CallDetail.tsx index 3c3a2b2..4cfbd9f 100644 --- a/src/components/dv/CallDetail.tsx +++ b/src/components/dv/CallDetail.tsx @@ -12,10 +12,11 @@ export interface CallDetailProps { selectedRow?: Record; potentialMiningAction?: ReactNode; placeOrderAction?: ReactNode; + refreshKey?: number; } /** Custom Call detail view matching specific groupings. */ -export function CallDetail({ instanceId, selectedRow, potentialMiningAction, placeOrderAction }: CallDetailProps) { +export function CallDetail({ instanceId, selectedRow, potentialMiningAction, placeOrderAction, refreshKey }: CallDetailProps) { const [data, setData] = useState | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -40,7 +41,7 @@ export function CallDetail({ instanceId, selectedRow, potentialMiningAction, pla return () => { live = false; }; - }, [instanceId]); + }, [instanceId, refreshKey]); if (error) { return ( @@ -162,13 +163,23 @@ export function CallDetail({ instanceId, selectedRow, potentialMiningAction, pla const uploadImage = extract('upload_image', remainingData); + const isVisitedState = valLower.includes('visited'); + const isProductiveCall = extract('is_productive_call', remainingData); + const actionTaken = extract('action', remainingData); + const orderChannel = extract('order_received_channel', remainingData); + const visitRemarks = extract('remarks', remainingData); + const storeStatus = extract('store_status', remainingData); + // Helper for rendering a row - const Row = ({ label, value }: { label: string, value: any }) => ( -
- {label}: - {formatValue(value)} -
- ); + const Row = ({ label, value }: { label: string, value: any }) => { + if (value == null || value === '-' || value === '') return null; + return ( +
+ {label}: + {formatValue(value)} +
+ ); + }; return (
@@ -294,7 +305,24 @@ export function CallDetail({ instanceId, selectedRow, potentialMiningAction, pla
- {/* 5. Potential Mining Card */} + {/* Productivity of Visit Card */} + {isVisitedState && ( + +
+ +

Productivity of Visit

+
+
+ + + + + +
+
+ )} + + {/* Potential Mining Card */} {miningData.length > 0 && (
diff --git a/src/components/reusable/AnalyticsChart.tsx b/src/components/reusable/AnalyticsChart.tsx index b9df0cb..d3651a4 100644 --- a/src/components/reusable/AnalyticsChart.tsx +++ b/src/components/reusable/AnalyticsChart.tsx @@ -2,6 +2,13 @@ import { Card } from './Card'; import { BarChart, Bar, + LineChart, + Line, + AreaChart, + Area, + PieChart, + Pie, + Cell, XAxis, YAxis, CartesianGrid, @@ -70,43 +77,149 @@ export function AnalyticsChart({ data }: AnalyticsChartProps) { if (finalData.length === 0) return null; - return ( - -
- - - - - - - {hasSeries && } - {seriesKeys.map((key, i) => ( + const lowerKey = String(chart.key || '').toLowerCase(); + let chartType = idx % 3; // 0 = Bar, 1 = Line, 2 = Area + + if (lowerKey.includes('status')) { + chartType = 3; // Pie + } else if (lowerKey.includes('route')) { + chartType = 4; // Donut + } else if (lowerKey.includes('brand') || lowerKey.includes('product')) { + chartType = 5; // Table + } + + const renderChartContent = () => { + return ( + <> + + + + + {hasSeries && } + {seriesKeys.map((key, i) => { + const color = colors[i % colors.length]; + if (chartType === 1) { + return ( + + ); + } else if (chartType === 2) { + return ( + + ); + } else { + return ( - ))} - - + ); + } + })} + + ); + }; + + return ( + +
+ {chartType === 5 ? ( +
+ + + + + {seriesKeys.map(k => ( + + ))} + + + + {finalData.filter(row => seriesKeys.some(k => row[k] > 0)).map((row, rIdx) => ( + + + {seriesKeys.map(k => ( + + ))} + + ))} + +
Dimension{hasSeries ? k : 'Value'}
{row.dimension}{row[k]}
+
+ ) : ( + + {chartType === 3 || chartType === 4 ? ( + + + + + {finalData.map((_, index) => ( + + ))} + + + ) : chartType === 1 ? ( + + {renderChartContent()} + + ) : chartType === 2 ? ( + + {renderChartContent()} + + ) : ( + + {renderChartContent()} + + )} + + )}
); diff --git a/src/components/reusable/Modal.tsx b/src/components/reusable/Modal.tsx index 1d719e7..f3eb402 100644 --- a/src/components/reusable/Modal.tsx +++ b/src/components/reusable/Modal.tsx @@ -44,27 +44,27 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', actions, c return createPortal(
{ if (e.target === e.currentTarget) onClose(); }} >
diff --git a/src/components/reusable/StatsTiles.tsx b/src/components/reusable/StatsTiles.tsx index 94f8c3a..e0f9f1b 100644 --- a/src/components/reusable/StatsTiles.tsx +++ b/src/components/reusable/StatsTiles.tsx @@ -26,26 +26,44 @@ export function StatsTiles({ tiles }: StatsTilesProps) { const isDanger = lowerKey.includes('no_order'); + let iconBgColor = 'bg-blue-50'; + let iconColor = 'text-blue-600'; + + if (Icon === ShoppingBag) { + iconBgColor = 'bg-green-50'; + iconColor = 'text-green-600'; + } else if (Icon === Scale) { + iconBgColor = 'bg-purple-50'; + iconColor = 'text-purple-600'; + } else if (Icon === Phone) { + iconBgColor = 'bg-teal-50'; + iconColor = 'text-teal-600'; + } else if (Icon === TrendingUp) { + iconBgColor = 'bg-orange-50'; + iconColor = 'text-orange-600'; + } else if (isDanger) { + iconBgColor = 'bg-red-50'; + iconColor = 'text-red-600'; + } + return (
-
- + {displayLabel} - +
+ +
-

{(tile.value as React.ReactNode) ?? "-"}

diff --git a/src/components/rv/RecordView.tsx b/src/components/rv/RecordView.tsx index e66d418..0a41a16 100644 --- a/src/components/rv/RecordView.tsx +++ b/src/components/rv/RecordView.tsx @@ -23,7 +23,7 @@ export interface RecordViewProps { columns?: string[]; /** Columns to hide. */ omitColumns?: string[]; - /** Rows per page. @default 25 */ + /** Rows per page. @default 10 */ pageSize?: number; /** Click handler — receives the raw row + index. */ onRowClick?: (row: Record, index: number) => void; @@ -55,7 +55,7 @@ export function RecordView({ rvUid, title = 'Records', columns, - pageSize = 25, + pageSize = 10, onRowClick, rowKey, headerActions, diff --git a/src/screens/CallsPage.tsx b/src/screens/CallsPage.tsx index 94172a1..9d7d42e 100644 --- a/src/screens/CallsPage.tsx +++ b/src/screens/CallsPage.tsx @@ -22,7 +22,7 @@ export function CallsPage() { const [miningPrefill, setMiningPrefill] = useState | undefined>(); useEffect(() => { - if (instanceId != null && !selectedRow) { + if (instanceId != null) { orderBookingClient.recordView(ORDER_BOOKING.recordViews.CALLS, { page: 1, limit: 1, @@ -33,7 +33,7 @@ export function CallsPage() { } }).catch(err => console.error("Failed to fetch selected row:", err)); } - }, [instanceId, selectedRow]); + }, [instanceId, refreshKey]); const handlePotentialMiningClick = async () => { setMiningLoading(true); @@ -123,6 +123,7 @@ export function CallsPage() { {miningLoading ? 'Loading...' : 'Potential Mining'} @@ -137,6 +138,12 @@ export function CallsPage() { Edit Order ); + } else if (stateName.includes('visited')) { + return ( + + ); } else if (stateName.includes('productive')) { return (
@@ -82,19 +79,31 @@ export function ConsoleLayout() {
-
- {user?.name && {user.name}} - + {user?.name ? user.name.charAt(0).toUpperCase() : 'S'} + + +
+
+

{user?.name || 'User'}

+

{user?.email || 'user@example.com'}

+
+
+ +
+
diff --git a/src/screens/LoginPage.tsx b/src/screens/LoginPage.tsx index d5949cb..3f8d9f0 100644 --- a/src/screens/LoginPage.tsx +++ b/src/screens/LoginPage.tsx @@ -1,7 +1,7 @@ import { useState, type FormEvent } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; import { useAuth } from '../auth/context'; -import { APP_ID } from '../api/config'; + import { Button } from '../components/buttons'; import { Card, Input } from '../components/reusable'; @@ -11,7 +11,7 @@ export function LoginPage() { const navigate = useNavigate(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [orgId, setOrgId] = useState(''); + const [busy, setBusy] = useState(false); const [error, setError] = useState(null); @@ -22,7 +22,7 @@ export function LoginPage() { setBusy(true); setError(null); try { - await login(email, password, orgId || undefined); + await login(email, password); navigate('/orders', { replace: true }); } catch (err) { setError((err as { message?: string })?.message ?? 'Login failed'); @@ -36,12 +36,11 @@ export function LoginPage() {

Krishna Sales

-

Field Sales console · Sandbox {APP_ID}

setEmail(e.target.value)} required autoFocus /> setPassword(e.target.value)} required /> - setOrgId(e.target.value)} /> + {error &&
{error}
}