From bd90a3d68e4cdd422d9ce5c8c265fb0b2178b2e8 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 29 Sep 2026 12:08:54 +0530 Subject: [PATCH] dashboard design --- .../_components/dashboard-client.tsx | 665 ++++++++++++++++++ .../rides/_components/ride-sheet.tsx | 13 +- distribution_fe/app/(dashboard)/page.tsx | 9 +- distribution_fe/app/globals.css | 7 + distribution_fe/lib/api/operations.ts | 10 +- distribution_fe/lib/api/sales.ts | 6 + distribution_fe/lib/api/stock.ts | 4 + distribution_fe/types/api.ts | 14 + 8 files changed, 714 insertions(+), 14 deletions(-) create mode 100644 distribution_fe/app/(dashboard)/_components/dashboard-client.tsx diff --git a/distribution_fe/app/(dashboard)/_components/dashboard-client.tsx b/distribution_fe/app/(dashboard)/_components/dashboard-client.tsx new file mode 100644 index 0000000..c2d1997 --- /dev/null +++ b/distribution_fe/app/(dashboard)/_components/dashboard-client.tsx @@ -0,0 +1,665 @@ +'use client' + +import { useCallback, useEffect, useMemo, useState } from 'react' +import { HugeiconsIcon } from '@hugeicons/react' +import { Add01Icon, Cancel01Icon } from '@hugeicons/core-free-icons' +import { ResponsiveGridLayout, DEFAULT_BREAKPOINTS, DEFAULT_COLS, useContainerWidth } from 'react-grid-layout' +import type { Layout, LayoutItem } from 'react-grid-layout' +import 'react-grid-layout/css/styles.css' + +import { Button } from '@/components/ui/button' +import { Checkbox } from '@/components/ui/checkbox' +import { Skeleton } from '@/components/ui/skeleton' +import { toast } from '@/components/ui/toast' +import { + shopPos, + rides, + loadingSheets, + deliveryTasks, + stock, + invoices, + getApiError, + getPaginatedItems, +} from '@/lib/api/index' +import type { LowStockItemResponse, StockMovementResponse } from '@/types/api' + +const STORAGE_KEY = 'distribution-dashboard-widgets-v3' +const ROW_HEIGHT = 36 +const GRID_MARGIN: [number, number] = [16, 16] + +type Breakpoint = keyof typeof DEFAULT_BREAKPOINTS +const BREAKPOINT_ORDER = Object.keys(DEFAULT_BREAKPOINTS) as Breakpoint[] + +type Category = 'sales' | 'ops' | 'stock' | 'finance' +type Size = 'sm' | 'md' | 'lg' + +interface WidgetMeta { + id: string + cat: Category + title: string + size: Size +} + +const CATEGORY_META: Record = { + sales: { label: 'Sales & Fulfillment', text: 'text-blue-600 dark:text-blue-400', bg: 'bg-blue-50 dark:bg-blue-500/10', solid: 'bg-blue-600' }, + ops: { label: 'Operations', text: 'text-violet-600 dark:text-violet-400', bg: 'bg-violet-50 dark:bg-violet-500/10', solid: 'bg-violet-600' }, + stock: { label: 'Stock', text: 'text-amber-600 dark:text-amber-400', bg: 'bg-amber-50 dark:bg-amber-500/10', solid: 'bg-amber-600' }, + finance: { label: 'Finance', text: 'text-emerald-600 dark:text-emerald-400', bg: 'bg-emerald-50 dark:bg-emerald-500/10', solid: 'bg-emerald-600' }, +} + +const WIDGET_META: WidgetMeta[] = [ + { id: 'pendingPO', cat: 'sales', title: 'Pending Shop POs', size: 'sm' }, + { id: 'awaitingConfirm', cat: 'sales', title: 'Awaiting Confirmation', size: 'sm' }, + { id: 'fulfillRate', cat: 'sales', title: 'Fulfillment Rate', size: 'md' }, + { id: 'openRides', cat: 'ops', title: 'Open Rides', size: 'md' }, + { id: 'loadingSheetsByStatus', cat: 'ops', title: 'Loading Sheets by Status', size: 'md' }, + { id: 'deliveryTasks', cat: 'ops', title: 'Pending Delivery Tasks', size: 'sm' }, + { id: 'lowStock', cat: 'stock', title: 'Low Stock Alerts', size: 'lg' }, + { id: 'stockMovements', cat: 'stock', title: 'Recent Stock Movements', size: 'lg' }, + { id: 'warehouseBalance', cat: 'stock', title: 'Warehouse Balances', size: 'md' }, + { id: 'outstandingPay', cat: 'finance', title: 'Outstanding Payments', size: 'sm' }, + { id: 'invoiceStatus', cat: 'finance', title: 'Invoices by Status', size: 'md' }, +] + +const DEFAULT_ACTIVE = [ + 'pendingPO', 'awaitingConfirm', 'fulfillRate', 'openRides', 'loadingSheetsByStatus', + 'deliveryTasks', 'lowStock', 'outstandingPay', 'invoiceStatus', +] + +/** Width (in that breakpoint's own columns), height, and minimums per widget size, per breakpoint. Narrower breakpoints + * give small/medium widgets a full-width row instead of shrinking them into unreadably thin columns. */ +const SIZE_CONFIG: Record> = { + lg: { + sm: { w: 3, h: 4, minW: 2, minH: 3 }, + md: { w: 6, h: 5, minW: 3, minH: 3 }, + lg: { w: 12, h: 7, minW: 4, minH: 4 }, + }, + md: { + sm: { w: 5, h: 4, minW: 3, minH: 3 }, + md: { w: 10, h: 5, minW: 4, minH: 3 }, + lg: { w: 10, h: 7, minW: 4, minH: 4 }, + }, + sm: { + sm: { w: 3, h: 4, minW: 2, minH: 3 }, + md: { w: 6, h: 5, minW: 3, minH: 3 }, + lg: { w: 6, h: 7, minW: 3, minH: 4 }, + }, + xs: { + sm: { w: 4, h: 4, minW: 2, minH: 3 }, + md: { w: 4, h: 5, minW: 2, minH: 3 }, + lg: { w: 4, h: 7, minW: 2, minH: 4 }, + }, + xxs: { + sm: { w: 2, h: 4, minW: 1, minH: 3 }, + md: { w: 2, h: 5, minW: 1, minH: 3 }, + lg: { w: 2, h: 7, minW: 1, minH: 4 }, + }, +} + +/** Lays widgets out left-to-right for one breakpoint's column count, wrapping to a new row when a widget won't fit. */ +function generateFlowLayout(ids: string[], bp: Breakpoint): LayoutItem[] { + const cols = DEFAULT_COLS[bp] + const metas = WIDGET_META.filter(w => ids.includes(w.id)) + let x = 0 + let y = 0 + let rowH = 0 + const items: LayoutItem[] = [] + for (const m of metas) { + const cfg = SIZE_CONFIG[bp][m.size] + if (x + cfg.w > cols) { + x = 0 + y += rowH + rowH = 0 + } + items.push({ i: m.id, x, y, w: cfg.w, h: cfg.h, minW: cfg.minW, minH: cfg.minH }) + x += cfg.w + rowH = Math.max(rowH, cfg.h) + } + return items +} + +function generateFlowLayouts(ids: string[]): Record { + return Object.fromEntries(BREAKPOINT_ORDER.map(bp => [bp, generateFlowLayout(ids, bp)])) as Record +} + +/** Places a newly-added widget below everything else on a given breakpoint, so it never overlaps what's already there. */ +function appendWidget(layout: LayoutItem[], meta: WidgetMeta, bp: Breakpoint): LayoutItem[] { + const cfg = SIZE_CONFIG[bp][meta.size] + const y = layout.reduce((max, item) => Math.max(max, item.y + item.h), 0) + return [...layout, { i: meta.id, x: 0, y, w: cfg.w, h: cfg.h, minW: cfg.minW, minH: cfg.minH }] +} + +interface DashboardLayoutState { + activeIds: string[] + layouts: Record +} + +function defaultLayoutState(): DashboardLayoutState { + return { activeIds: [...DEFAULT_ACTIVE], layouts: generateFlowLayouts(DEFAULT_ACTIVE) } +} + +function loadStoredLayoutState(): DashboardLayoutState { + try { + const raw = window.localStorage.getItem(STORAGE_KEY) + if (!raw) return defaultLayoutState() + + const parsed = JSON.parse(raw) + const validIds = new Set(WIDGET_META.map(w => w.id)) + const activeIds: string[] = Array.isArray(parsed?.activeIds) + ? parsed.activeIds.filter((id: unknown): id is string => typeof id === 'string' && validIds.has(id)) + : [] + if (activeIds.length === 0) return defaultLayoutState() + + const layouts = {} as Record + for (const bp of BREAKPOINT_ORDER) { + const storedLayout: LayoutItem[] = Array.isArray(parsed?.layouts?.[bp]) + ? parsed.layouts[bp].filter((l: unknown): l is LayoutItem => + !!l && typeof l === 'object' && typeof (l as LayoutItem).i === 'string' && validIds.has((l as LayoutItem).i) && activeIds.includes((l as LayoutItem).i)) + : [] + const missingIds = activeIds.filter(id => !storedLayout.some(l => l.i === id)) + layouts[bp] = missingIds.length > 0 ? [...storedLayout, ...generateFlowLayout(missingIds, bp)] : storedLayout + } + + return { activeIds, layouts } + } catch { + return defaultLayoutState() + } +} + +const fmtMoney = (v: number) => `Rs. ${v.toLocaleString('en-LK', { minimumFractionDigits: 2 })}` + +interface OpenRide { + id: string + vehicleNumberPlate: string + driverFullName: string +} + +interface WarehouseBalanceRow { + locationId: string + name: string + quantity: number +} + +interface DashboardData { + pendingPOCount: number + awaitingConfirmCount: number + totalPOCount: number + fulfilledPOCount: number + openRides: OpenRide[] + plannedSheetsCount: number + confirmedSheetsCount: number + pendingTasksCount: number + partialTasksCount: number + lowStock: LowStockItemResponse[] + movements: StockMovementResponse[] + warehouseBalances: WarehouseBalanceRow[] + outstandingAmount: number + outstandingInvoiceCount: number + draftInvoiceCount: number + printedInvoiceCount: number + finalizedInvoiceCount: number +} + +async function loadDashboardData(): Promise { + const [ + pendingPO, awaitingConfirm, allPO, fulfilledPO, + openRidesRes, + plannedSheets, confirmedSheets, + pendingTasks, partialTasks, + lowStock, + movements, + balances, + outstanding, + draftInv, printedInv, finalizedInv, + ] = await Promise.all([ + shopPos.list({ status: 'Pending', pageSize: 1 }), + shopPos.list({ confirmed: true, status: 'Pending', pageSize: 1 }), + shopPos.list({ pageSize: 1 }), + shopPos.list({ status: 'Fulfilled', pageSize: 1 }), + rides.list({ status: 'Open', pageSize: 5 }), + loadingSheets.list({ status: 'Planned', pageSize: 1 }), + loadingSheets.list({ status: 'Confirmed', pageSize: 1 }), + deliveryTasks.list({ status: 'Pending', pageSize: 1 }), + deliveryTasks.list({ status: 'PartiallyDelivered', pageSize: 1 }), + stock.lowStock(), + stock.movements({ pageSize: 5 }), + stock.balances({ locationType: 'Warehouse', pageSize: 300 }), + invoices.outstandingSummary(), + invoices.list({ status: 'Draft', pageSize: 1 }), + invoices.list({ status: 'Printed', pageSize: 1 }), + invoices.list({ status: 'Finalized', pageSize: 1 }), + ]) + + const balanceRows = getPaginatedItems(balances) + const byWarehouse = new Map() + for (const b of balanceRows) { + const existing = byWarehouse.get(b.locationId) + if (existing) existing.quantity += b.quantity + else byWarehouse.set(b.locationId, { locationId: b.locationId, name: b.locationName, quantity: b.quantity }) + } + const warehouseBalances = Array.from(byWarehouse.values()) + .sort((a, b) => b.quantity - a.quantity) + .slice(0, 5) + + return { + pendingPOCount: pendingPO.total, + awaitingConfirmCount: awaitingConfirm.total, + totalPOCount: allPO.total, + fulfilledPOCount: fulfilledPO.total, + openRides: getPaginatedItems(openRidesRes).map(r => ({ id: r.id, vehicleNumberPlate: r.vehicleNumberPlate, driverFullName: r.driverFullName })), + plannedSheetsCount: plannedSheets.total, + confirmedSheetsCount: confirmedSheets.total, + pendingTasksCount: pendingTasks.total, + partialTasksCount: partialTasks.total, + lowStock: getPaginatedItems(lowStock).slice(0, 5), + movements: getPaginatedItems(movements), + warehouseBalances, + outstandingAmount: outstanding.outstandingAmount, + outstandingInvoiceCount: outstanding.invoiceCount, + draftInvoiceCount: draftInv.total, + printedInvoiceCount: printedInv.total, + finalizedInvoiceCount: finalizedInv.total, + } +} + +function CategoryPill({ cat }: { cat: Category }) { + const meta = CATEGORY_META[cat] + return ( + + {meta.label} + + ) +} + +function Bar({ pct, colorClass }: { pct: number; colorClass: string }) { + return ( +
+
+
+ ) +} + +function WidgetBody({ id, data }: { id: string; data: DashboardData }) { + switch (id) { + case 'pendingPO': + return ( +
+
{data.pendingPOCount}
+
{data.totalPOCount} total orders
+
+ ) + case 'awaitingConfirm': + return ( +
+
{data.awaitingConfirmCount}
+
Confirmed, not yet loaded
+
+ ) + case 'fulfillRate': { + const pct = data.totalPOCount > 0 ? Math.round((data.fulfilledPOCount / data.totalPOCount) * 100) : 0 + return ( +
+
{pct}%
+ +
{data.fulfilledPOCount} of {data.totalPOCount} orders fulfilled
+
+ ) + } + case 'openRides': + return data.openRides.length === 0 ? ( +

No open rides right now.

+ ) : ( +
+ {data.openRides.map(r => ( +
+
+
{r.vehicleNumberPlate}
+
{r.driverFullName}
+
+ Open +
+ ))} +
+ ) + case 'loadingSheetsByStatus': { + const total = data.plannedSheetsCount + data.confirmedSheetsCount + const plannedPct = total > 0 ? (data.plannedSheetsCount / total) * 100 : 0 + const confirmedPct = total > 0 ? (data.confirmedSheetsCount / total) * 100 : 0 + return ( +
+
+
+ Planned + {data.plannedSheetsCount} +
+ +
+
+
+ Confirmed + {data.confirmedSheetsCount} +
+ +
+
+ ) + } + case 'deliveryTasks': + return ( +
+
{data.pendingTasksCount}
+
{data.partialTasksCount} partially delivered
+
+ ) + case 'lowStock': + return data.lowStock.length === 0 ? ( +

No items below their reorder level.

+ ) : ( +
+
+ ItemWarehouseOn handReorder +
+ {data.lowStock.map(row => ( +
+ {row.itemName} + {row.locationName} + {row.quantity} + {row.reorderLevel} +
+ ))} +
+ ) + case 'stockMovements': + return data.movements.length === 0 ? ( +

No recent stock movements.

+ ) : ( +
+
+ ItemTypeQtyRoute +
+ {data.movements.map(m => ( +
+ {m.itemName} + {m.type} + {m.quantity} + {m.fromLocationName ?? '—'} → {m.toLocationName ?? '—'} +
+ ))} +
+ ) + case 'warehouseBalance': { + const max = Math.max(1, ...data.warehouseBalances.map(w => w.quantity)) + return data.warehouseBalances.length === 0 ? ( +

No warehouse stock recorded.

+ ) : ( +
+ {data.warehouseBalances.map(w => ( +
+
+ {w.name} + {w.quantity} +
+ +
+ ))} +
+ ) + } + case 'outstandingPay': + return ( +
+
{fmtMoney(data.outstandingAmount)}
+
{data.outstandingInvoiceCount} invoices
+
+ ) + case 'invoiceStatus': { + const total = data.draftInvoiceCount + data.printedInvoiceCount + data.finalizedInvoiceCount + const segs = [ + { label: 'Draft', count: data.draftInvoiceCount, color: 'bg-slate-400' }, + { label: 'Printed', count: data.printedInvoiceCount, color: 'bg-amber-600' }, + { label: 'Finalized', count: data.finalizedInvoiceCount, color: 'bg-emerald-600' }, + ] + return ( +
+
+ {segs.map(s => ( +
0 ? `${(s.count / total) * 100}%` : '0%' }} /> + ))} +
+
+ {segs.map(s => ( +
+ + + {s.label} + + {s.count} +
+ ))} +
+
+ ) + } + default: + return null + } +} + +function mergeLayoutsChange( + full: Record, + changed: Partial>, +): Record { + const next = { ...full } + for (const bp of BREAKPOINT_ORDER) { + const changedArr = changed[bp] + if (!changedArr) continue + next[bp] = full[bp].map(item => { + const updated = changedArr.find(c => c.i === item.i) + return updated ? { ...item, x: updated.x, y: updated.y, w: updated.w, h: updated.h } : item + }) + } + return next +} + +export function DashboardClient() { + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [state, setState] = useState(defaultLayoutState) + const [editMode, setEditMode] = useState(false) + const [showPicker, setShowPicker] = useState(false) + const [category, setCategory] = useState<'all' | Category>('all') + const { width, containerRef, mounted } = useContainerWidth({ initialWidth: 1200 }) + + useEffect(() => { + setState(loadStoredLayoutState()) + }, []) + + const persist = useCallback((next: DashboardLayoutState) => { + setState(next) + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) + } catch { + // localStorage unavailable (private browsing, etc.) — layout just won't persist + } + }, []) + + const fetchData = useCallback(async () => { + setLoading(true) + try { + setData(await loadDashboardData()) + } catch (err) { + toast.add({ title: 'Failed to load dashboard', description: getApiError(err), type: 'error' }) + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + fetchData() + }, [fetchData]) + + function toggleWidget(id: string, checked: boolean) { + if (checked) { + const meta = WIDGET_META.find(w => w.id === id) + if (!meta || state.activeIds.includes(id)) return + const layouts = { ...state.layouts } + for (const bp of BREAKPOINT_ORDER) layouts[bp] = appendWidget(layouts[bp], meta, bp) + persist({ activeIds: [...state.activeIds, id], layouts }) + } else { + const layouts = { ...state.layouts } + for (const bp of BREAKPOINT_ORDER) layouts[bp] = layouts[bp].filter(l => l.i !== id) + persist({ activeIds: state.activeIds.filter(x => x !== id), layouts }) + } + } + + function handleLayoutChange(_layout: Layout, changedLayouts: Partial>) { + setState(prev => { + const next = { ...prev, layouts: mergeLayoutsChange(prev.layouts, changedLayouts) } + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) + } catch { + // localStorage unavailable — position just won't persist + } + return next + }) + } + + const visibleWidgets = useMemo( + () => state.activeIds + .map(id => WIDGET_META.find(w => w.id === id)) + .filter((w): w is WidgetMeta => Boolean(w)) + .filter(w => category === 'all' || w.cat === category), + [state.activeIds, category], + ) + + const visibleLayouts = useMemo(() => { + const visibleIds = new Set(visibleWidgets.map(w => w.id)) + return Object.fromEntries( + BREAKPOINT_ORDER.map(bp => [bp, state.layouts[bp].filter(l => visibleIds.has(l.i))]), + ) as Record + }, [state.layouts, visibleWidgets]) + + const filters: Array<{ key: 'all' | Category; label: string }> = [ + { key: 'all', label: 'All widgets' }, + { key: 'sales', label: CATEGORY_META.sales.label }, + { key: 'ops', label: CATEGORY_META.ops.label }, + { key: 'stock', label: CATEGORY_META.stock.label }, + { key: 'finance', label: CATEGORY_META.finance.label }, + ] + + return ( +
+
+
+

Dashboard

+

+ {editMode ? 'Drag to move, drag a corner to resize, or tick which widgets show.' : 'Add, remove, drag and resize widgets to make this view yours.'} +

+
+ +
+ {editMode && ( + + )} + + + {showPicker && ( +
+
+ Widgets + +
+
+ {WIDGET_META.map(w => { + const checked = state.activeIds.includes(w.id) + return ( + + ) + })} +
+
+ )} +
+
+ +
+ {filters.map(f => ( + + ))} +
+ + {loading || !data ? ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + ))} +
+ ) : visibleWidgets.length === 0 ? ( +
+ No widgets on this dashboard yet. Click “Widgets” to add some. +
+ ) : ( +
+ {mounted && ( + + {visibleWidgets.map(w => ( +
+ {editMode && ( + + )} +
+
{w.title}
+
+
+ +
+ ))} +
+ )} +
+ )} +
+ ) +} diff --git a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx index bb5412b..54886af 100644 --- a/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx +++ b/distribution_fe/app/(dashboard)/operations/rides/_components/ride-sheet.tsx @@ -34,7 +34,7 @@ import type { VehicleResponse, DriverProfileResponse } from '@/types/api' const schema = z.object({ vehicleId: z.string().min(1, 'Vehicle is required'), driverProfileId: z.string().min(1, 'Driver is required'), - meterstart: z.number().min(0, 'Meter start must be 0 or more'), + meterstart: z.number().min(0, 'Meter start must be 0 or more').optional(), }) type FormValues = z.infer @@ -54,7 +54,7 @@ export function RideSheet({ open, onClose, onSuccess }: RideSheetProps) { defaultValues: { vehicleId: '', driverProfileId: '', - meterstart: 0, + meterstart: undefined, }, }) @@ -64,7 +64,7 @@ export function RideSheet({ open, onClose, onSuccess }: RideSheetProps) { form.reset({ vehicleId: '', driverProfileId: '', - meterstart: 0, + meterstart: undefined, }) Promise.all([ @@ -82,7 +82,7 @@ export function RideSheet({ open, onClose, onSuccess }: RideSheetProps) { async function onSubmit(values: FormValues) { try { - await ridesApi.create(values) + await ridesApi.create({ ...values, meterstart: values.meterstart ?? 0 }) toast.add({ title: 'Ride created', type: 'success' }) onSuccess() onClose() @@ -149,7 +149,6 @@ export function RideSheet({ open, onClose, onSuccess }: RideSheetProps) { control={form.control} name="meterstart" label="Meter Start (km)" - required render={({ field, invalid }) => ( field.onChange(Number(e.target.value))} + value={field.value === undefined ? '' : Number(field.value as number)} + onChange={(e) => field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} onBlur={field.onBlur} placeholder="0" aria-invalid={invalid} diff --git a/distribution_fe/app/(dashboard)/page.tsx b/distribution_fe/app/(dashboard)/page.tsx index 342568f..820818d 100644 --- a/distribution_fe/app/(dashboard)/page.tsx +++ b/distribution_fe/app/(dashboard)/page.tsx @@ -1,8 +1,5 @@ +import { DashboardClient } from './_components/dashboard-client' + export default function DashboardPage() { - return ( -
-

Dashboard

-

Welcome to the Distribution ERP.

-
- ) + return } diff --git a/distribution_fe/app/globals.css b/distribution_fe/app/globals.css index e8597e1..ff09f81 100644 --- a/distribution_fe/app/globals.css +++ b/distribution_fe/app/globals.css @@ -126,4 +126,11 @@ html { @apply font-sans; } +} + +/* react-grid-layout's default resize-handle mark is a fixed dark color that + disappears against dark-mode cards — recolor it to the theme's muted tone. */ +.react-grid-item > .react-resizable-handle::after { + border-right-color: var(--muted-foreground); + border-bottom-color: var(--muted-foreground); } \ No newline at end of file diff --git a/distribution_fe/lib/api/operations.ts b/distribution_fe/lib/api/operations.ts index 9d7e672..e4115ae 100644 --- a/distribution_fe/lib/api/operations.ts +++ b/distribution_fe/lib/api/operations.ts @@ -7,6 +7,7 @@ import type { RideStatus, CancelRequestType, LoadingSheetStatus, + DeliveryTaskStatus, } from '@/types/api' import type { PageParams } from './master' @@ -16,7 +17,7 @@ import type { PageParams } from './master' export interface RideBody { vehicleId: string driverProfileId: string - meterstart: number + meterstart?: number } export interface RideListParams extends PageParams { @@ -95,6 +96,10 @@ export interface DeliveryTaskItemQuantity { quantityFulfilled: number } +export interface DeliveryTaskListParams extends PageParams { + status?: DeliveryTaskStatus +} + export const deliveryTasks = { generate: (rideId: string) => api.post(`/rides/${rideId}/tasks/generate`).then(r => r.data), @@ -102,6 +107,9 @@ export const deliveryTasks = { listByRide: (rideId: string) => api.get(`/rides/${rideId}/tasks`).then(r => r.data), + list: (params?: DeliveryTaskListParams) => + api.get>('/delivery-tasks', { params }).then(r => r.data), + get: (id: string) => api.get(`/delivery-tasks/${id}`).then(r => r.data), diff --git a/distribution_fe/lib/api/sales.ts b/distribution_fe/lib/api/sales.ts index f9ddd00..c7fbcdb 100644 --- a/distribution_fe/lib/api/sales.ts +++ b/distribution_fe/lib/api/sales.ts @@ -4,10 +4,12 @@ import type { ShopPOResponse, InvoiceResponse, InvoiceStatus, + PaymentStatus, PaymentResponse, PaymentMethod, EntryMode, ShopPOStatus, + OutstandingPaymentsResponse, } from '@/types/api' import type { PageParams } from './master' @@ -69,12 +71,16 @@ export interface InvoiceListParams extends PageParams { routeId?: string shopId?: string status?: InvoiceStatus + paymentStatus?: PaymentStatus } export const invoices = { list: (params?: InvoiceListParams) => api.get>('/invoices', { params }).then(r => r.data), + outstandingSummary: () => + api.get('/invoices/outstanding-summary').then(r => r.data), + get: (id: string) => api.get(`/invoices/${id}`).then(r => r.data), diff --git a/distribution_fe/lib/api/stock.ts b/distribution_fe/lib/api/stock.ts index 51d615e..bdfbebe 100644 --- a/distribution_fe/lib/api/stock.ts +++ b/distribution_fe/lib/api/stock.ts @@ -3,6 +3,7 @@ import type { PaginatedResponse, StockBalanceResponse, StockMovementResponse, + LowStockItemResponse, LocationType, MovementType, } from '@/types/api' @@ -40,6 +41,9 @@ export const stock = { movements: (params?: StockMovementParams) => api.get>('/stock/movements', { params }).then(r => r.data), + lowStock: (locationId?: string) => + api.get('/stock/low-stock', { params: { locationId } }).then(r => r.data), + adjust: (body: AdjustmentBody) => api.post('/stock/adjustment', body).then(r => r.data), diff --git a/distribution_fe/types/api.ts b/distribution_fe/types/api.ts index a3ccbf7..31bbcc1 100644 --- a/distribution_fe/types/api.ts +++ b/distribution_fe/types/api.ts @@ -502,6 +502,15 @@ export interface StockBalanceResponse { locationName: string } +export interface LowStockItemResponse { + itemId: string + itemName: string + locationId: string + locationName: string + quantity: number + reorderLevel: number +} + export interface StockMovementResponse { id: string itemId: string @@ -667,6 +676,11 @@ export interface InvoiceResponse { createdAt: string } +export interface OutstandingPaymentsResponse { + invoiceCount: number + outstandingAmount: number +} + export interface PaymentResponse { id: string invoiceId: string