changed the theme for header,charts,tiles
This commit is contained in:
parent
930809a581
commit
6bb5b6e18a
@ -12,6 +12,7 @@ import type {
|
|||||||
import { APP_ID } from './config';
|
import { APP_ID } from './config';
|
||||||
|
|
||||||
const TOKEN_KEY = 'krishna_sales_token';
|
const TOKEN_KEY = 'krishna_sales_token';
|
||||||
|
const USER_KEY = 'krishna_sales_user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP client for the Zino gateway (sandbox).
|
* HTTP client for the Zino gateway (sandbox).
|
||||||
@ -23,13 +24,22 @@ export class ZinoClient {
|
|||||||
readonly baseUrl: string;
|
readonly baseUrl: string;
|
||||||
readonly workflowUuid: string;
|
readonly workflowUuid: string;
|
||||||
private token: string | null = null;
|
private token: string | null = null;
|
||||||
|
private user: User | null = null;
|
||||||
private onAuthError?: () => void;
|
private onAuthError?: () => void;
|
||||||
|
|
||||||
constructor(baseUrl: string, workflowUuid: string, onAuthError?: () => void) {
|
constructor(baseUrl: string, workflowUuid: string, onAuthError?: () => void) {
|
||||||
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
||||||
this.workflowUuid = workflowUuid;
|
this.workflowUuid = workflowUuid;
|
||||||
this.onAuthError = onAuthError;
|
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 {
|
setAuthErrorHandler(fn: () => void): void {
|
||||||
@ -39,8 +49,13 @@ export class ZinoClient {
|
|||||||
setToken(token: string | null): void {
|
setToken(token: string | null): void {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
if (token) localStorage.setItem(TOKEN_KEY, token);
|
if (token) {
|
||||||
else localStorage.removeItem(TOKEN_KEY);
|
localStorage.setItem(TOKEN_KEY, token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
localStorage.removeItem(USER_KEY);
|
||||||
|
this.user = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): string | null {
|
getToken(): string | null {
|
||||||
@ -95,6 +110,10 @@ export class ZinoClient {
|
|||||||
...(orgId ? { org_id: Number(orgId) } : {}),
|
...(orgId ? { org_id: Number(orgId) } : {}),
|
||||||
});
|
});
|
||||||
this.setToken(res.token);
|
this.setToken(res.token);
|
||||||
|
if (typeof window !== 'undefined' && res.user) {
|
||||||
|
this.user = res.user;
|
||||||
|
localStorage.setItem(USER_KEY, JSON.stringify(res.user));
|
||||||
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,9 +121,10 @@ export class ZinoClient {
|
|||||||
this.setToken(null);
|
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 {
|
currentUser(): User | null {
|
||||||
if (!this.token) return null;
|
if (!this.token) return null;
|
||||||
|
if (this.user) return this.user;
|
||||||
try {
|
try {
|
||||||
const p = JSON.parse(atob(this.token.split('.')[1]));
|
const p = JSON.parse(atob(this.token.split('.')[1]));
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -12,10 +12,11 @@ export interface CallDetailProps {
|
|||||||
selectedRow?: Record<string, any>;
|
selectedRow?: Record<string, any>;
|
||||||
potentialMiningAction?: ReactNode;
|
potentialMiningAction?: ReactNode;
|
||||||
placeOrderAction?: ReactNode;
|
placeOrderAction?: ReactNode;
|
||||||
|
refreshKey?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Custom Call detail view matching specific groupings. */
|
/** 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<Record<string, unknown> | null>(null);
|
const [data, setData] = useState<Record<string, unknown> | 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);
|
||||||
@ -40,7 +41,7 @@ export function CallDetail({ instanceId, selectedRow, potentialMiningAction, pla
|
|||||||
return () => {
|
return () => {
|
||||||
live = false;
|
live = false;
|
||||||
};
|
};
|
||||||
}, [instanceId]);
|
}, [instanceId, refreshKey]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@ -162,13 +163,23 @@ export function CallDetail({ instanceId, selectedRow, potentialMiningAction, pla
|
|||||||
|
|
||||||
const uploadImage = extract('upload_image', remainingData);
|
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
|
// Helper for rendering a row
|
||||||
const Row = ({ label, value }: { label: string, value: any }) => (
|
const Row = ({ label, value }: { label: string, value: any }) => {
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center py-2 border-b border-border-subtle last:border-0">
|
if (value == null || value === '-' || value === '') return null;
|
||||||
<span className="text-sm text-muted sm:w-1/3">{label}:</span>
|
return (
|
||||||
<span className="text-sm font-bold text-strong sm:w-2/3">{formatValue(value)}</span>
|
<div className="flex flex-col sm:flex-row sm:items-center py-2 border-b border-border-subtle last:border-0">
|
||||||
</div>
|
<span className="text-sm text-muted sm:w-1/3">{label}:</span>
|
||||||
);
|
<span className="text-sm font-bold text-strong sm:w-2/3">{formatValue(value)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
@ -294,7 +305,24 @@ export function CallDetail({ instanceId, selectedRow, potentialMiningAction, pla
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 5. Potential Mining Card */}
|
{/* Productivity of Visit Card */}
|
||||||
|
{isVisitedState && (
|
||||||
|
<Card pad={false} className="shadow-md">
|
||||||
|
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||||||
|
<ClipboardList className="text-[var(--z-bg-primary)]" size={18} />
|
||||||
|
<h3 className="text-lg font-bold text-[var(--z-bg-primary)] m-0">Productivity of Visit</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-5 flex flex-col">
|
||||||
|
<Row label="Is Productive Call" value={isProductiveCall} />
|
||||||
|
<Row label="Store Status" value={storeStatus} />
|
||||||
|
<Row label="Action" value={actionTaken} />
|
||||||
|
<Row label="Order Received Channel" value={orderChannel} />
|
||||||
|
<Row label="Remarks" value={visitRemarks} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Potential Mining Card */}
|
||||||
{miningData.length > 0 && (
|
{miningData.length > 0 && (
|
||||||
<Card pad={false} className="shadow-md">
|
<Card pad={false} className="shadow-md">
|
||||||
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
<div className="flex items-center gap-2 p-4 border-b border-border-subtle bg-slate-50/50">
|
||||||
|
|||||||
@ -2,6 +2,13 @@ import { Card } from './Card';
|
|||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
Bar,
|
Bar,
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
AreaChart,
|
||||||
|
Area,
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
Cell,
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
@ -70,43 +77,149 @@ export function AnalyticsChart({ data }: AnalyticsChartProps) {
|
|||||||
|
|
||||||
if (finalData.length === 0) return null;
|
if (finalData.length === 0) return null;
|
||||||
|
|
||||||
return (
|
const lowerKey = String(chart.key || '').toLowerCase();
|
||||||
<Card key={chart.chart_uid || idx} title={title} className="shadow-sm border-t-4 border-t-indigo-500">
|
let chartType = idx % 3; // 0 = Bar, 1 = Line, 2 = Area
|
||||||
<div className="h-[320px] w-full mt-4">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
if (lowerKey.includes('status')) {
|
||||||
<BarChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
chartType = 3; // Pie
|
||||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E2E8F0" />
|
} else if (lowerKey.includes('route')) {
|
||||||
<XAxis
|
chartType = 4; // Donut
|
||||||
dataKey="dimension"
|
} else if (lowerKey.includes('brand') || lowerKey.includes('product')) {
|
||||||
axisLine={false}
|
chartType = 5; // Table
|
||||||
tickLine={false}
|
}
|
||||||
tick={{ fontSize: 12, fill: '#64748B' }}
|
|
||||||
dy={10}
|
const renderChartContent = () => {
|
||||||
/>
|
return (
|
||||||
<YAxis
|
<>
|
||||||
axisLine={false}
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#E2E8F0" />
|
||||||
tickLine={false}
|
<XAxis
|
||||||
tick={{ fontSize: 12, fill: '#64748B' }}
|
dataKey="dimension"
|
||||||
allowDecimals={false}
|
axisLine={false}
|
||||||
/>
|
tickLine={false}
|
||||||
<Tooltip
|
tick={{ fontSize: 12, fill: '#64748B' }}
|
||||||
cursor={{ fill: '#F8FAFC' }}
|
dy={10}
|
||||||
contentStyle={{ borderRadius: '8px', border: '1px solid #E2E8F0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '14px', fontFamily: 'inherit' }}
|
/>
|
||||||
labelStyle={{ fontWeight: 'bold', color: '#0F172A', marginBottom: '4px' }}
|
<YAxis
|
||||||
/>
|
axisLine={false}
|
||||||
{hasSeries && <Legend wrapperStyle={{ paddingTop: '20px' }} />}
|
tickLine={false}
|
||||||
{seriesKeys.map((key, i) => (
|
tick={{ fontSize: 12, fill: '#64748B' }}
|
||||||
|
allowDecimals={false}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
cursor={{ fill: '#F8FAFC', stroke: '#E2E8F0', strokeWidth: 1, strokeDasharray: '3 3' }}
|
||||||
|
contentStyle={{ borderRadius: '8px', border: '1px solid #E2E8F0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '14px', fontFamily: 'inherit' }}
|
||||||
|
labelStyle={{ fontWeight: 'bold', color: '#0F172A', marginBottom: '4px' }}
|
||||||
|
/>
|
||||||
|
{hasSeries && <Legend wrapperStyle={{ paddingTop: '20px' }} />}
|
||||||
|
{seriesKeys.map((key, i) => {
|
||||||
|
const color = colors[i % colors.length];
|
||||||
|
if (chartType === 1) {
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={key}
|
||||||
|
name={hasSeries ? key : "Value"}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={3}
|
||||||
|
dot={{ r: 4, strokeWidth: 2 }}
|
||||||
|
activeDot={{ r: 6, strokeWidth: 0 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (chartType === 2) {
|
||||||
|
return (
|
||||||
|
<Area
|
||||||
|
key={key}
|
||||||
|
type="monotone"
|
||||||
|
dataKey={key}
|
||||||
|
name={hasSeries ? key : "Value"}
|
||||||
|
stroke={color}
|
||||||
|
fill={color}
|
||||||
|
fillOpacity={0.2}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
<Bar
|
<Bar
|
||||||
key={key}
|
key={key}
|
||||||
dataKey={key}
|
dataKey={key}
|
||||||
name={hasSeries ? key : "Value"}
|
name={hasSeries ? key : "Value"}
|
||||||
fill={colors[i % colors.length]}
|
fill={color}
|
||||||
radius={[4, 4, 0, 0]}
|
radius={[4, 4, 0, 0]}
|
||||||
maxBarSize={40}
|
maxBarSize={40}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
</BarChart>
|
}
|
||||||
</ResponsiveContainer>
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={chart.chart_uid || idx} title={title} className="shadow-sm border border-gray-100">
|
||||||
|
<div className="h-[320px] w-full mt-4">
|
||||||
|
{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">
|
||||||
|
<thead className="bg-gray-50/80 text-gray-700 sticky top-0 z-10 shadow-sm">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 font-semibold border-b border-gray-200">Dimension</th>
|
||||||
|
{seriesKeys.map(k => (
|
||||||
|
<th key={k} className="px-4 py-3 font-semibold border-b border-gray-200 text-right">{hasSeries ? k : 'Value'}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100">
|
||||||
|
{finalData.filter(row => seriesKeys.some(k => row[k] > 0)).map((row, rIdx) => (
|
||||||
|
<tr key={rIdx} className="hover:bg-gray-50 transition-colors">
|
||||||
|
<td className="px-4 py-3 font-medium text-gray-900">{row.dimension}</td>
|
||||||
|
{seriesKeys.map(k => (
|
||||||
|
<td key={k} className="px-4 py-3 tabular-nums text-right">{row[k]}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
{chartType === 3 || chartType === 4 ? (
|
||||||
|
<PieChart margin={{ top: 10, right: 10, left: 10, bottom: 10 }}>
|
||||||
|
<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}
|
||||||
|
>
|
||||||
|
{finalData.map((_, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
</PieChart>
|
||||||
|
) : chartType === 1 ? (
|
||||||
|
<LineChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||||
|
{renderChartContent()}
|
||||||
|
</LineChart>
|
||||||
|
) : chartType === 2 ? (
|
||||||
|
<AreaChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||||
|
{renderChartContent()}
|
||||||
|
</AreaChart>
|
||||||
|
) : (
|
||||||
|
<BarChart data={finalData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||||
|
{renderChartContent()}
|
||||||
|
</BarChart>
|
||||||
|
)}
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -44,27 +44,27 @@ export function Modal({ open, onClose, title, subtitle, width = 'md', actions, c
|
|||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[10000] flex items-start justify-end overflow-hidden bg-black/10 transition-opacity"
|
className="fixed inset-0 z-[10000] flex items-center justify-center p-4 sm:p-6 overflow-hidden bg-black/50 backdrop-blur-sm transition-opacity"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
if (e.target === e.currentTarget) onClose();
|
if (e.target === e.currentTarget) onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{`
|
<style>{`
|
||||||
@keyframes slideInRight {
|
@keyframes popIn {
|
||||||
from { transform: translateX(100%); }
|
from { opacity: 0; transform: scale(0.95) translateY(10px); }
|
||||||
to { transform: translateX(0); }
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||||
}
|
}
|
||||||
.animate-slide-in-right {
|
.animate-pop-in {
|
||||||
animation: slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
animation: popIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full bg-[var(--z-block-bg)] shadow-[auto_0_30px_rgba(0,0,0,0.1)] my-0',
|
'w-full bg-[var(--z-block-bg)] shadow-2xl my-0',
|
||||||
WIDTH_CLASSES[width],
|
WIDTH_CLASSES[width],
|
||||||
'flex flex-col h-[100dvh] rounded-l-2xl rounded-r-none animate-slide-in-right border-l border-[var(--z-border-default)]',
|
'flex flex-col max-h-[calc(100dvh-3rem)] rounded-xl animate-pop-in border border-[var(--z-border-default)] overflow-hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<header className="shrink-0 flex items-start justify-between gap-3 px-5 py-4 border-b border-[var(--z-border-default)]">
|
<header className="shrink-0 flex items-start justify-between gap-3 px-5 py-4 border-b border-[var(--z-border-default)]">
|
||||||
|
|||||||
@ -26,26 +26,44 @@ export function StatsTiles({ tiles }: StatsTilesProps) {
|
|||||||
|
|
||||||
const isDanger = lowerKey.includes('no_order');
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={tile.tile_uid || idx}
|
key={tile.tile_uid || idx}
|
||||||
className={`flex flex-col justify-between w-full h-[100px] p-4 rounded-lg bg-[var(--z-bg-neutral-100)] border border-[var(--z-border-neutral-300)] border-t-4 shadow-sm transition-transform duration-200 hover:-translate-y-1 ${
|
className={`flex flex-col justify-between w-full h-[100px] p-5 rounded-2xl bg-white border border-gray-100 shadow-sm transition-all duration-200 hover:shadow-md hover:-translate-y-1 cursor-pointer`}
|
||||||
isDanger ? 'border-t-[var(--z-text-danger-400)]' : 'border-t-[var(--z-text-primary)]'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center w-full">
|
||||||
<span className={`text-[10px] font-bold uppercase tracking-wider whitespace-nowrap overflow-hidden text-ellipsis max-w-[80%] ${
|
<span className={`text-sm font-bold truncate pr-2 ${
|
||||||
isDanger ? 'text-[var(--z-text-danger-400)]' : 'text-[var(--z-text-neutral-500)]'
|
isDanger ? 'text-red-600' : 'text-gray-600'
|
||||||
}`}>
|
}`}>
|
||||||
{displayLabel}
|
{displayLabel}
|
||||||
</span>
|
</span>
|
||||||
<Icon size={16} className={`${
|
<div className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${iconBgColor} ${iconColor}`}>
|
||||||
isDanger ? 'text-[var(--z-text-danger-400)]' : 'text-[var(--z-text-success-400)]'
|
<Icon size={18} strokeWidth={2.5} />
|
||||||
}`} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className={`text-[30px] font-extrabold leading-tight tabular-nums ${
|
<p className={`text-[26px] font-extrabold leading-none tabular-nums mt-auto ${
|
||||||
isDanger ? 'text-[var(--z-text-danger-400)]' : 'text-[var(--z-text-primary)]'
|
isDanger ? 'text-red-600' : 'text-[#0058be]'
|
||||||
}`}>
|
}`}>
|
||||||
{(tile.value as React.ReactNode) ?? "-"}
|
{(tile.value as React.ReactNode) ?? "-"}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export interface RecordViewProps {
|
|||||||
columns?: string[];
|
columns?: string[];
|
||||||
/** Columns to hide. */
|
/** Columns to hide. */
|
||||||
omitColumns?: string[];
|
omitColumns?: string[];
|
||||||
/** Rows per page. @default 25 */
|
/** Rows per page. @default 10 */
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
/** Click handler — receives the raw row + index. */
|
/** Click handler — receives the raw row + index. */
|
||||||
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
||||||
@ -55,7 +55,7 @@ export function RecordView({
|
|||||||
rvUid,
|
rvUid,
|
||||||
title = 'Records',
|
title = 'Records',
|
||||||
columns,
|
columns,
|
||||||
pageSize = 25,
|
pageSize = 10,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
rowKey,
|
rowKey,
|
||||||
headerActions,
|
headerActions,
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export function CallsPage() {
|
|||||||
const [miningPrefill, setMiningPrefill] = useState<Record<string, unknown> | undefined>();
|
const [miningPrefill, setMiningPrefill] = useState<Record<string, unknown> | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (instanceId != null && !selectedRow) {
|
if (instanceId != null) {
|
||||||
orderBookingClient.recordView(ORDER_BOOKING.recordViews.CALLS, {
|
orderBookingClient.recordView(ORDER_BOOKING.recordViews.CALLS, {
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
@ -33,7 +33,7 @@ export function CallsPage() {
|
|||||||
}
|
}
|
||||||
}).catch(err => console.error("Failed to fetch selected row:", err));
|
}).catch(err => console.error("Failed to fetch selected row:", err));
|
||||||
}
|
}
|
||||||
}, [instanceId, selectedRow]);
|
}, [instanceId, refreshKey]);
|
||||||
|
|
||||||
const handlePotentialMiningClick = async () => {
|
const handlePotentialMiningClick = async () => {
|
||||||
setMiningLoading(true);
|
setMiningLoading(true);
|
||||||
@ -123,6 +123,7 @@ export function CallsPage() {
|
|||||||
<CallDetail
|
<CallDetail
|
||||||
instanceId={instanceId}
|
instanceId={instanceId}
|
||||||
selectedRow={selectedRow}
|
selectedRow={selectedRow}
|
||||||
|
refreshKey={refreshKey}
|
||||||
potentialMiningAction={
|
potentialMiningAction={
|
||||||
<Button size="sm" variant="secondary" className="w-48 max-w-full" onClick={handlePotentialMiningClick} disabled={miningLoading}>
|
<Button size="sm" variant="secondary" className="w-48 max-w-full" onClick={handlePotentialMiningClick} disabled={miningLoading}>
|
||||||
{miningLoading ? 'Loading...' : 'Potential Mining'}
|
{miningLoading ? 'Loading...' : 'Potential Mining'}
|
||||||
@ -137,6 +138,12 @@ export function CallsPage() {
|
|||||||
Edit Order
|
Edit Order
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
} else if (stateName.includes('visited')) {
|
||||||
|
return (
|
||||||
|
<Button onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PRODUCTIVITY_OF_VISIT.uid, name: 'Productivity of Visit' })}>
|
||||||
|
Productivity of Visit
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
} else if (stateName.includes('productive')) {
|
} else if (stateName.includes('productive')) {
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
|
<Button onClick={() => setActiveActivity({ id: ORDER_BOOKING.activities.PLACE_ORDER.uid, name: 'Place Order' })}>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { LogOut, FileText, ChevronDown } from 'lucide-react';
|
import { LogOut, ChevronDown } from 'lucide-react';
|
||||||
import { cn } from '../lib/cn';
|
import { cn } from '../lib/cn';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
import { onAuthErrorAll, orderBookingClient } from '../api/clients';
|
||||||
|
|
||||||
import { Button } from '../components/buttons';
|
|
||||||
import { SCREENS } from './tabs';
|
import { SCREENS } from './tabs';
|
||||||
import { REPORT_MAP } from './ReportPage';
|
import { REPORT_MAP } from './ReportPage';
|
||||||
|
|
||||||
@ -30,25 +30,23 @@ export function ConsoleLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-app flex flex-col">
|
<div className="h-screen bg-app flex flex-col">
|
||||||
<header className="sticky top-0 z-10 shrink-0 flex items-center justify-between gap-3 px-6 h-[60px] bg-[var(--z-bg-primary)] border-b border-[var(--z-bg-primary)] shadow-sm">
|
<header className="sticky top-0 z-10 shrink-0 flex items-center justify-between gap-6 px-6 h-[56px] shadow-sm" style={{ background: 'var(--nav-bg-color)' }}>
|
||||||
<div className="flex flex-col">
|
<div className="flex items-center min-w-max w-48">
|
||||||
<span className="text-md font-extrabold text-white tracking-[-0.01em] leading-none">Krishna Sales</span>
|
<span className="text-lg font-bold text-white tracking-wide leading-none">Krishna Sales</span>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex items-center gap-1 h-full">
|
<nav className="flex items-center justify-center gap-2 h-full flex-1">
|
||||||
{SCREENS.map((t) => {
|
{SCREENS.map((t) => {
|
||||||
const Icon = t.icon;
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={t.key}
|
key={t.key}
|
||||||
to={`/${t.key}`}
|
to={`/${t.key}`}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
'flex items-center gap-1.5 no-underline font-sans text-sm font-medium px-3 py-1.5 rounded-md transition-all duration-150',
|
'flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150',
|
||||||
isActive ? 'bg-white text-[var(--z-text-primary)]' : 'text-white hover:text-[var(--z-text-primary)] hover:bg-white',
|
isActive ? 'bg-white/20 text-white' : 'text-white/90 hover:bg-white/10 hover:text-white',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon size={16} />
|
|
||||||
{t.label}
|
{t.label}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
@ -56,12 +54,11 @@ export function ConsoleLayout() {
|
|||||||
|
|
||||||
<div className="relative group flex items-center h-full">
|
<div className="relative group flex items-center h-full">
|
||||||
<button className={cn(
|
<button className={cn(
|
||||||
"flex items-center gap-1.5 no-underline font-sans text-sm font-medium px-3 py-1.5 rounded-md transition-all duration-150 cursor-pointer",
|
"flex items-center gap-1.5 no-underline font-sans text-[13px] font-semibold px-3 py-1.5 rounded-md transition-all duration-150 cursor-pointer",
|
||||||
isReportsActive ? "bg-white text-[var(--z-text-primary)]" : "text-white hover:text-[var(--z-text-primary)] hover:bg-white"
|
isReportsActive ? "bg-white/20 text-white" : "text-white/90 hover:bg-white/10 hover:text-white"
|
||||||
)}>
|
)}>
|
||||||
<FileText size={16} />
|
|
||||||
Reports
|
Reports
|
||||||
<ChevronDown size={14} className="ml-0.5" />
|
<ChevronDown size={14} className="ml-0.5 opacity-70" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="absolute top-[80%] left-0 mt-1 w-56 bg-white rounded-md shadow-lg py-1 border border-gray-200 hidden group-hover:block z-50">
|
<div className="absolute top-[80%] left-0 mt-1 w-56 bg-white rounded-md shadow-lg py-1 border border-gray-200 hidden group-hover:block z-50">
|
||||||
@ -82,19 +79,31 @@ export function ConsoleLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="flex items-center gap-3">
|
<div className="relative group flex items-center justify-end shrink-0 w-48 h-full py-2">
|
||||||
{user?.name && <span className="text-sm text-white hidden sm:inline">{user.name}</span>}
|
<button
|
||||||
<Button
|
className="w-8 h-8 rounded-full bg-black/20 text-white flex items-center justify-center text-sm font-bold hover:bg-black/30 transition-colors cursor-pointer"
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
iconLeft={<LogOut size={14} />}
|
|
||||||
onClick={() => {
|
|
||||||
logout();
|
|
||||||
navigate('/login', { replace: true });
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Sign out
|
{user?.name ? user.name.charAt(0).toUpperCase() : 'S'}
|
||||||
</Button>
|
</button>
|
||||||
|
|
||||||
|
<div className="absolute top-[85%] right-0 mt-1 w-60 bg-white rounded-md shadow-xl py-2 border border-gray-200 hidden group-hover:block z-50 text-left">
|
||||||
|
<div className="px-4 py-3 border-b border-gray-100 mb-1">
|
||||||
|
<p className="text-sm font-bold text-gray-900 truncate">{user?.name || 'User'}</p>
|
||||||
|
<p className="text-xs text-gray-500 truncate mt-0.5">{user?.email || 'user@example.com'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
navigate('/login', { replace: true });
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-3 py-2 text-sm text-red-600 font-medium hover:bg-red-50 rounded-md transition-colors flex items-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<LogOut size={16} />
|
||||||
|
Sign out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, type FormEvent } from 'react';
|
import { useState, type FormEvent } from 'react';
|
||||||
import { Navigate, useNavigate } from 'react-router-dom';
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../auth/context';
|
import { useAuth } from '../auth/context';
|
||||||
import { APP_ID } from '../api/config';
|
|
||||||
import { Button } from '../components/buttons';
|
import { Button } from '../components/buttons';
|
||||||
import { Card, Input } from '../components/reusable';
|
import { Card, Input } from '../components/reusable';
|
||||||
|
|
||||||
@ -11,7 +11,7 @@ export function LoginPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [orgId, setOrgId] = useState('');
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ export function LoginPage() {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await login(email, password, orgId || undefined);
|
await login(email, password);
|
||||||
navigate('/orders', { replace: true });
|
navigate('/orders', { replace: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError((err as { message?: string })?.message ?? 'Login failed');
|
setError((err as { message?: string })?.message ?? 'Login failed');
|
||||||
@ -36,12 +36,11 @@ export function LoginPage() {
|
|||||||
<Card className="w-full max-w-[400px]">
|
<Card className="w-full max-w-[400px]">
|
||||||
<div className="flex flex-col gap-1 mb-5">
|
<div className="flex flex-col gap-1 mb-5">
|
||||||
<h1 className="m-0 text-2xl font-extrabold text-strong tracking-[-0.02em]">Krishna Sales</h1>
|
<h1 className="m-0 text-2xl font-extrabold text-strong tracking-[-0.02em]">Krishna Sales</h1>
|
||||||
<p className="m-0 text-sm text-faint">Field Sales console · Sandbox {APP_ID}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={submit} className="flex flex-col gap-4">
|
<form onSubmit={submit} className="flex flex-col gap-4">
|
||||||
<Input label="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
<Input label="Email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
||||||
<Input label="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
<Input label="Password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
||||||
<Input label="Org ID" hint="Optional" value={orgId} onChange={(e) => setOrgId(e.target.value)} />
|
|
||||||
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
|
{error && <div className="text-xs text-ruby-600 font-medium">{error}</div>}
|
||||||
<Button type="submit" full disabled={busy}>
|
<Button type="submit" full disabled={busy}>
|
||||||
{busy ? 'Signing in…' : 'Sign in'}
|
{busy ? 'Signing in…' : 'Sign in'}
|
||||||
|
|||||||
@ -196,7 +196,7 @@
|
|||||||
|
|
||||||
|
|
||||||
/* Navbar background color */
|
/* Navbar background color */
|
||||||
--nav-bg-color: #fff;
|
--nav-bg-color: linear-gradient(91deg, #1b37a5 0.17%, #2e56e1 99.89%);
|
||||||
--nav-item-color: #10182b;
|
--nav-item-color: #10182b;
|
||||||
--nav-item-active: #1b84ff;
|
--nav-item-active: #1b84ff;
|
||||||
--nav-item-active-bg: #c0d4ed45;
|
--nav-item-active-bg: #c0d4ed45;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user