Update frontend to match part 3

This commit is contained in:
2025-01-28 13:40:28 -05:00
parent 57b0e9a120
commit 25a0bc8d4c
10 changed files with 377 additions and 232 deletions

View File

@@ -5,13 +5,14 @@ import config from "@/config"
import { formatCurrency } from "@/lib/utils"
import { ClipboardList, AlertCircle, Layers, DollarSign, ShoppingCart } from "lucide-react" // Importing icons
import { useState } from "react"
import { PurchaseOrderStatus, ReceivingStatus } from "@/types/status-codes"
interface PurchaseMetricsData {
activePurchaseOrders: number
overduePurchaseOrders: number
onOrderUnits: number
onOrderCost: number
onOrderRetail: number
activePurchaseOrders: number // Orders that are not canceled, done, or fully received
overduePurchaseOrders: number // Orders past their expected delivery date
onOrderUnits: number // Total units across all active orders
onOrderCost: number // Total cost across all active orders
onOrderRetail: number // Total retail value across all active orders
vendorOrders: {
vendor: string
orders: number

View File

@@ -15,10 +15,10 @@ interface Product {
pid: number;
sku: string;
title: string;
daily_sales_avg: number;
weekly_sales_avg: number;
growth_rate: number;
total_revenue: number;
daily_sales_avg: string;
weekly_sales_avg: string;
growth_rate: string;
total_revenue: string;
}
export function TrendingProducts() {
@@ -75,20 +75,20 @@ export function TrendingProducts() {
</span>
</div>
</TableCell>
<TableCell>{parseFloat(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell>{Number(product.daily_sales_avg).toFixed(1)}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-1">
{parseFloat(product.growth_rate) > 0 ? (
{Number(product.growth_rate) > 0 ? (
<TrendingUp className="h-4 w-4 text-success" />
) : (
<TrendingDown className="h-4 w-4 text-destructive" />
)}
<span
className={
parseFloat(product.growth_rate) > 0 ? "text-success" : "text-destructive"
Number(product.growth_rate) > 0 ? "text-success" : "text-destructive"
}
>
{formatPercent(parseFloat(product.growth_rate))}
{formatPercent(Number(product.growth_rate))}
</span>
</div>
</TableCell>

View File

@@ -24,7 +24,7 @@ type FilterValue = string | number | boolean;
type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between";
interface FilterValueWithOperator {
value: FilterValue | [number, number];
value: FilterValue | [string, string];
operator: ComparisonOperator;
}
@@ -317,18 +317,32 @@ export function ProductFilters({
});
}, []);
const handleApplyFilter = (value: FilterValue | [number, number]) => {
const handleApplyFilter = (value: FilterValue | [string, string]) => {
if (!selectedFilter) return;
const newFilters = {
...activeFilters,
[selectedFilter.id]: {
value,
operator: selectedOperator,
},
};
let filterValue: ActiveFilterValue;
if (selectedFilter.type === "number") {
if (selectedOperator === "between" && Array.isArray(value)) {
filterValue = {
value: [value[0].toString(), value[1].toString()],
operator: selectedOperator,
};
} else {
filterValue = {
value: value.toString(),
operator: selectedOperator,
};
}
} else {
filterValue = value;
}
onFilterChange({
...activeFilters,
[selectedFilter.id]: filterValue,
});
onFilterChange(newFilters as Record<string, ActiveFilterValue>);
handlePopoverClose();
};
@@ -394,38 +408,14 @@ export function ProductFilters({
const getFilterDisplayValue = (filter: ActiveFilter) => {
const filterValue = activeFilters[filter.id];
const filterOption = filterOptions.find((opt) => opt.id === filter.id);
// For between ranges
if (Array.isArray(filterValue)) {
return `${filter.label} between ${filterValue[0]} and ${filterValue[1]}`;
if (typeof filter.value === "object" && "operator" in filter.value) {
const { operator, value } = filter.value;
if (Array.isArray(value)) {
return `${operator} ${value[0]} and ${value[1]}`;
}
return `${operator} ${value}`;
}
// For direct selections (select type) or text search
if (
filterOption?.type === "select" ||
filterOption?.type === "text" ||
typeof filterValue !== "object"
) {
const value =
typeof filterValue === "object" ? filterValue.value : filterValue;
return `${filter.label}: ${value}`;
}
// For numeric filters with operators
const operator = filterValue.operator;
const value = filterValue.value;
const operatorDisplay = {
"=": "=",
">": ">",
">=": "≥",
"<": "<",
"<=": "≤",
between: "between",
}[operator];
return `${filter.label} ${operatorDisplay} ${value}`;
return filter.value.toString();
};
return (

View File

@@ -261,6 +261,11 @@ export function ProductTable({
return columnDef.format(num);
}
}
// If the value is already a number, format it directly
if (typeof value === 'number') {
return columnDef.format(value);
}
// For other formats (e.g., date formatting), pass the value as is
return columnDef.format(value);
}
return value ?? '-';

View File

@@ -20,12 +20,21 @@ import {
PaginationPrevious,
} from '../components/ui/pagination';
import { motion } from 'motion/react';
import {
PurchaseOrderStatus,
ReceivingStatus as ReceivingStatusCode,
getPurchaseOrderStatusLabel,
getReceivingStatusLabel,
getPurchaseOrderStatusVariant,
getReceivingStatusVariant
} from '../types/status-codes';
interface PurchaseOrder {
id: number;
vendor_name: string;
order_date: string;
status: string;
status: number;
receiving_status: number;
total_items: number;
total_quantity: number;
total_cost: number;
@@ -113,6 +122,16 @@ export default function PurchaseOrders() {
limit: 100,
});
const STATUS_FILTER_OPTIONS = [
{ value: 'all', label: 'All Statuses' },
{ value: String(PurchaseOrderStatus.Created), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Created) },
{ value: String(PurchaseOrderStatus.ElectronicallyReadySend), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.ElectronicallyReadySend) },
{ value: String(PurchaseOrderStatus.Ordered), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Ordered) },
{ value: String(PurchaseOrderStatus.ReceivingStarted), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.ReceivingStarted) },
{ value: String(PurchaseOrderStatus.Done), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Done) },
{ value: String(PurchaseOrderStatus.Canceled), label: getPurchaseOrderStatusLabel(PurchaseOrderStatus.Canceled) },
];
const fetchData = async () => {
try {
const searchParams = new URLSearchParams({
@@ -171,16 +190,25 @@ export default function PurchaseOrders() {
}
};
const getStatusBadge = (status: string) => {
const variants: Record<string, { variant: "default" | "secondary" | "destructive" | "outline"; label: string }> = {
pending: { variant: "outline", label: "Pending" },
received: { variant: "default", label: "Received" },
partial: { variant: "secondary", label: "Partial" },
cancelled: { variant: "destructive", label: "Cancelled" },
};
const statusConfig = variants[status.toLowerCase()] || variants.pending;
return <Badge variant={statusConfig.variant}>{statusConfig.label}</Badge>;
const getStatusBadge = (status: number, receivingStatus: number) => {
// If the PO is canceled, show that status
if (status === PurchaseOrderStatus.Canceled) {
return <Badge variant={getPurchaseOrderStatusVariant(status)}>
{getPurchaseOrderStatusLabel(status)}
</Badge>;
}
// If receiving has started, show receiving status
if (status >= PurchaseOrderStatus.ReceivingStarted) {
return <Badge variant={getReceivingStatusVariant(receivingStatus)}>
{getReceivingStatusLabel(receivingStatus)}
</Badge>;
}
// Otherwise show PO status
return <Badge variant={getPurchaseOrderStatusVariant(status)}>
{getPurchaseOrderStatusLabel(status)}
</Badge>;
};
const formatNumber = (value: number) => {
@@ -252,45 +280,44 @@ export default function PurchaseOrders() {
</div>
{/* Filters */}
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-center">
<div className="flex items-center gap-2 flex-1">
<Input
placeholder="Search orders..."
value={filters.search}
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
className="h-8 w-[300px]"
/>
</div>
<div className="flex flex-wrap items-center gap-2">
<Select
value={filters.status}
onValueChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
>
<SelectTrigger className="h-8 w-[180px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
{filterOptions.statuses.map(status => (
<SelectItem key={status} value={status}>{status}</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={filters.vendor}
onValueChange={(value) => setFilters(prev => ({ ...prev, vendor: value }))}
>
<SelectTrigger className="h-8 w-[180px]">
<SelectValue placeholder="Vendor" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Vendors</SelectItem>
{filterOptions.vendors.map(vendor => (
<SelectItem key={vendor} value={vendor}>{vendor}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="mb-4 flex items-center gap-4">
<Input
placeholder="Search orders..."
value={filters.search}
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
className="max-w-xs"
/>
<Select
value={filters.status}
onValueChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
{STATUS_FILTER_OPTIONS.map(option => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={filters.vendor}
onValueChange={(value) => setFilters(prev => ({ ...prev, vendor: value }))}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select vendor" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Vendors</SelectItem>
{filterOptions.vendors.map(vendor => (
<SelectItem key={vendor} value={vendor}>
{vendor}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Purchase Orders Table */}
@@ -343,7 +370,7 @@ export default function PurchaseOrders() {
<TableCell>{po.id}</TableCell>
<TableCell>{po.vendor_name}</TableCell>
<TableCell>{new Date(po.order_date).toLocaleDateString()}</TableCell>
<TableCell>{getStatusBadge(po.status)}</TableCell>
<TableCell>{getStatusBadge(po.status, po.receiving_status)}</TableCell>
<TableCell>{po.total_items.toLocaleString()}</TableCell>
<TableCell>{po.total_quantity.toLocaleString()}</TableCell>
<TableCell>${formatNumber(po.total_cost)}</TableCell>

View File

@@ -3,10 +3,10 @@ export interface Product {
title: string;
SKU: string;
stock_quantity: number;
price: number;
regular_price: number;
cost_price: number;
landing_cost_price: number | null;
price: string; // DECIMAL(15,3)
regular_price: string; // DECIMAL(15,3)
cost_price: string; // DECIMAL(15,3)
landing_cost_price: string | null; // DECIMAL(15,3)
barcode: string;
vendor: string;
vendor_reference: string;
@@ -24,32 +24,32 @@ export interface Product {
updated_at: string;
// Metrics
daily_sales_avg?: number;
weekly_sales_avg?: number;
monthly_sales_avg?: number;
avg_quantity_per_order?: number;
daily_sales_avg?: string; // DECIMAL(15,3)
weekly_sales_avg?: string; // DECIMAL(15,3)
monthly_sales_avg?: string; // DECIMAL(15,3)
avg_quantity_per_order?: string; // DECIMAL(15,3)
number_of_orders?: number;
first_sale_date?: string;
last_sale_date?: string;
last_purchase_date?: string;
days_of_inventory?: number;
weeks_of_inventory?: number;
reorder_point?: number;
safety_stock?: number;
avg_margin_percent?: number;
total_revenue?: number;
inventory_value?: number;
cost_of_goods_sold?: number;
gross_profit?: number;
gmroi?: number;
avg_lead_time_days?: number;
days_of_inventory?: string; // DECIMAL(15,3)
weeks_of_inventory?: string; // DECIMAL(15,3)
reorder_point?: string; // DECIMAL(15,3)
safety_stock?: string; // DECIMAL(15,3)
avg_margin_percent?: string; // DECIMAL(15,3)
total_revenue?: string; // DECIMAL(15,3)
inventory_value?: string; // DECIMAL(15,3)
cost_of_goods_sold?: string; // DECIMAL(15,3)
gross_profit?: string; // DECIMAL(15,3)
gmroi?: string; // DECIMAL(15,3)
avg_lead_time_days?: string; // DECIMAL(15,3)
last_received_date?: string;
abc_class?: string;
stock_status?: string;
turnover_rate?: number;
current_lead_time?: number;
target_lead_time?: number;
turnover_rate?: string; // DECIMAL(15,3)
current_lead_time?: string; // DECIMAL(15,3)
target_lead_time?: string; // DECIMAL(15,3)
lead_time_status?: string;
reorder_qty?: number;
overstocked_amt?: number;
overstocked_amt?: string; // DECIMAL(15,3)
}

View File

@@ -0,0 +1,81 @@
// Purchase Order Status Codes
export enum PurchaseOrderStatus {
Canceled = 0,
Created = 1,
ElectronicallyReadySend = 10,
Ordered = 11,
Preordered = 12,
ElectronicallySent = 13,
ReceivingStarted = 15,
Done = 50
}
// Receiving Status Codes
export enum ReceivingStatus {
Canceled = 0,
Created = 1,
PartialReceived = 30,
FullReceived = 40,
Paid = 50
}
// Status Code Display Names
export const PurchaseOrderStatusLabels: Record<PurchaseOrderStatus, string> = {
[PurchaseOrderStatus.Canceled]: 'Canceled',
[PurchaseOrderStatus.Created]: 'Created',
[PurchaseOrderStatus.ElectronicallyReadySend]: 'Ready to Send',
[PurchaseOrderStatus.Ordered]: 'Ordered',
[PurchaseOrderStatus.Preordered]: 'Preordered',
[PurchaseOrderStatus.ElectronicallySent]: 'Sent',
[PurchaseOrderStatus.ReceivingStarted]: 'Receiving Started',
[PurchaseOrderStatus.Done]: 'Done'
};
export const ReceivingStatusLabels: Record<ReceivingStatus, string> = {
[ReceivingStatus.Canceled]: 'Canceled',
[ReceivingStatus.Created]: 'Created',
[ReceivingStatus.PartialReceived]: 'Partially Received',
[ReceivingStatus.FullReceived]: 'Fully Received',
[ReceivingStatus.Paid]: 'Paid'
};
// Helper functions
export function getPurchaseOrderStatusLabel(status: number): string {
return PurchaseOrderStatusLabels[status as PurchaseOrderStatus] || 'Unknown';
}
export function getReceivingStatusLabel(status: number): string {
return ReceivingStatusLabels[status as ReceivingStatus] || 'Unknown';
}
// Status checks
export function isReceivingComplete(status: number): boolean {
return status >= ReceivingStatus.PartialReceived;
}
export function isPurchaseOrderComplete(status: number): boolean {
return status === PurchaseOrderStatus.Done;
}
export function isPurchaseOrderCanceled(status: number): boolean {
return status === PurchaseOrderStatus.Canceled;
}
export function isReceivingCanceled(status: number): boolean {
return status === ReceivingStatus.Canceled;
}
// Badge variants for different statuses
export function getPurchaseOrderStatusVariant(status: number): 'default' | 'secondary' | 'destructive' | 'outline' {
if (isPurchaseOrderCanceled(status)) return 'destructive';
if (isPurchaseOrderComplete(status)) return 'default';
if (status >= PurchaseOrderStatus.ElectronicallyReadySend) return 'secondary';
return 'outline';
}
export function getReceivingStatusVariant(status: number): 'default' | 'secondary' | 'destructive' | 'outline' {
if (isReceivingCanceled(status)) return 'destructive';
if (status === ReceivingStatus.Paid) return 'default';
if (status >= ReceivingStatus.PartialReceived) return 'secondary';
return 'outline';
}