diff --git a/App.tsx b/App.tsx index c84282f..dda1404 100644 --- a/App.tsx +++ b/App.tsx @@ -22,6 +22,7 @@ const AdsPerformance = lazy(() => import('./components/AdsPerformance')); const ForecastView = lazy(() => import('./components/ForecastView')); const VendorDataView = lazy(() => import('./components/VendorDataView')); const ExperimentsView = lazy(() => import('./components/ExperimentsView')); +const MktDataView = lazy(() => import('./components/mkt/MktDataView')); // Loading fallback component @@ -49,7 +50,7 @@ const App: React.FC = () => { const [trafficData, setTrafficData] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); - const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'ads' | 'forecast' | 'vendor' | 'experiments'>('dashboard'); + const [view, setView] = useState<'dashboard' | 'table' | 'weekly' | 'ads' | 'forecast' | 'vendor' | 'experiments' | 'mkt'>('dashboard'); const [forecastData, setForecastData] = useState([]); const [isChatOpen, setIsChatOpen] = useState(false); const [activeUrl, setActiveUrl] = useState(() => localStorage.getItem('craze_csv_url') || PERMANENT_DROPBOX_URL); @@ -768,6 +769,13 @@ const App: React.FC = () => { > Weekly Sales + + + +
+
+
+

Gross Sales

+

{formatCurrency(product.grossSales)}

+
+
+

Net Margin

+

= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>{formatCurrency(metrics.netMargin)}

+
+
+

Margin %

+

= 0.2 ? 'text-emerald-400' : metrics.marginPercent >= 0.1 ? 'text-amber-400' : 'text-rose-400'}`}> + {formatPercent(metrics.marginPercent)} +

+
+
+

ACOS

+

{formatPercent(metrics.acos)}

+
+
+ +
+

Profitability Analysis (Waterfall)

+

Breakdown of deductions from gross sales to net margin.

+
+ +
+ + + + + `$${val / 1000}k`} + tick={{ fontSize: 12, fill: '#8B949E' }} + axisLine={false} + tickLine={false} + /> + { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.name}

+

+ {data.val > 0 && !data.isTotal ? '+' : ''}{formatCurrency(data.val)} +

+
+ ); + } + return null; + }} + /> + + + + {data.map((entry, index) => ( + + ))} + +
+
+
+
+ + + ); +}; diff --git a/components/mkt/data.ts b/components/mkt/data.ts new file mode 100644 index 0000000..77fca6b --- /dev/null +++ b/components/mkt/data.ts @@ -0,0 +1,140 @@ +import { Product } from './types'; + +export const mockProducts: Product[] = [ + { + id: '1', + asin: 'B08F7N8P1Q', + sku: 'SKU-WIDGET-01', + name: 'Premium Widget Pro Max', + image: 'https://picsum.photos/seed/widget1/100/100', + category: 'Electronics', + brand: 'TechCorp', + grossSales: 45000, + unitsSold: 1500, + ppcSpend: 4500, + deals: 1200, + promos: 800, + chargebacks: 300, + chargebacksPrevMonth: 250, + cogs: 15000, + }, + { + id: '2', + asin: 'B09G8M7P2R', + sku: 'SKU-GADGET-02', + name: 'Smart Gadget Mini', + image: 'https://picsum.photos/seed/gadget2/100/100', + category: 'Electronics', + brand: 'TechCorp', + grossSales: 12000, + unitsSold: 800, + ppcSpend: 3000, + deals: 500, + promos: 200, + chargebacks: 800, + chargebacksPrevMonth: 400, // Anomaly! > 15% increase + cogs: 6000, + }, + { + id: '3', + asin: 'B07H6L5P3S', + sku: 'SKU-HOME-03', + name: 'Ergonomic Office Chair', + image: 'https://picsum.photos/seed/chair3/100/100', + category: 'Home & Office', + brand: 'HomePlus', + grossSales: 85000, + unitsSold: 425, + ppcSpend: 8500, + deals: 2000, + promos: 1500, + chargebacks: 1200, + chargebacksPrevMonth: 1100, + cogs: 35000, + }, + { + id: '4', + asin: 'B06J5K4P4T', + sku: 'SKU-KITCHEN-04', + name: 'Stainless Steel Knife Set', + image: 'https://picsum.photos/seed/knife4/100/100', + category: 'Kitchen', + brand: 'ChefMaster', + grossSales: 28000, + unitsSold: 700, + ppcSpend: 4200, + deals: 800, + promos: 600, + chargebacks: 150, + chargebacksPrevMonth: 160, + cogs: 9000, + }, + { + id: '5', + asin: 'B05K4J3P5U', + sku: 'SKU-FITNESS-05', + name: 'Yoga Mat Extra Thick', + image: 'https://picsum.photos/seed/yoga5/100/100', + category: 'Fitness', + brand: 'FitLife', + grossSales: 15000, + unitsSold: 600, + ppcSpend: 3500, + deals: 1000, + promos: 500, + chargebacks: 400, + chargebacksPrevMonth: 380, + cogs: 5000, + }, + { + id: '6', + asin: 'B04L3H2P6V', + sku: 'SKU-BEAUTY-06', + name: 'Organic Face Serum', + image: 'https://picsum.photos/seed/serum6/100/100', + category: 'Beauty', + brand: 'NatureGlow', + grossSales: 32000, + unitsSold: 1280, + ppcSpend: 2800, + deals: 500, + promos: 300, + chargebacks: 100, + chargebacksPrevMonth: 90, + cogs: 8000, + }, + { + id: '7', + asin: 'B03M2G1P7W', + sku: 'SKU-TOY-07', + name: 'Educational Building Blocks', + image: 'https://picsum.photos/seed/toy7/100/100', + category: 'Toys', + brand: 'KidGenius', + grossSales: 9500, + unitsSold: 380, + ppcSpend: 2500, + deals: 400, + promos: 200, + chargebacks: 50, + chargebacksPrevMonth: 45, + cogs: 4000, + }, + { + id: '8', + asin: 'B02N1F0P8X', + sku: 'SKU-PET-08', + name: 'Automatic Pet Feeder', + image: 'https://picsum.photos/seed/pet8/100/100', + category: 'Pet Supplies', + brand: 'PetCare', + grossSales: 42000, + unitsSold: 840, + ppcSpend: 6000, + deals: 1500, + promos: 1000, + chargebacks: 600, + chargebacksPrevMonth: 550, + cogs: 18000, + } +]; diff --git a/components/mkt/types.ts b/components/mkt/types.ts new file mode 100644 index 0000000..a9fadda --- /dev/null +++ b/components/mkt/types.ts @@ -0,0 +1,17 @@ +export interface Product { + id: string; + asin: string; + sku: string; + name: string; + image: string; + category: string; + brand: string; + grossSales: number; + unitsSold: number; + ppcSpend: number; + deals: number; + promos: number; + chargebacks: number; + chargebacksPrevMonth: number; + cogs: number; +} diff --git a/components/mkt/utils.ts b/components/mkt/utils.ts new file mode 100644 index 0000000..ecd04f0 --- /dev/null +++ b/components/mkt/utils.ts @@ -0,0 +1,48 @@ +import { Product } from './types'; +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(value); +}; + +export const formatPercent = (value: number) => { + return new Intl.NumberFormat('en-US', { + style: 'percent', + minimumFractionDigits: 1, + maximumFractionDigits: 1, + }).format(value); +}; + +export const calculateProductMetrics = (product: Product, includeCOGS: boolean) => { + const incentives = product.deals + product.promos; + const cogsDeduction = includeCOGS ? product.cogs : 0; + const totalDeductions = product.ppcSpend + incentives + product.chargebacks + cogsDeduction; + const netMargin = product.grossSales - totalDeductions; + const marginPercent = product.grossSales > 0 ? netMargin / product.grossSales : 0; + const acos = product.grossSales > 0 ? product.ppcSpend / product.grossSales : 0; + + const chargebackIncrease = product.chargebacksPrevMonth > 0 + ? (product.chargebacks - product.chargebacksPrevMonth) / product.chargebacksPrevMonth + : 0; + const hasChargebackAnomaly = chargebackIncrease > 0.15; + + return { + incentives, + totalDeductions, + netMargin, + marginPercent, + acos, + chargebackIncrease, + hasChargebackAnomaly + }; +}; diff --git a/package-lock.json b/package-lock.json index dbb94c5..390ee97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,15 @@ "dependencies": { "@google/genai": "^1.30.0", "@supabase/supabase-js": "^2.97.0", + "clsx": "^2.1.1", "dotenv": "^17.3.1", + "lucide-react": "^0.577.0", "node-fetch": "^3.3.2", "papaparse": "^5.5.3", "react": "^19.2.0", "react-dom": "^19.2.0", "recharts": "^3.5.0", + "tailwind-merge": "^3.5.0", "xlsx": "^0.18.5" }, "devDependencies": { @@ -3681,6 +3684,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4493,6 +4505,16 @@ "node": ">=8" } }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tar": { "version": "7.5.2", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", diff --git a/package.json b/package.json index 1f2b348..29b22fd 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,15 @@ "dependencies": { "@google/genai": "^1.30.0", "@supabase/supabase-js": "^2.97.0", + "clsx": "^2.1.1", "dotenv": "^17.3.1", + "lucide-react": "^0.577.0", "node-fetch": "^3.3.2", "papaparse": "^5.5.3", "react": "^19.2.0", "react-dom": "^19.2.0", "recharts": "^3.5.0", + "tailwind-merge": "^3.5.0", "xlsx": "^0.18.5" }, "devDependencies": {