From be746664c5b053318872911ffd6dcdf85ad0dac5 Mon Sep 17 00:00:00 2001 From: NotII <46204250+NotII@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:52:38 +0100 Subject: [PATCH] Add profit analysis modal and cost tracking for products Introduces a Profit Analysis modal for products, allowing users to view profit, margin, and markup calculations based on cost per unit and pricing tiers. Adds cost per unit input to the product modal, updates product types, and integrates the analysis modal into the products page and product table. This enhances product management with profit tracking and analysis features. --- app/dashboard/products/page.tsx | 30 +++ components/modals/product-modal.tsx | 26 ++ components/modals/profit-analysis-modal.tsx | 265 ++++++++++++++++++++ components/tables/product-table.tsx | 15 +- lib/types/index.ts | 1 + 5 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 components/modals/profit-analysis-modal.tsx 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 {}