diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..02687bd --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,123 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..a262171 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from 'next' +import { GeistSans } from 'geist/font/sans' +import { GeistMono } from 'geist/font/mono' +import './globals.css' + +export const metadata: Metadata = { + title: 'v0 App', + description: 'Created with v0', + generator: 'v0.app', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + + + {children} + + ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..64085a2 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,561 @@ +"use client" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { + DollarSign, + Package, + Users, + AlertTriangle, + ShoppingCart, + CreditCard, + BarChart3, + Plus, + Building2, +} from "lucide-react" +import { useBusinessData } from "@/hooks/use-business-data" +import { useProducts } from "@/hooks/use-products" +import { useClients } from "@/hooks/use-clients" +import { useSuppliers } from "@/hooks/use-suppliers" +import { ProductForm } from "@/components/products/product-form" +import { ProductsTable } from "@/components/products/products-table" +import { ClientForm } from "@/components/clients/client-form" +import { ClientsTable } from "@/components/clients/clients-table" +import { PaymentForm } from "@/components/clients/payment-form" +import { SupplierForm } from "@/components/suppliers/supplier-form" +import { SuppliersTable } from "@/components/suppliers/suppliers-table" +import { PurchaseForm } from "@/components/suppliers/purchase-form" +import { SupplierPaymentForm } from "@/components/suppliers/supplier-payment-form" +import { CreditOverview } from "@/components/credit/credit-overview" +import { TransactionHistory } from "@/components/credit/transaction-history" +import { useState } from "react" +import type { Product, Client, Supplier } from "@/types/business" + +export default function Dashboard() { + const { kpis, recentTransactions, topProducts, clientsWithCredit, suppliersWithCredit, loading } = useBusinessData() + const { + products, + loading: productsLoading, + addProduct, + updateProduct, + deleteProduct, + getLowStockProducts, + getOutOfStockProducts, + } = useProducts() + + const { + clients, + loading: clientsLoading, + addClient, + updateClient, + deleteClient, + addPayment, + getClientById, + getClientsWithOutstandingCredit, + getClientsOverCreditLimit, + getTotalOutstandingAmount, + } = useClients() + + const { + suppliers, + loading: suppliersLoading, + addSupplier, + updateSupplier, + deleteSupplier, + addPurchase, + recordPayment, + getSupplierById, + getSuppliersWithOutstandingBalance, + getTotalAmountOwed, + } = useSuppliers() + + const [showProductForm, setShowProductForm] = useState(false) + const [editingProduct, setEditingProduct] = useState() + const [showClientForm, setShowClientForm] = useState(false) + const [editingClient, setEditingClient] = useState() + const [showPaymentForm, setShowPaymentForm] = useState(false) + const [paymentClientId, setPaymentClientId] = useState("") + const [showSupplierForm, setShowSupplierForm] = useState(false) + const [editingSupplier, setEditingSupplier] = useState() + const [showPurchaseForm, setShowPurchaseForm] = useState(false) + const [purchaseSupplierId, setPurchaseSupplierId] = useState("") + const [showSupplierPaymentForm, setShowSupplierPaymentForm] = useState(false) + const [supplierPaymentId, setSupplierPaymentId] = useState("") + + const handleEditProduct = (product: Product) => { + setEditingProduct(product) + setShowProductForm(true) + } + + const handleProductSubmit = (productData: Omit) => { + if (editingProduct) { + updateProduct(editingProduct.id, productData) + setEditingProduct(undefined) + } else { + addProduct(productData) + } + } + + const handleCloseProductForm = () => { + setShowProductForm(false) + setEditingProduct(undefined) + } + + const handleEditClient = (client: Client) => { + setEditingClient(client) + setShowClientForm(true) + } + + const handleClientSubmit = (clientData: Omit) => { + if (editingClient) { + updateClient(editingClient.id, clientData) + setEditingClient(undefined) + } else { + addClient(clientData) + } + } + + const handleCloseClientForm = () => { + setShowClientForm(false) + setEditingClient(undefined) + } + + const handleAddPayment = (clientId: string) => { + setPaymentClientId(clientId) + setShowPaymentForm(true) + } + + const handlePaymentSubmit = (payment: { amount: number; notes: string; date: string }) => { + addPayment(paymentClientId, payment.amount, payment.notes, payment.date) + setShowPaymentForm(false) + setPaymentClientId("") + } + + const handleEditSupplier = (supplier: Supplier) => { + setEditingSupplier(supplier) + setShowSupplierForm(true) + } + + const handleSupplierSubmit = (supplierData: Omit) => { + if (editingSupplier) { + updateSupplier(editingSupplier.id, supplierData) + setEditingSupplier(undefined) + } else { + addSupplier(supplierData) + } + } + + const handleCloseSupplierForm = () => { + setShowSupplierForm(false) + setEditingSupplier(undefined) + } + + const handleAddPurchase = (supplierId: string) => { + setPurchaseSupplierId(supplierId) + setShowPurchaseForm(true) + } + + const handlePurchaseSubmit = (purchase: { + amount: number + description: string + date: string + invoiceNumber: string + }) => { + addPurchase(purchaseSupplierId, purchase.amount, purchase.description, purchase.date, purchase.invoiceNumber) + setShowPurchaseForm(false) + setPurchaseSupplierId("") + } + + const handleRecordPayment = (supplierId: string) => { + setSupplierPaymentId(supplierId) + setShowSupplierPaymentForm(true) + } + + const handleSupplierPaymentSubmit = (payment: { + amount: number + notes: string + date: string + paymentMethod: string + }) => { + recordPayment(supplierPaymentId, payment.amount, payment.notes, payment.date, payment.paymentMethod) + setShowSupplierPaymentForm(false) + setSupplierPaymentId("") + } + + if (loading || productsLoading || clientsLoading || suppliersLoading) { + return ( +
+
+
+

Loading dashboard...

+
+
+ ) + } + + const lowStockProducts = getLowStockProducts() + const outOfStockProducts = getOutOfStockProducts() + const clientsWithOutstanding = getClientsWithOutstandingCredit() + const clientsOverLimit = getClientsOverCreditLimit() + const totalOutstanding = getTotalOutstandingAmount() + const suppliersWithBalance = getSuppliersWithOutstandingBalance() + const totalOwed = getTotalAmountOwed() + const paymentClient = getClientById(paymentClientId) + const purchaseSupplier = getSupplierById(purchaseSupplierId) + const paymentSupplier = getSupplierById(supplierPaymentId) + + return ( +
+ {/* Header */} +
+
+
+
+

Wholesale Manager

+

Business Dashboard

+
+ +
+
+
+ +
+ {/* KPI Cards */} +
+ + + Total Revenue + + + +
${kpis.totalRevenue.toLocaleString()}
+

+ +{kpis.revenueGrowth}% from last month +

+
+
+ + + + Active Products + + + +
{products.length}
+

+ {lowStockProducts.length} low stock, {outOfStockProducts.length} out of stock +

+
+
+ + + + Active Clients + + + +
{clients.length}
+

${totalOutstanding.toLocaleString()} outstanding

+
+
+ + + + Suppliers + + + +
{suppliers.length}
+

${totalOwed.toLocaleString()} owed

+
+
+
+ + {/* Main Content Tabs */} + + + Overview + Products + Clients + Suppliers + Credit Tracking + + + +
+ {/* Recent Transactions */} + + + + + Recent Transactions + + Latest business activities + + +
+ {recentTransactions.slice(0, 5).map((transaction) => ( +
+
+

{transaction.client}

+

{transaction.product}

+
+
+

${transaction.amount}

+ + {transaction.status} + +
+
+ ))} +
+
+
+ + {/* Top Products */} + + + + + Top Selling Products + + Best performers this month + + +
+ {topProducts.map((product, index) => ( +
+
+
+ #{index + 1} +
+
+

{product.name}

+

{product.category}

+
+
+
+

{product.unitsSold} sold

+

${product.revenue}

+
+
+ ))} +
+
+
+
+
+ + + + +
+
+ + + Products Management + + Manage your product inventory and pricing +
+ +
+
+ + {/* Stock Alerts */} + {(lowStockProducts.length > 0 || outOfStockProducts.length > 0) && ( +
+
+ +

Stock Alerts

+
+ {outOfStockProducts.length > 0 && ( +

+ {outOfStockProducts.length} products are out of stock +

+ )} + {lowStockProducts.length > 0 && ( +

+ {lowStockProducts.length} products are running low +

+ )} +
+ )} + + +
+
+
+ + + + +
+
+ + + Clients Management + + Manage your clients and track credit status +
+ +
+
+ + {/* Credit Alerts */} + {(clientsWithOutstanding.length > 0 || clientsOverLimit.length > 0) && ( +
+
+ +

Credit Alerts

+
+ {clientsOverLimit.length > 0 && ( +

+ {clientsOverLimit.length} clients are over their credit limit +

+ )} +

+ Total outstanding: ${totalOutstanding.toLocaleString()} +

+
+ )} + + +
+
+
+ + + + +
+
+ + + Suppliers Management + + Manage your suppliers and track payment obligations +
+ +
+
+ + {/* Payment Alerts */} + {suppliersWithBalance.length > 0 && ( +
+
+ +

Payment Obligations

+
+

+ {suppliersWithBalance.length} suppliers have outstanding balances +

+

+ Total amount owed: ${totalOwed.toLocaleString()} +

+
+ )} + + +
+
+
+ + {/* Added new credit tracking tab */} + + + + +
+
+ + {/* Product Form Dialog */} + + + {/* Client Form Dialog */} + + + {/* Payment Form Dialog */} + {paymentClient && ( + { + setShowPaymentForm(open) + if (!open) setPaymentClientId("") + }} + onSubmit={handlePaymentSubmit} + /> + )} + + {/* Supplier Form Dialog */} + + + {/* Purchase Form Dialog */} + {purchaseSupplier && ( + { + setShowPurchaseForm(open) + if (!open) setPurchaseSupplierId("") + }} + onSubmit={handlePurchaseSubmit} + /> + )} + + {/* Supplier Payment Form Dialog */} + {paymentSupplier && ( + { + setShowSupplierPaymentForm(open) + if (!open) setSupplierPaymentId("") + }} + onSubmit={handleSupplierPaymentSubmit} + /> + )} +
+ ) +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..335484f --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/frontend/components/clients/client-form.tsx b/frontend/components/clients/client-form.tsx new file mode 100644 index 0000000..9dcfb5c --- /dev/null +++ b/frontend/components/clients/client-form.tsx @@ -0,0 +1,200 @@ +"use client" + +import type React from "react" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import type { Client } from "@/types/business" + +interface ClientFormProps { + client?: Client + open: boolean + onOpenChange: (open: boolean) => void + onSubmit: (client: Omit) => void +} + +const paymentTerms = ["Net 15", "Net 30", "Net 45", "Net 60", "COD", "Prepaid", "Custom"] + +export function ClientForm({ client, open, onOpenChange, onSubmit }: ClientFormProps) { + const [formData, setFormData] = useState({ + name: client?.name || "", + email: client?.email || "", + phone: client?.phone || "", + address: client?.address || "", + creditLimit: client?.creditLimit || 0, + paymentTerms: client?.paymentTerms || "Net 30", + notes: client?.notes || "", + contactPerson: client?.contactPerson || "", + businessType: client?.businessType || "", + }) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + onSubmit({ + ...formData, + outstandingAmount: client?.outstandingAmount || 0, + }) + onOpenChange(false) + // Reset form if it's a new client + if (!client) { + setFormData({ + name: "", + email: "", + phone: "", + address: "", + creditLimit: 0, + paymentTerms: "Net 30", + notes: "", + contactPerson: "", + businessType: "", + }) + } + } + + return ( + + + + {client ? "Edit Client" : "Add New Client"} + + {client ? "Update client information" : "Enter the details for the new client"} + + + +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Enter business name" + required + /> +
+ +
+ + setFormData({ ...formData, contactPerson: e.target.value })} + placeholder="Primary contact name" + /> +
+
+ +
+
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="client@example.com" + /> +
+ +
+ + setFormData({ ...formData, phone: e.target.value })} + placeholder="(555) 123-4567" + /> +
+
+ +
+ +