diff --git a/app/dashboard/products/page.tsx b/app/dashboard/products/page.tsx index 0a75ae1..6b9c437 100644 --- a/app/dashboard/products/page.tsx +++ b/app/dashboard/products/page.tsx @@ -27,6 +27,10 @@ const ImportProductsModal = dynamic(() => import("@/components/modals/import-pro loading: () =>
Loading...
}); +const ProfitAnalysisModal = dynamic(() => import("@/components/modals/profit-analysis-modal").then(mod => ({ default: mod.ProfitAnalysisModal })), { + loading: () =>
Loading...
+}); + function ProductTableSkeleton() { return ( @@ -92,9 +96,12 @@ export default function ProductsPage() { category: "", pricing: [{ minQuantity: 1, pricePerUnit: 0 }], image: null, + costPerUnit: 0, }); const [importModalOpen, setImportModalOpen] = useState(false); const [addProductOpen, setAddProductOpen] = useState(false); + const [profitAnalysisOpen, setProfitAnalysisOpen] = useState(false); + const [selectedProductForAnalysis, setSelectedProductForAnalysis] = useState<{id: string, name: string} | null>(null); // Fetch products and categories useEffect(() => { @@ -269,6 +276,7 @@ export default function ProductsPage() { pricePerUnit: tier.pricePerUnit, })) : [{ minQuantity: 1, pricePerUnit: 0 }], + costPerUnit: product.costPerUnit || 0, }); setEditing(true); setAddProductOpen(true); @@ -286,6 +294,7 @@ export default function ProductsPage() { lowStockThreshold: 10, pricing: [{ minQuantity: 1, pricePerUnit: 0 }], image: null, + costPerUnit: 0, }); setEditing(false); setModalOpen(true); @@ -331,12 +340,23 @@ export default function ProductsPage() { category: "", pricing: [{ minQuantity: 1, pricePerUnit: 0 }], image: null, + costPerUnit: 0, }); if (setImagePreview) { setImagePreview(null); } }; + const handleProfitAnalysis = (productId: string, productName: string) => { + setSelectedProductForAnalysis({ id: productId, name: productName }); + setProfitAnalysisOpen(true); + }; + + const handleProfitAnalysisClose = () => { + setProfitAnalysisOpen(false); + setSelectedProductForAnalysis(null); + }; + // Handle toggle product enabled status const handleToggleEnabled = async (productId: string, enabled: boolean) => { try { @@ -413,6 +433,7 @@ export default function ProductsPage() { onEdit={handleEditProduct} onDelete={handleDeleteProduct} onToggleEnabled={handleToggleEnabled} + onProfitAnalysis={handleProfitAnalysis} getCategoryNameById={getCategoryNameById} /> @@ -437,6 +458,15 @@ export default function ProductsPage() { setProducts(products); }} /> + + {selectedProductForAnalysis && ( + + )} ); diff --git a/components/modals/product-modal.tsx b/components/modals/product-modal.tsx index c34c65f..19b2ed5 100644 --- a/components/modals/product-modal.tsx +++ b/components/modals/product-modal.tsx @@ -307,6 +307,32 @@ const ProductBasicInfo: React.FC<{ )} +
+

💰 Cost & Profit Tracking

+

+ Track your costs to automatically calculate profit margins and markup percentages. +

+ +
+ +

+ How much you paid for each unit of this product +

+ +
+
+
diff --git a/components/modals/profit-analysis-modal.tsx b/components/modals/profit-analysis-modal.tsx new file mode 100644 index 0000000..f8e2447 --- /dev/null +++ b/components/modals/profit-analysis-modal.tsx @@ -0,0 +1,265 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { TrendingUp, TrendingDown, Calculator, DollarSign } from "lucide-react"; +import { toast } from "sonner"; +import { apiRequest } from "@/lib/api"; + +interface ProfitAnalysisModalProps { + open: boolean; + onClose: () => void; + productId: string; + productName: string; +} + +interface ProfitData { + productId: string; + name: string; + costPerUnit: number; + pricing: Array<{ + minQuantity: number; + pricePerUnit: number; + }>; + profitMargins: Array<{ + minQuantity: number; + pricePerUnit: number; + profit: number | null; + profitMargin: number | null; + markup: number | null; + }>; + summary: { + hasCostData: boolean; + averageProfit: number | null; + averageProfitMargin: number | null; + averageMarkup: number | null; + }; +} + +export const ProfitAnalysisModal: React.FC = ({ + open, + onClose, + productId, + productName, +}) => { + const [profitData, setProfitData] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (open && productId) { + fetchProfitAnalysis(); + } + }, [open, productId]); + + const fetchProfitAnalysis = async () => { + try { + setLoading(true); + const response = await apiRequest(`/products/${productId}/profit-analysis`); + setProfitData(response); + } catch (error) { + console.error("Error fetching profit analysis:", error); + toast.error("Failed to load profit analysis"); + } finally { + setLoading(false); + } + }; + + const formatCurrency = (amount: number | null) => { + if (amount === null) return "N/A"; + return `£${amount.toFixed(2)}`; + }; + + const formatPercentage = (percentage: number | null) => { + if (percentage === null) return "N/A"; + return `${percentage.toFixed(1)}%`; + }; + + const getProfitColor = (profit: number | null) => { + if (profit === null) return "text-muted-foreground"; + return profit >= 0 ? "text-green-600" : "text-red-600"; + }; + + const getProfitIcon = (profit: number | null) => { + if (profit === null) return Calculator; + return profit >= 0 ? TrendingUp : TrendingDown; + }; + + if (loading) { + return ( + + + + Profit Analysis - {productName} + +
+
+
+

Loading profit analysis...

+
+
+
+
+ ); + } + + if (!profitData) { + return ( + + + + Profit Analysis - {productName} + +
+

No profit data available

+
+
+
+ ); + } + + return ( + + + + + + Profit Analysis - {productName} + + + +
+ {/* Summary Cards */} + {profitData.summary.hasCostData ? ( +
+ + + Average Profit + + +
+ {formatCurrency(profitData.summary.averageProfit)} +
+

Per unit sold

+
+
+ + + + Average Profit Margin + + +
+ {formatPercentage(profitData.summary.averageProfitMargin)} +
+

Of selling price

+
+
+ + + + Average Markup + + +
+ {formatPercentage(profitData.summary.averageMarkup)} +
+

On cost price

+
+
+
+ ) : ( + + +
+ +

No Cost Data Available

+

+ Add a cost per unit to this product to see profit calculations. +

+ Cost Per Unit: {formatCurrency(profitData.costPerUnit)} +
+
+
+ )} + + {/* Cost Information */} + + + Cost Information + + +
+ Cost Per Unit: + {formatCurrency(profitData.costPerUnit)} +
+
+
+ + {/* Pricing Tier Analysis */} + + + Pricing Tier Analysis + + +
+ {profitData.profitMargins.map((tier, index) => { + const ProfitIcon = getProfitIcon(tier.profit); + + return ( +
+
+ +
+

+ {tier.minQuantity}+ units @ {formatCurrency(tier.pricePerUnit)} +

+

+ Minimum quantity: {tier.minQuantity} +

+
+
+ +
+
+ Profit: {formatCurrency(tier.profit)} +
+
+ Margin: {formatPercentage(tier.profitMargin)} | + Markup: {formatPercentage(tier.markup)} +
+
+
+ ); + })} +
+
+
+ + {/* Help Text */} + + +
+

Understanding the Metrics:

+
    +
  • Profit: Selling price minus cost price
  • +
  • Profit Margin: Profit as a percentage of selling price
  • +
  • Markup: Profit as a percentage of cost price
  • +
+
+
+
+
+ +
+ +
+
+
+ ); +}; diff --git a/components/tables/product-table.tsx b/components/tables/product-table.tsx index 0d398e5..1f552c1 100644 --- a/components/tables/product-table.tsx +++ b/components/tables/product-table.tsx @@ -1,5 +1,5 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle } from "lucide-react"; +import { Edit, Trash, AlertTriangle, CheckCircle, AlertCircle, Calculator } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Product } from "@/models/products"; import { Badge } from "@/components/ui/badge"; @@ -11,6 +11,7 @@ interface ProductTableProps { onEdit: (product: Product) => void; onDelete: (productId: string) => void; onToggleEnabled: (productId: string, enabled: boolean) => void; + onProfitAnalysis?: (productId: string, productName: string) => void; getCategoryNameById: (categoryId: string) => string; } @@ -20,6 +21,7 @@ const ProductTable = ({ onEdit, onDelete, onToggleEnabled, + onProfitAnalysis, getCategoryNameById }: ProductTableProps) => { @@ -93,6 +95,17 @@ const ProductTable = ({ /> + {onProfitAnalysis && ( + + )} diff --git a/lib/types/index.ts b/lib/types/index.ts index 8d8dad7..19c2634 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -47,6 +47,7 @@ export interface Product { enabled?: boolean pricing: PricingTier[] image?: string | File | null + costPerUnit?: number // Cost price for profit calculations } export interface ProductData extends Product {}