Compare commits
No commits in common. "69be65f39f3f25bee4e0e824a1755da2f431f06d" and "ff2da725e28204d70dbd1eb33cbb726711aa3110" have entirely different histories.
69be65f39f
...
ff2da725e2
@ -10,7 +10,7 @@ import { DailyLogsPage } from './screens/DailyLogsPage'
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<BrowserRouter basename='/krishna_sales_mobile'>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route element={<ConsoleLayout />}>
|
<Route element={<ConsoleLayout />}>
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export const ORDER_BOOKING = {
|
|||||||
},
|
},
|
||||||
recordViews: {
|
recordViews: {
|
||||||
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
|
ORDERS: '1e424561-9a27-44f5-9b68-64d63296d837',
|
||||||
CALLS: '56b21b46-6d2c-4e10-b5a3-c63d058d0afa',
|
CALLS: 'b4d7a710-24e7-416d-885a-31fd1e727aab',
|
||||||
},
|
},
|
||||||
detailViews: {
|
detailViews: {
|
||||||
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',
|
ORDERS: '0804c6c3-6cf9-4050-94bb-fa48dd5de87d',
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
import { formatValue } from '../../lib/format';
|
|
||||||
import { Badge } from '../reusable/Badge';
|
|
||||||
import { CheckCircle2Icon, Factory, UserIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
export function OrderCard({ row, fields }: { row: Record<string, any>; fields: any[] }) {
|
|
||||||
const orderIdStr = row.order_id;
|
|
||||||
|
|
||||||
// Extract store lookup object if it exists
|
|
||||||
const storeObj = (row.select_store || row.store) as Record<string, unknown> | null;
|
|
||||||
const storeVal = formatValue(storeObj?.business_name || '—');
|
|
||||||
const routeVal = formatValue(storeObj?.route_name || '—');
|
|
||||||
const distributorVal = formatValue(storeObj?.distributor_name || '—');
|
|
||||||
|
|
||||||
// Extract user/performed_by object
|
|
||||||
const userKey = Object.keys(row).find(k => k.includes('user_id') || k.includes('created_by'));
|
|
||||||
const userObj = userKey ? row[userKey] as Record<string, unknown> : null;
|
|
||||||
const userVal = formatValue(userObj?.name || userObj?.user_name || '—');
|
|
||||||
|
|
||||||
const dateVal = row.date_of_order ? formatValue(row.date_of_order) : '—';
|
|
||||||
|
|
||||||
const stateName = String(row.current_state_name || '');
|
|
||||||
const isProductive = stateName.toLowerCase().includes('productive') || stateName.toLowerCase().includes('closed');
|
|
||||||
const productiveLabel = stateName || 'Pending';
|
|
||||||
|
|
||||||
// Order Details Grid
|
|
||||||
const gridField = fields.find(f => f.data_type === 'grid' || f.field_key === 'order_details');
|
|
||||||
const gridValRaw = gridField ? row[gridField.field_key] : row.order_details;
|
|
||||||
const gridVal = Array.isArray(gridValRaw) ? gridValRaw : [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full bg-white dark:bg-slate-900 rounded-2xl shadow-[0_2px_12px_-4px_rgba(0,0,0,0.08)] border border-slate-200/75 dark:border-slate-800 p-0 overflow-hidden transition-all hover:shadow-[0_8px_24px_-8px_rgba(0,0,0,0.12)]">
|
|
||||||
|
|
||||||
{/* Top Header Section */}
|
|
||||||
<div className="p-4 bg-slate-50 dark:bg-slate-800/40 border-b border-slate-100 dark:border-slate-800">
|
|
||||||
<div className="flex justify-between items-start mb-4">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-0.5">Order ID</span>
|
|
||||||
<span className="text-[17px] font-extrabold text-slate-800 dark:text-slate-100 tracking-tight">{orderIdStr}</span>
|
|
||||||
</div>
|
|
||||||
<Badge tone={isProductive ? 'success' : 'neutral'} className="shadow-sm border-0 font-medium tracking-wide">
|
|
||||||
{isProductive && <CheckCircle2Icon size={14} className="text-emerald-500 mr-1" />}
|
|
||||||
{productiveLabel}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Date</span>
|
|
||||||
<span className="text-[13px] font-semibold text-slate-700 dark:text-slate-300">{dateVal}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-0.5 col-span-2">
|
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Store & Route</span>
|
|
||||||
<span className="text-[13px] font-semibold text-slate-700 dark:text-slate-300 truncate">
|
|
||||||
{storeVal} <span className="text-slate-400 font-normal ml-1">({routeVal})</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Middle Section: Distributor & User */}
|
|
||||||
<div className="px-4 py-3 bg-slate-50/60 dark:bg-slate-800/20 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between gap-2">
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
|
||||||
<div className="w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900/40 flex items-center justify-center shrink-0 text-indigo-600 dark:text-indigo-400">
|
|
||||||
<Factory size={14} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col min-w-0">
|
|
||||||
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">Distributor</span>
|
|
||||||
<span className="text-[12px] font-semibold text-slate-700 dark:text-slate-300 truncate">{distributorVal}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-px h-8 bg-slate-200 dark:bg-slate-700 shrink-0 mx-2"></div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2.5 min-w-0 flex-1 justify-end">
|
|
||||||
<div className="flex flex-col items-end min-w-0 text-right">
|
|
||||||
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">Placed by</span>
|
|
||||||
<span className="text-[12px] font-semibold text-slate-700 dark:text-slate-300 truncate">{userVal}</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/40 flex items-center justify-center shrink-0 text-blue-600 dark:text-blue-400">
|
|
||||||
<UserIcon size={14} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Bottom Section: Order Items */}
|
|
||||||
{gridVal.length > 0 && (
|
|
||||||
<div className="p-4 bg-white dark:bg-slate-900">
|
|
||||||
<div className="flex justify-between items-center mb-3">
|
|
||||||
<h3 className="text-[11px] font-bold text-slate-500 dark:text-slate-400 uppercase tracking-widest flex items-center">
|
|
||||||
Order Items
|
|
||||||
<span className="ml-2 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 px-2 py-0.5 rounded-full text-[10px]">
|
|
||||||
{gridVal.length}
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
|
||||||
{gridVal.map((item, i) => {
|
|
||||||
const productName = formatValue(item.product_name || item.product_category || 'Unknown Product');
|
|
||||||
const sku = formatValue(item.sku_code || item.sku || 'N/A');
|
|
||||||
const bags = formatValue(item.bags || item.total_bags || item.quantity || 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={i} className="flex items-center justify-between gap-4 p-2.5 rounded-xl bg-slate-50/80 dark:bg-slate-800/30 border border-slate-100 dark:border-slate-800/60 transition-colors">
|
|
||||||
<div className="flex flex-col gap-1 min-w-0">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 text-[9px] uppercase font-bold tracking-widest px-2 py-0.5 rounded-md">
|
|
||||||
{sku}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[13px] font-bold text-slate-700 dark:text-slate-200 truncate">
|
|
||||||
{productName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end justify-center shrink-0 pl-2">
|
|
||||||
<span className="text-[22px] font-black text-slate-800 dark:text-slate-100 leading-none tracking-tighter">
|
|
||||||
{bags}
|
|
||||||
</span>
|
|
||||||
<span className="text-[9px] text-slate-400 font-bold uppercase tracking-widest mt-1">
|
|
||||||
bags
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { orderBookingClient } from '../../api/clients';
|
import { orderBookingClient } from '../../api/clients';
|
||||||
import { ORDER_BOOKING } from '../../api/config';
|
import { ORDER_BOOKING } from '../../api/config';
|
||||||
import { RecordView } from './RecordView';
|
import { RecordView } from './RecordView';
|
||||||
import { OrderCard } from '../cards/OrderCard';
|
|
||||||
|
|
||||||
export interface WiredRecordViewProps {
|
export interface WiredRecordViewProps {
|
||||||
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
onRowClick?: (row: Record<string, unknown>, index: number) => void;
|
||||||
@ -23,7 +22,6 @@ export function OrdersView({ onRowClick, pageSize, headerActions, rowActions, re
|
|||||||
headerActions={headerActions}
|
headerActions={headerActions}
|
||||||
rowActions={rowActions}
|
rowActions={rowActions}
|
||||||
refreshKey={refreshKey}
|
refreshKey={refreshKey}
|
||||||
renderItem={(row, fields) => <OrderCard row={row} fields={fields} />}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Search } from 'lucide-react';
|
import { Search, ChevronRight } from 'lucide-react';
|
||||||
import { cn } from '../../lib/cn';
|
import { cn } from '../../lib/cn';
|
||||||
import { formatValue } from '../../lib/format';
|
import { formatValue } from '../../lib/format';
|
||||||
import type { ZinoClient } from '../../api/client';
|
import type { ZinoClient } from '../../api/client';
|
||||||
@ -32,8 +32,6 @@ export interface RecordViewProps {
|
|||||||
rowActions?: (row: Record<string, unknown>) => React.ReactNode;
|
rowActions?: (row: Record<string, unknown>) => React.ReactNode;
|
||||||
/** Pass a new value to trigger a refresh. */
|
/** Pass a new value to trigger a refresh. */
|
||||||
refreshKey?: number;
|
refreshKey?: number;
|
||||||
/** Custom render function for the entire row card body */
|
|
||||||
renderItem?: (row: Record<string, unknown>, fields: RecordViewField[]) => React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,7 +50,6 @@ export function RecordView({
|
|||||||
headerActions,
|
headerActions,
|
||||||
rowActions,
|
rowActions,
|
||||||
refreshKey,
|
refreshKey,
|
||||||
renderItem,
|
|
||||||
}: RecordViewProps) {
|
}: RecordViewProps) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@ -186,34 +183,30 @@ export function RecordView({
|
|||||||
key={rowKey ? rowKey(row, i) : String(row.instance_id ?? i)}
|
key={rowKey ? rowKey(row, i) : String(row.instance_id ?? i)}
|
||||||
onClick={onRowClick ? () => onRowClick(row, i) : undefined}
|
onClick={onRowClick ? () => onRowClick(row, i) : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'transition-transform duration-150 relative w-full',
|
'rounded-xl border border-border-subtle bg-card p-4 shadow-sm transition-transform duration-150',
|
||||||
!renderItem && 'rounded-xl border border-border-subtle bg-card p-4 shadow-sm',
|
|
||||||
onRowClick && 'cursor-pointer active:scale-[0.99]',
|
onRowClick && 'cursor-pointer active:scale-[0.99]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn(renderItem ? "" : "flex items-center gap-3")}>
|
<div className="flex items-center gap-3">
|
||||||
<div className="min-w-0 flex-1 w-full">
|
<div className="min-w-0 flex-1">
|
||||||
{renderItem ? (
|
{primary && (
|
||||||
renderItem(row, fields)
|
<p className="text-[15px] font-semibold text-strong truncate">
|
||||||
) : (
|
{formatValue(row[primary.field_key])}
|
||||||
<>
|
</p>
|
||||||
{primary && (
|
)}
|
||||||
<p className="text-[15px] font-semibold text-strong truncate">
|
{secondary.length > 0 && (
|
||||||
{formatValue(row[primary.field_key])}
|
<div className="mt-1 flex flex-col gap-0.5">
|
||||||
|
{secondary.map((f) => (
|
||||||
|
<p key={f.field_key} className="text-xs text-muted truncate">
|
||||||
|
<span className="text-faint">{f.output_label}:</span> {formatValue(row[f.field_key])}
|
||||||
</p>
|
</p>
|
||||||
)}
|
))}
|
||||||
{secondary.length > 0 && (
|
</div>
|
||||||
<div className="mt-1 flex flex-col gap-0.5">
|
|
||||||
{secondary.map((f) => (
|
|
||||||
<p key={f.field_key} className="text-xs text-muted truncate">
|
|
||||||
<span className="text-faint">{f.output_label}:</span> {formatValue(row[f.field_key])}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{onRowClick && (
|
||||||
|
<ChevronRight size={18} className="shrink-0 text-slate-300" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{rowActions && (
|
{rowActions && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
|
import { NavLink, Navigate, Outlet, useNavigate } from 'react-router-dom';
|
||||||
import { LogOut, Sun, Moon } from 'lucide-react';
|
import { LogOut } 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';
|
||||||
@ -13,32 +13,6 @@ export function ConsoleLayout() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = orderBookingClient.currentUser();
|
const user = orderBookingClient.currentUser();
|
||||||
|
|
||||||
const [isDark, setIsDark] = useState(() => document.documentElement.classList.contains('dark'));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const saved = localStorage.getItem('theme');
|
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
if (saved === 'dark' || (!saved && prefersDark)) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
setIsDark(true);
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
setIsDark(false);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
if (isDark) {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
localStorage.setItem('theme', 'light');
|
|
||||||
setIsDark(false);
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
setIsDark(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// A 401 from any workflow client bounces back to login.
|
// A 401 from any workflow client bounces back to login.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onAuthErrorAll(() => {
|
onAuthErrorAll(() => {
|
||||||
@ -50,35 +24,23 @@ export function ConsoleLayout() {
|
|||||||
if (!authed) return <Navigate to="/login" replace />;
|
if (!authed) return <Navigate to="/login" replace />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[100dvh] overflow-hidden bg-app flex flex-col">
|
<div className="min-h-screen bg-app flex flex-col">
|
||||||
<header className="sticky top-0 z-20 shrink-0 flex items-center justify-between gap-3 px-4 h-14 pt-[env(safe-area-inset-top)] bg-navy-grad shadow-md">
|
<header className="sticky top-0 z-20 shrink-0 flex items-center justify-between gap-3 px-4 h-14 pt-[env(safe-area-inset-top)] bg-navy-grad shadow-md">
|
||||||
<div className="flex flex-col justify-center leading-none flex-1">
|
<div className="flex flex-col justify-center leading-none">
|
||||||
<span className="text-base font-extrabold text-on-navy tracking-[-0.01em] leading-none">Krishna Sales</span>
|
<span className="text-base font-extrabold text-on-navy tracking-[-0.01em] leading-none">Krishna Sales</span>
|
||||||
<span className="text-2xs text-on-navy-muted">Sandbox {APP_ID}{user?.name ? ` · ${user.name}` : ''}</span>
|
<span className="text-2xs text-on-navy-muted">Sandbox {APP_ID}{user?.name ? ` · ${user.name}` : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
<div className="flex items-center gap-1">
|
type="button"
|
||||||
<button
|
aria-label="Sign out"
|
||||||
type="button"
|
onClick={() => {
|
||||||
aria-label="Toggle Theme"
|
logout();
|
||||||
onClick={toggleTheme}
|
navigate('/login', { replace: true });
|
||||||
className="inline-flex items-center justify-center size-9 rounded-md text-on-navy-muted hover:text-white hover:bg-white/10 transition-colors duration-150 cursor-pointer"
|
}}
|
||||||
>
|
className="inline-flex items-center justify-center size-9 rounded-md text-on-navy-muted hover:text-white hover:bg-white/10 transition-colors duration-150 cursor-pointer"
|
||||||
{isDark ? <Sun size={18} /> : <Moon size={18} />}
|
>
|
||||||
</button>
|
<LogOut size={18} />
|
||||||
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Sign out"
|
|
||||||
onClick={() => {
|
|
||||||
logout();
|
|
||||||
navigate('/login', { replace: true });
|
|
||||||
}}
|
|
||||||
className="inline-flex items-center justify-center size-9 rounded-md text-on-navy-muted hover:text-white hover:bg-white/10 transition-colors duration-150 cursor-pointer"
|
|
||||||
>
|
|
||||||
<LogOut size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto px-4 pt-4 pb-24">
|
<main className="flex-1 overflow-y-auto px-4 pt-4 pb-24">
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
============================================================ */
|
============================================================ */
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700;14..32,800&family=Inter+Tight:wght@500;600;700;800&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700;14..32,800&family=Inter+Tight:wght@500;600;700;800&display=swap');
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
|
||||||
@import './tokens/colors.css';
|
@import './tokens/colors.css';
|
||||||
@import './tokens/typography.css';
|
@import './tokens/typography.css';
|
||||||
@import './tokens/spacing.css';
|
@import './tokens/spacing.css';
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import tailwindcss from '@tailwindcss/vite'
|
|||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
const base = process.env.VITE_BASE_URL ?? '/krishna_sales_mobile'
|
const base = process.env.VITE_BASE_URL ?? '/'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base,
|
base,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user