order detail product card design done
This commit is contained in:
parent
cd4de05a68
commit
377c851ba6
@ -127,7 +127,6 @@ export class ZinoClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const p = JSON.parse(atob(this.token.split('.')[1]));
|
const p = JSON.parse(atob(this.token.split('.')[1]));
|
||||||
console.log('--- JWT PAYLOAD ---', p);
|
|
||||||
return {
|
return {
|
||||||
id: String(p.user_id ?? p.sub ?? ''),
|
id: String(p.user_id ?? p.sub ?? ''),
|
||||||
org_id: String(p.org_id ?? ''),
|
org_id: String(p.org_id ?? ''),
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export const ORDER_BOOKING = {
|
|||||||
recordViews: {
|
recordViews: {
|
||||||
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
|
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
|
||||||
CALLS: 'b4d7a710-24e7-416d-885a-31fd1e727aab',
|
CALLS: 'b4d7a710-24e7-416d-885a-31fd1e727aab',
|
||||||
ANALYTICS: '56b21b46-6d2c-4e10-b5a3-c63d058d0afa'
|
ANALYTICS: 'b4d7a710-24e7-416d-885a-31fd1e727aab'
|
||||||
},
|
},
|
||||||
detailViews: {
|
detailViews: {
|
||||||
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',
|
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',
|
||||||
|
|||||||
@ -159,24 +159,16 @@ export function SmartGridField({
|
|||||||
{value.length === 0 ? (
|
{value.length === 0 ? (
|
||||||
<span className="text-sm text-faint italic">No rows added.</span>
|
<span className="text-sm text-faint italic">No rows added.</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="flex flex-col gap-3">
|
||||||
<table className="w-full text-left border-collapse bg-white border border-border-subtle shadow-sm rounded">
|
{value.map((row, i) => {
|
||||||
<thead>
|
let productName = 'Unknown Product';
|
||||||
<tr className="bg-sunk border-b border-border-default">
|
let bags = '0';
|
||||||
{visibleColumns.map(col => (
|
|
||||||
<th key={col.id} className="p-3 text-xs font-semibold text-muted uppercase tracking-wider whitespace-nowrap">
|
visibleColumns.forEach(col => {
|
||||||
{col.name}
|
const isName = col.id.toLowerCase().includes('name') || col.name.toLowerCase().includes('name') || (col.id.toLowerCase().includes('category') && productName === 'Unknown Product');
|
||||||
</th>
|
const isBags = col.id.toLowerCase().includes('bags') || col.name.toLowerCase().includes('bags') || col.id.toLowerCase().includes('quantity');
|
||||||
))}
|
|
||||||
<th className="p-3 text-xs font-semibold text-muted uppercase tracking-wider w-16"></th>
|
if (isName || isBags) {
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{value.map((row, i) => (
|
|
||||||
<tr key={i} className="border-b border-border-subtle last:border-b-0 hover:bg-slate-50/50">
|
|
||||||
{visibleColumns.map(col => (
|
|
||||||
<td key={col.id} className="p-3 text-sm text-strong whitespace-nowrap">
|
|
||||||
{(() => {
|
|
||||||
const val = row[col.id];
|
const val = row[col.id];
|
||||||
let displayVal = String(val ?? '-');
|
let displayVal = String(val ?? '-');
|
||||||
|
|
||||||
@ -185,35 +177,31 @@ export function SmartGridField({
|
|||||||
if (opt) displayVal = opt.label;
|
if (opt) displayVal = opt.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((col.id.toLowerCase().includes('difference') || col.name?.toLowerCase().includes('difference')) && val != null && val !== '') {
|
if (isName && (productName === 'Unknown Product' || col.id.toLowerCase().includes('name'))) {
|
||||||
const numVal = Number(val);
|
productName = displayVal;
|
||||||
if (!isNaN(numVal)) {
|
|
||||||
if (numVal < 0) {
|
|
||||||
displayVal = `${Math.abs(numVal)} kgs less`;
|
|
||||||
} else if (numVal > 0) {
|
|
||||||
displayVal = `${numVal} kgs more`;
|
|
||||||
} else {
|
|
||||||
displayVal = `0 kgs`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (isBags) bags = displayVal;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return displayVal;
|
return (
|
||||||
})()}
|
<div key={i} className="bg-slate-50 border border-slate-200 rounded-xl p-3 shadow-sm flex items-center justify-between">
|
||||||
</td>
|
<div className="flex flex-col min-w-0 pr-4">
|
||||||
))}
|
<span className="font-bold text-slate-800 text-[14px] truncate">{productName}</span>
|
||||||
<td className="p-2 align-middle text-center flex items-center justify-end gap-1">
|
<span className="font-extrabold text-indigo-600 text-[13px] mt-0.5">{bags} Bags</span>
|
||||||
<button type="button" onClick={() => startEdit(i)} className="text-blue-600 hover:bg-blue-50 p-1.5 rounded transition-colors" title="Edit row">
|
</div>
|
||||||
<Pencil size={16} />
|
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<button type="button" onClick={() => startEdit(i)} className="text-indigo-600 bg-white border border-indigo-100 hover:bg-indigo-50 p-1.5 rounded-lg transition-colors flex items-center justify-center shadow-sm" title="Edit">
|
||||||
|
<Pencil size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => removeRow(i)} className="text-ruby-600 hover:bg-ruby-50 p-1.5 rounded transition-colors" title="Remove row">
|
<button type="button" onClick={() => removeRow(i)} className="text-red-600 bg-white border border-red-100 hover:bg-red-50 p-1.5 rounded-lg transition-colors flex items-center justify-center shadow-sm" title="Remove">
|
||||||
<Trash2 size={16} />
|
<Trash2 size={14} />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
))}
|
);
|
||||||
</tbody>
|
})}
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export function StatsTile({ tile, idx = 0 }: StatsTileProps) {
|
|||||||
const isDanger = lowerKey.includes('no_order');
|
const isDanger = lowerKey.includes('no_order');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-stats-tile z-stats-tile--secondary">
|
<div className={`z-stats-tile z-stats-tile--secondary ${isDanger ? 'z-stats-tile--danger' : ''}`}>
|
||||||
<div className="z-stats-header">
|
<div className="z-stats-header">
|
||||||
<span className={`z-stats-label`}>{displayLabel}</span>
|
<span className={`z-stats-label`}>{displayLabel}</span>
|
||||||
<Icon size={16} className={`z-stats-icon ${isDanger ? 'z-stats-icon--danger' : ''}`} />
|
<Icon size={16} className={`z-stats-icon ${isDanger ? 'z-stats-icon--danger' : ''}`} />
|
||||||
|
|||||||
@ -12,27 +12,24 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Horizontal Scroll Container */
|
/* Vertical Layout Container */
|
||||||
.z-stats-container {
|
.z-stats-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: auto;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
/* For box-shadow clipping */
|
|
||||||
scroll-snap-type: x mandatory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base Tile */
|
/* Base Tile */
|
||||||
.z-stats-tile {
|
.z-stats-tile {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 160px;
|
width: 100%;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border-radius: var(--z-border-radius-lg);
|
border-radius: var(--z-border-radius-lg);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
scroll-snap-align: start;
|
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
box-shadow: var(--block-shadow);
|
box-shadow: var(--block-shadow);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
@ -42,6 +39,7 @@
|
|||||||
.z-stats-tile--secondary {
|
.z-stats-tile--secondary {
|
||||||
background-color: var(--z-bg-neutral-100);
|
background-color: var(--z-bg-neutral-100);
|
||||||
border: 1px solid var(--z-border-neutral-300);
|
border: 1px solid var(--z-border-neutral-300);
|
||||||
|
border-left: 4px solid var(--z-text-primary-400);
|
||||||
color: var(--z-text-neutral-900);
|
color: var(--z-text-neutral-900);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +91,10 @@
|
|||||||
color: var(--z-text-danger-400);
|
color: var(--z-text-danger-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.z-stats-tile--secondary.z-stats-tile--danger {
|
||||||
|
border-left-color: var(--z-text-danger-400);
|
||||||
|
}
|
||||||
|
|
||||||
.z-stats-footer {
|
.z-stats-footer {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { orderBookingClient } from '../api/clients';
|
|||||||
import { ORDER_BOOKING } from '../api/config';
|
import { ORDER_BOOKING } from '../api/config';
|
||||||
import { Spinner } from '../components/reusable/Spinner';
|
import { Spinner } from '../components/reusable/Spinner';
|
||||||
import { EmptyState } from '../components/reusable/EmptyState';
|
import { EmptyState } from '../components/reusable/EmptyState';
|
||||||
|
import { StatsTiles } from '../components/reusable/StatsTiles';
|
||||||
import { Activity, Package, ShoppingBag, BarChart3, TrendingUp, Phone } from 'lucide-react';
|
import { Activity, Package, ShoppingBag, BarChart3, TrendingUp, Phone } from 'lucide-react';
|
||||||
import { formatValue } from '../lib/format';
|
import { formatValue } from '../lib/format';
|
||||||
import {
|
import {
|
||||||
@ -63,18 +64,7 @@ export function AnalyticsPage() {
|
|||||||
if (loading) return <div className="h-full flex items-center justify-center"><Spinner label="Loading Analytics..." /></div>;
|
if (loading) return <div className="h-full flex items-center justify-center"><Spinner label="Loading Analytics..." /></div>;
|
||||||
if (error) return <EmptyState title="Analytics Error" hint={error} />;
|
if (error) return <EmptyState title="Analytics Error" hint={error} />;
|
||||||
|
|
||||||
// Tile Helper
|
|
||||||
const getTileLabelAndIcon = (key: string) => {
|
|
||||||
switch (key) {
|
|
||||||
case 'total_orders': return { label: 'Total Orders', icon: ShoppingBag, color: 'text-indigo-500', bg: 'bg-indigo-50' };
|
|
||||||
case 'total_bags': return { label: 'Total Bags', icon: Package, color: 'text-amber-500', bg: 'bg-amber-50' };
|
|
||||||
case 'total_kgs': return { label: 'Total KGs', icon: Activity, color: 'text-emerald-500', bg: 'bg-emerald-50' };
|
|
||||||
case 'total_calls': return { label: 'Total Calls', icon: Phone, color: 'text-blue-500', bg: 'bg-blue-50' };
|
|
||||||
case 'total_productive_calls': return { label: 'Productive Calls', icon: TrendingUp, color: 'text-green-500', bg: 'bg-green-50' };
|
|
||||||
case 'no_order_calls': return { label: 'No Order Calls', icon: Activity, color: 'text-red-500', bg: 'bg-red-50' };
|
|
||||||
default: return { label: key.replace(/_/g, ' '), icon: BarChart3, color: 'text-slate-500', bg: 'bg-slate-50' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderChart = (chart: ChartData, index: number) => {
|
const renderChart = (chart: ChartData, index: number) => {
|
||||||
const data = chart.rows.map(r => ({
|
const data = chart.rows.map(r => ({
|
||||||
@ -149,24 +139,7 @@ export function AnalyticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tiles Grid */}
|
{/* Tiles Grid */}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
<StatsTiles tiles={tiles as any} />
|
||||||
{tiles.map((t) => {
|
|
||||||
const { label, icon: Icon, color, bg } = getTileLabelAndIcon(t.key);
|
|
||||||
return (
|
|
||||||
<div key={t.tile_uid} className="bg-white rounded-2xl p-4 shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-100 flex flex-col gap-3 transition-transform hover:-translate-y-1 hover:shadow-lg">
|
|
||||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${bg} ${color}`}>
|
|
||||||
<Icon size={20} strokeWidth={2.5} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider line-clamp-1">{label}</span>
|
|
||||||
<span className="text-xl font-black text-slate-800 tracking-tight">
|
|
||||||
{t.key === 'total_kgs' ? Number(t.value).toLocaleString(undefined, { maximumFractionDigits: 1 }) : formatValue(t.value)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Charts Grid */}
|
{/* Charts Grid */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 pb-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 pb-4">
|
||||||
|
|||||||
@ -126,7 +126,6 @@ export function CallsPage() {
|
|||||||
<Button size="sm" onClick={(e) => {
|
<Button size="sm" onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("Clicked Edit Order, ID:", ORDER_BOOKING.activities.EDIT_ORDER?.uid);
|
|
||||||
setActiveActivity({ id: ORDER_BOOKING.activities.EDIT_ORDER.uid, name: 'Edit Order' });
|
setActiveActivity({ id: ORDER_BOOKING.activities.EDIT_ORDER.uid, name: 'Edit Order' });
|
||||||
setIsFabOpen(false);
|
setIsFabOpen(false);
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@ -31,13 +31,13 @@ export interface ScreenDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SCREENS: ScreenDef[] = [
|
export const SCREENS: ScreenDef[] = [
|
||||||
{ key: 'analytics', label: 'Analytics', icon: BarChart2, View: AnalyticsPage as any },
|
|
||||||
{ key: 'sales-report', label: 'Sales Report', icon: FileText, View: DailySalesReportPage as any },
|
|
||||||
{ key: 'daily', label: 'Home', icon: Home, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
{ key: 'daily', label: 'Home', icon: Home, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
||||||
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
{ key: 'orders', label: 'Orders', icon: ShoppingCart, View: OrdersView, Detail: OrderDetail, noun: 'Order' },
|
||||||
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
{ key: 'calls', label: 'Calls', icon: Phone, View: CallsView, Detail: CallDetail, noun: 'Call' },
|
||||||
{ key: 'stores', label: 'Stores', icon: Store, View: StoresView, Detail: StoreDetail, noun: 'Store' },
|
{ key: 'stores', label: 'Stores', icon: Store, View: StoresView, Detail: StoreDetail, noun: 'Store' },
|
||||||
{ key: 'all-daily', label: 'All Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
{ key: 'all-daily', label: 'All Logs', icon: ClipboardList, View: DailyLogsView, Detail: DailyLogDetail, noun: 'Daily Log' },
|
||||||
|
{ key: 'analytics', label: 'Analytics', icon: BarChart2, View: AnalyticsPage as any },
|
||||||
|
{ key: 'sales-report', label: 'Sales Report', icon: FileText, View: DailySalesReportPage as any }
|
||||||
];
|
];
|
||||||
|
|
||||||
export function screenByKey(key: string | undefined): ScreenDef | undefined {
|
export function screenByKey(key: string | undefined): ScreenDef | undefined {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user