unzip frontend
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
|
||||
// Mock data types
|
||||
interface KPIs {
|
||||
totalRevenue: number
|
||||
revenueGrowth: number
|
||||
activeProducts: number
|
||||
lowStockItems: number
|
||||
activeClients: number
|
||||
newClientsThisMonth: number
|
||||
profitMargin: number
|
||||
marginImprovement: number
|
||||
}
|
||||
|
||||
interface Transaction {
|
||||
id: string
|
||||
client: string
|
||||
product: string
|
||||
amount: number
|
||||
status: "paid" | "pending" | "overdue"
|
||||
date: string
|
||||
}
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
name: string
|
||||
category: string
|
||||
unitsSold: number
|
||||
revenue: number
|
||||
}
|
||||
|
||||
interface ClientCredit {
|
||||
id: string
|
||||
name: string
|
||||
outstandingAmount: number
|
||||
daysOverdue: number
|
||||
lastPayment: string
|
||||
}
|
||||
|
||||
interface SupplierCredit {
|
||||
id: string
|
||||
name: string
|
||||
amountOwed: number
|
||||
status: "current" | "overdue"
|
||||
nextPayment: string
|
||||
}
|
||||
|
||||
// Mock data - in a real app, this would come from your database
|
||||
const mockKPIs: KPIs = {
|
||||
totalRevenue: 125000,
|
||||
revenueGrowth: 12.5,
|
||||
activeProducts: 156,
|
||||
lowStockItems: 8,
|
||||
activeClients: 42,
|
||||
newClientsThisMonth: 6,
|
||||
profitMargin: 28.5,
|
||||
marginImprovement: 3.2,
|
||||
}
|
||||
|
||||
const mockTransactions: Transaction[] = [
|
||||
{
|
||||
id: "1",
|
||||
client: "ABC Electronics",
|
||||
product: "Wireless Headphones",
|
||||
amount: 2500,
|
||||
status: "paid",
|
||||
date: "2024-01-15",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
client: "Tech Solutions Inc",
|
||||
product: "Laptop Chargers",
|
||||
amount: 1800,
|
||||
status: "pending",
|
||||
date: "2024-01-14",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
client: "Mobile World",
|
||||
product: "Phone Cases",
|
||||
amount: 950,
|
||||
status: "paid",
|
||||
date: "2024-01-13",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
client: "Digital Store",
|
||||
product: "USB Cables",
|
||||
amount: 650,
|
||||
status: "overdue",
|
||||
date: "2024-01-12",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
client: "Gadget Hub",
|
||||
product: "Power Banks",
|
||||
amount: 3200,
|
||||
status: "paid",
|
||||
date: "2024-01-11",
|
||||
},
|
||||
]
|
||||
|
||||
const mockTopProducts: Product[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Wireless Headphones",
|
||||
category: "Audio",
|
||||
unitsSold: 145,
|
||||
revenue: 14500,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Phone Cases",
|
||||
category: "Accessories",
|
||||
unitsSold: 230,
|
||||
revenue: 11500,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Power Banks",
|
||||
category: "Electronics",
|
||||
unitsSold: 89,
|
||||
revenue: 8900,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "USB Cables",
|
||||
category: "Accessories",
|
||||
unitsSold: 156,
|
||||
revenue: 7800,
|
||||
},
|
||||
]
|
||||
|
||||
const mockClientsWithCredit: ClientCredit[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Tech Solutions Inc",
|
||||
outstandingAmount: 4500,
|
||||
daysOverdue: 15,
|
||||
lastPayment: "2023-12-20",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Digital Store",
|
||||
outstandingAmount: 2800,
|
||||
daysOverdue: 45,
|
||||
lastPayment: "2023-11-30",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Mobile Mart",
|
||||
outstandingAmount: 1200,
|
||||
daysOverdue: 8,
|
||||
lastPayment: "2024-01-05",
|
||||
},
|
||||
]
|
||||
|
||||
const mockSuppliersWithCredit: SupplierCredit[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Electronics Wholesale Co",
|
||||
amountOwed: 8500,
|
||||
status: "current",
|
||||
nextPayment: "2024-01-25",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Global Tech Supplies",
|
||||
amountOwed: 3200,
|
||||
status: "overdue",
|
||||
nextPayment: "2024-01-10",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Premium Components Ltd",
|
||||
amountOwed: 5600,
|
||||
status: "current",
|
||||
nextPayment: "2024-01-30",
|
||||
},
|
||||
]
|
||||
|
||||
export function useBusinessData() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [kpis, setKpis] = useState<KPIs>(mockKPIs)
|
||||
const [recentTransactions, setRecentTransactions] = useState<Transaction[]>(mockTransactions)
|
||||
const [topProducts, setTopProducts] = useState<Product[]>(mockTopProducts)
|
||||
const [clientsWithCredit, setClientsWithCredit] = useState<ClientCredit[]>(mockClientsWithCredit)
|
||||
const [suppliersWithCredit, setSuppliersWithCredit] = useState<SupplierCredit[]>(mockSuppliersWithCredit)
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate loading data
|
||||
const timer = setTimeout(() => {
|
||||
setLoading(false)
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
kpis,
|
||||
recentTransactions,
|
||||
topProducts,
|
||||
clientsWithCredit,
|
||||
suppliersWithCredit,
|
||||
loading,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import type { Client } from "@/types/business"
|
||||
|
||||
// Mock clients data
|
||||
const mockClients: Client[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "ABC Electronics",
|
||||
email: "orders@abcelectronics.com",
|
||||
phone: "(555) 123-4567",
|
||||
address: "123 Tech Street, Silicon Valley, CA 94000",
|
||||
creditLimit: 10000,
|
||||
outstandingAmount: 4500,
|
||||
paymentTerms: "Net 30",
|
||||
contactPerson: "John Smith",
|
||||
businessType: "Electronics Retailer",
|
||||
notes: "Large volume customer, always pays on time",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Tech Solutions Inc",
|
||||
email: "billing@techsolutions.com",
|
||||
phone: "(555) 987-6543",
|
||||
address: "456 Business Ave, Downtown, NY 10001",
|
||||
creditLimit: 15000,
|
||||
outstandingAmount: 2800,
|
||||
paymentTerms: "Net 15",
|
||||
contactPerson: "Sarah Johnson",
|
||||
businessType: "IT Services",
|
||||
notes: "Prefers email communication",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Mobile World",
|
||||
email: "contact@mobileworld.com",
|
||||
phone: "(555) 456-7890",
|
||||
address: "789 Mobile Plaza, Austin, TX 78701",
|
||||
creditLimit: 8000,
|
||||
outstandingAmount: 0,
|
||||
paymentTerms: "Net 30",
|
||||
contactPerson: "Mike Chen",
|
||||
businessType: "Mobile Phone Store",
|
||||
notes: "New customer, good payment history so far",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Digital Store",
|
||||
email: "admin@digitalstore.com",
|
||||
phone: "(555) 321-0987",
|
||||
address: "321 Digital Way, Seattle, WA 98101",
|
||||
creditLimit: 5000,
|
||||
outstandingAmount: 5200,
|
||||
paymentTerms: "Net 45",
|
||||
contactPerson: "Lisa Wong",
|
||||
businessType: "Online Retailer",
|
||||
notes: "Currently over credit limit - monitor closely",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
]
|
||||
|
||||
export function useClients() {
|
||||
const [clients, setClients] = useState<Client[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate loading from localStorage or API
|
||||
const savedClients = localStorage.getItem("wholesale-clients")
|
||||
if (savedClients) {
|
||||
setClients(JSON.parse(savedClients))
|
||||
} else {
|
||||
setClients(mockClients)
|
||||
localStorage.setItem("wholesale-clients", JSON.stringify(mockClients))
|
||||
}
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
const addClient = (clientData: Omit<Client, "id" | "createdAt">) => {
|
||||
const newClient: Client = {
|
||||
...clientData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const updatedClients = [...clients, newClient]
|
||||
setClients(updatedClients)
|
||||
localStorage.setItem("wholesale-clients", JSON.stringify(updatedClients))
|
||||
}
|
||||
|
||||
const updateClient = (clientId: string, clientData: Omit<Client, "id" | "createdAt">) => {
|
||||
const updatedClients = clients.map((client) => (client.id === clientId ? { ...client, ...clientData } : client))
|
||||
setClients(updatedClients)
|
||||
localStorage.setItem("wholesale-clients", JSON.stringify(updatedClients))
|
||||
}
|
||||
|
||||
const deleteClient = (clientId: string) => {
|
||||
const updatedClients = clients.filter((client) => client.id !== clientId)
|
||||
setClients(updatedClients)
|
||||
localStorage.setItem("wholesale-clients", JSON.stringify(updatedClients))
|
||||
}
|
||||
|
||||
const addPayment = (clientId: string, amount: number, notes: string, date: string) => {
|
||||
const updatedClients = clients.map((client) =>
|
||||
client.id === clientId
|
||||
? { ...client, outstandingAmount: Math.max(0, client.outstandingAmount - amount) }
|
||||
: client,
|
||||
)
|
||||
setClients(updatedClients)
|
||||
localStorage.setItem("wholesale-clients", JSON.stringify(updatedClients))
|
||||
|
||||
// In a real app, you'd also save the payment record to a payments table
|
||||
console.log(`Payment recorded: $${amount} from client ${clientId} on ${date}. Notes: ${notes}`)
|
||||
}
|
||||
|
||||
const getClientById = (clientId: string) => {
|
||||
return clients.find((client) => client.id === clientId)
|
||||
}
|
||||
|
||||
const getClientsWithOutstandingCredit = () => {
|
||||
return clients.filter((client) => client.outstandingAmount > 0)
|
||||
}
|
||||
|
||||
const getClientsOverCreditLimit = () => {
|
||||
return clients.filter((client) => client.outstandingAmount > client.creditLimit)
|
||||
}
|
||||
|
||||
const getTotalOutstandingAmount = () => {
|
||||
return clients.reduce((total, client) => total + client.outstandingAmount, 0)
|
||||
}
|
||||
|
||||
return {
|
||||
clients,
|
||||
loading,
|
||||
addClient,
|
||||
updateClient,
|
||||
deleteClient,
|
||||
addPayment,
|
||||
getClientById,
|
||||
getClientsWithOutstandingCredit,
|
||||
getClientsOverCreditLimit,
|
||||
getTotalOutstandingAmount,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import type { Product } from "@/types/business"
|
||||
|
||||
// Mock products data
|
||||
const mockProducts: Product[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Wireless Headphones",
|
||||
category: "Audio",
|
||||
description: "High-quality wireless headphones with noise cancellation",
|
||||
buyPrice: 45.0,
|
||||
sellPrice: 89.99,
|
||||
stock: 25,
|
||||
minStock: 5,
|
||||
supplier: "Audio Tech Co",
|
||||
sku: "WH-001",
|
||||
createdAt: "2024-01-01",
|
||||
updatedAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Phone Cases",
|
||||
category: "Accessories",
|
||||
description: "Protective cases for various phone models",
|
||||
buyPrice: 3.5,
|
||||
sellPrice: 12.99,
|
||||
stock: 150,
|
||||
minStock: 20,
|
||||
supplier: "Mobile Accessories Ltd",
|
||||
sku: "PC-002",
|
||||
createdAt: "2024-01-01",
|
||||
updatedAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Power Banks",
|
||||
category: "Electronics",
|
||||
description: "10000mAh portable power banks",
|
||||
buyPrice: 18.0,
|
||||
sellPrice: 34.99,
|
||||
stock: 3,
|
||||
minStock: 10,
|
||||
supplier: "Power Solutions Inc",
|
||||
sku: "PB-003",
|
||||
createdAt: "2024-01-01",
|
||||
updatedAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "USB Cables",
|
||||
category: "Accessories",
|
||||
description: "USB-C charging cables 6ft length",
|
||||
buyPrice: 2.25,
|
||||
sellPrice: 8.99,
|
||||
stock: 0,
|
||||
minStock: 15,
|
||||
supplier: "Cable World",
|
||||
sku: "UC-004",
|
||||
createdAt: "2024-01-01",
|
||||
updatedAt: "2024-01-01",
|
||||
},
|
||||
]
|
||||
|
||||
export function useProducts() {
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate loading from localStorage or API
|
||||
const savedProducts = localStorage.getItem("wholesale-products")
|
||||
if (savedProducts) {
|
||||
setProducts(JSON.parse(savedProducts))
|
||||
} else {
|
||||
setProducts(mockProducts)
|
||||
localStorage.setItem("wholesale-products", JSON.stringify(mockProducts))
|
||||
}
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
const addProduct = (productData: Omit<Product, "id" | "createdAt" | "updatedAt">) => {
|
||||
const newProduct: Product = {
|
||||
...productData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const updatedProducts = [...products, newProduct]
|
||||
setProducts(updatedProducts)
|
||||
localStorage.setItem("wholesale-products", JSON.stringify(updatedProducts))
|
||||
}
|
||||
|
||||
const updateProduct = (productId: string, productData: Omit<Product, "id" | "createdAt" | "updatedAt">) => {
|
||||
const updatedProducts = products.map((product) =>
|
||||
product.id === productId ? { ...product, ...productData, updatedAt: new Date().toISOString() } : product,
|
||||
)
|
||||
setProducts(updatedProducts)
|
||||
localStorage.setItem("wholesale-products", JSON.stringify(updatedProducts))
|
||||
}
|
||||
|
||||
const deleteProduct = (productId: string) => {
|
||||
const updatedProducts = products.filter((product) => product.id !== productId)
|
||||
setProducts(updatedProducts)
|
||||
localStorage.setItem("wholesale-products", JSON.stringify(updatedProducts))
|
||||
}
|
||||
|
||||
const getProductById = (productId: string) => {
|
||||
return products.find((product) => product.id === productId)
|
||||
}
|
||||
|
||||
const getLowStockProducts = () => {
|
||||
return products.filter((product) => product.stock <= product.minStock)
|
||||
}
|
||||
|
||||
const getOutOfStockProducts = () => {
|
||||
return products.filter((product) => product.stock === 0)
|
||||
}
|
||||
|
||||
return {
|
||||
products,
|
||||
loading,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
getProductById,
|
||||
getLowStockProducts,
|
||||
getOutOfStockProducts,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import type { Supplier } from "@/types/business"
|
||||
|
||||
// Mock suppliers data
|
||||
const mockSuppliers: Supplier[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Electronics Wholesale Co",
|
||||
email: "orders@electronicswholesale.com",
|
||||
phone: "(555) 111-2222",
|
||||
address: "100 Industrial Blvd, Manufacturing District, CA 90210",
|
||||
paymentTerms: "Net 30",
|
||||
amountOwed: 8500,
|
||||
contactPerson: "David Wilson",
|
||||
businessType: "Electronics Manufacturer",
|
||||
notes: "Primary supplier for electronic components",
|
||||
taxId: "12-3456789",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Global Tech Supplies",
|
||||
email: "billing@globaltechsupplies.com",
|
||||
phone: "(555) 333-4444",
|
||||
address: "250 Tech Park Ave, Innovation City, TX 75001",
|
||||
paymentTerms: "Net 15",
|
||||
amountOwed: 3200,
|
||||
contactPerson: "Maria Rodriguez",
|
||||
businessType: "Technology Distributor",
|
||||
notes: "Fast shipping, good quality products",
|
||||
taxId: "98-7654321",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Premium Components Ltd",
|
||||
email: "accounts@premiumcomponents.com",
|
||||
phone: "(555) 555-6666",
|
||||
address: "500 Component Way, Quality Town, NY 10001",
|
||||
paymentTerms: "Net 45",
|
||||
amountOwed: 0,
|
||||
contactPerson: "James Thompson",
|
||||
businessType: "Component Manufacturer",
|
||||
notes: "High-end components, premium pricing",
|
||||
taxId: "55-1122334",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Budget Electronics Supply",
|
||||
email: "info@budgetelectronics.com",
|
||||
phone: "(555) 777-8888",
|
||||
address: "75 Discount Drive, Value City, FL 33101",
|
||||
paymentTerms: "COD",
|
||||
amountOwed: 1250,
|
||||
contactPerson: "Susan Lee",
|
||||
businessType: "Discount Supplier",
|
||||
notes: "Good for budget-friendly options",
|
||||
taxId: "44-9988776",
|
||||
createdAt: "2024-01-01",
|
||||
},
|
||||
]
|
||||
|
||||
export function useSuppliers() {
|
||||
const [suppliers, setSuppliers] = useState<Supplier[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate loading from localStorage or API
|
||||
const savedSuppliers = localStorage.getItem("wholesale-suppliers")
|
||||
if (savedSuppliers) {
|
||||
setSuppliers(JSON.parse(savedSuppliers))
|
||||
} else {
|
||||
setSuppliers(mockSuppliers)
|
||||
localStorage.setItem("wholesale-suppliers", JSON.stringify(mockSuppliers))
|
||||
}
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
const addSupplier = (supplierData: Omit<Supplier, "id" | "createdAt">) => {
|
||||
const newSupplier: Supplier = {
|
||||
...supplierData,
|
||||
id: Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
const updatedSuppliers = [...suppliers, newSupplier]
|
||||
setSuppliers(updatedSuppliers)
|
||||
localStorage.setItem("wholesale-suppliers", JSON.stringify(updatedSuppliers))
|
||||
}
|
||||
|
||||
const updateSupplier = (supplierId: string, supplierData: Omit<Supplier, "id" | "createdAt">) => {
|
||||
const updatedSuppliers = suppliers.map((supplier) =>
|
||||
supplier.id === supplierId ? { ...supplier, ...supplierData } : supplier,
|
||||
)
|
||||
setSuppliers(updatedSuppliers)
|
||||
localStorage.setItem("wholesale-suppliers", JSON.stringify(updatedSuppliers))
|
||||
}
|
||||
|
||||
const deleteSupplier = (supplierId: string) => {
|
||||
const updatedSuppliers = suppliers.filter((supplier) => supplier.id !== supplierId)
|
||||
setSuppliers(updatedSuppliers)
|
||||
localStorage.setItem("wholesale-suppliers", JSON.stringify(updatedSuppliers))
|
||||
}
|
||||
|
||||
const addPurchase = (
|
||||
supplierId: string,
|
||||
amount: number,
|
||||
description: string,
|
||||
date: string,
|
||||
invoiceNumber: string,
|
||||
) => {
|
||||
const updatedSuppliers = suppliers.map((supplier) =>
|
||||
supplier.id === supplierId ? { ...supplier, amountOwed: supplier.amountOwed + amount } : supplier,
|
||||
)
|
||||
setSuppliers(updatedSuppliers)
|
||||
localStorage.setItem("wholesale-suppliers", JSON.stringify(updatedSuppliers))
|
||||
|
||||
// In a real app, you'd also save the purchase record to a purchases table
|
||||
console.log(
|
||||
`Purchase recorded: $${amount} from supplier ${supplierId} on ${date}. Invoice: ${invoiceNumber}. Description: ${description}`,
|
||||
)
|
||||
}
|
||||
|
||||
const recordPayment = (supplierId: string, amount: number, notes: string, date: string, paymentMethod: string) => {
|
||||
const updatedSuppliers = suppliers.map((supplier) =>
|
||||
supplier.id === supplierId ? { ...supplier, amountOwed: Math.max(0, supplier.amountOwed - amount) } : supplier,
|
||||
)
|
||||
setSuppliers(updatedSuppliers)
|
||||
localStorage.setItem("wholesale-suppliers", JSON.stringify(updatedSuppliers))
|
||||
|
||||
// In a real app, you'd also save the payment record to a payments table
|
||||
console.log(
|
||||
`Payment recorded: $${amount} to supplier ${supplierId} on ${date}. Method: ${paymentMethod}. Notes: ${notes}`,
|
||||
)
|
||||
}
|
||||
|
||||
const getSupplierById = (supplierId: string) => {
|
||||
return suppliers.find((supplier) => supplier.id === supplierId)
|
||||
}
|
||||
|
||||
const getSuppliersWithOutstandingBalance = () => {
|
||||
return suppliers.filter((supplier) => supplier.amountOwed > 0)
|
||||
}
|
||||
|
||||
const getTotalAmountOwed = () => {
|
||||
return suppliers.reduce((total, supplier) => total + supplier.amountOwed, 0)
|
||||
}
|
||||
|
||||
return {
|
||||
suppliers,
|
||||
loading,
|
||||
addSupplier,
|
||||
updateSupplier,
|
||||
deleteSupplier,
|
||||
addPurchase,
|
||||
recordPayment,
|
||||
getSupplierById,
|
||||
getSuppliersWithOutstandingBalance,
|
||||
getTotalAmountOwed,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client"
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
Reference in New Issue
Block a user