3595 {activeTab === 'dashboard' && canAccess('dashboard') && ( 3596
3597 3598 3599 {/* ── HERO WELCOME BANNER ── */} 3600
3601
3602
3603 3604

Live Dashboard

3605
3606

3607 স্বাগতম, {currentUser?.name?.split(' ')[0] || 'Admin'} 👋 3608

3609

আপনার স্টোরের সম্পূর্ণ আর্থিক চিত্র — রেভিনিউ, খরচ এবং মুনাফা একনজরে।

3610
3611 {[ 3612 { label: `${orders.filter(o => o.status === 'pending').length} Pending`, bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-200' }, 3613 { label: `${completedOrders} Completed`, bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200' }, 3614 { label: `${products.length} Products`, bg: 'bg-blue-50', text: 'text-blue-700', border: 'border-blue-200' }, 3615 ].map(p => ( 3616 {p.label} 3617 ))} 3618
3619
3620
3621 {[ 3622 { label: 'Pending', value: orders.filter(o => o.status === 'pending').length, icon: '⏳', bg: 'bg-amber-50', text: 'text-amber-700', border: 'border-amber-200' }, 3623 { label: 'Revenue', value: `৳${confirmedOrderValue.toLocaleString()}`, icon: '💰', bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200' }, 3624 { label: isProfit ? 'Net Profit' : 'Net Loss', value: `${isProfit ? '+' : '-'}৳${Math.abs(netProfit).toLocaleString()}`, icon: isProfit ? '📈' : '📉', bg: isProfit ? 'bg-emerald-50' : 'bg-red-50', text: isProfit ? 'text-emerald-700' : 'text-red-700', border: isProfit ? 'border-emerald-200' : 'border-red-200' }, 3625 ].map((s, i) => ( 3626
3627 {s.icon} 3628
3629

{s.label}

3630

{s.value}

3631
3632
3633 ))} 3634
3635
3636 3637 {/* ── REDESIGNED STAT CARDS ── */} 3638
3639 {[ 3640 { label: 'Total Orders', value: totalOrders, sub: `${pendingOrders} pending`, icon: ClipboardList, color: 'text-blue-600', bg: 'bg-blue-50', border: 'border-blue-100', shadow: 'shadow-blue-500/10' }, 3641 { label: 'Incomplete', value: totalLeads, sub: `${totalInitiatedCheckouts} initiated`, icon: Users, color: 'text-purple-600', bg: 'bg-purple-50', border: 'border-purple-100', shadow: 'shadow-purple-500/10' }, 3642 { label: 'Revenue', value: `৳${confirmedOrderValue.toLocaleString()}`, sub: `${grossMargin.toFixed(1)}% gross margin`, icon: CreditCard, color: 'text-emerald-600', bg: 'bg-emerald-50', border: 'border-emerald-100', shadow: 'shadow-emerald-500/10' }, 3643 { label: 'Net Margin', value: `${profitMargin.toFixed(1)}%`, sub: `৳${Math.abs(netProfit).toLocaleString()} ${isProfit ? 'profit' : 'loss'}`, icon: isProfit ? TrendingUpIcon : TrendingDownIcon, color: isProfit ? 'text-emerald-600' : 'text-red-600', bg: isProfit ? 'bg-emerald-50' : 'bg-red-50', border: isProfit ? 'border-emerald-100' : 'border-red-100', shadow: isProfit ? 'shadow-emerald-500/10' : 'shadow-red-500/10' }, 3644 ].map((s, i) => ( 3645
3646
3647 3648
3649

{s.value}

3650

{s.label}

3651
3652 {s.sub} 3653
3654
3655 ))} 3656
3657 3658 {/* ── P&L STATEMENT ── */} 3659
3660
3661
3662
3663 3664
3665
3666

Profit & Loss Statement

3667

Based on confirmed & completed orders

3668
3669
3670 3671 {isProfit ? : } 3672 {isProfit ? 'PROFITABLE' : 'LOSS'} 3673 3674
3675
3676 3677 {/* Column 1: Income */} 3678
3679

💰 Income

3680
3681
3682 Total Sales Revenue 3683 ৳{confirmedOrderValue.toLocaleString()} 3684
3685

Confirmed + Completed orders

3686
3687
3688
3689 Gross Profit 3690 ৳{grossProfit.toLocaleString()} 3691
3692

{grossMargin.toFixed(1)}% Gross Margin

3693
3694
3695 3696 {/* Column 2: Expenses */} 3697
3698

📤 Expenses

3699
3700
3701 Product Cost (COGS) 3702 -৳{totalCost.toLocaleString()} 3703
3704

From mainBalance per variation

3705
3706
3707
3708 Delivery Charges 3709 -৳{totalDeliveryCharges.toLocaleString()} 3710
3711

Paid on confirmed orders

3712
3713
3714
3715 Facebook Ads Spend 3716 -৳{Math.round(fbAdsData.spend).toLocaleString()} 3717
3718

{fbAdsData.connected ? `ROAS ${fbAdsData.roas.toFixed(1)}x` : 'connect করুন'}

3719
3720
3721 Total Expenses 3722 -৳{(totalCost + totalExpenses).toLocaleString()} 3723
3724
3725 3726 {/* Column 3: Result */} 3727
3728

📈 Result

3729
3730
3731 {isProfit ? : } 3732
3733

{isProfit ? '✅ NET PROFIT' : '❌ NET LOSS'}

3734

{isProfit ? '+' : '-'}৳{Math.abs(netProfit).toLocaleString()}

3735 {profitMargin.toFixed(1)}% Net Margin 3736
3737
3738 {[ 3739 { label: 'Revenue', val: confirmedOrderValue, cls: 'text-green-600' }, 3740 { label: 'COGS', val: -totalCost, cls: 'text-amber-600' }, 3741 { label: 'Delivery', val: -totalDeliveryCharges, cls: 'text-violet-600' }, 3742 { label: 'FB Ads', val: -Math.round(fbAdsData.spend), cls: 'text-blue-600' }, 3743 ].map(r => ( 3744
3745 {r.label} 3746 {r.val < 0 ? '-' : '+'} ৳{Math.abs(r.val).toLocaleString()} 3747
3748 ))} 3749
3750
3751
3752
3753 3754 {/* ── FACEBOOK ADS PERFORMANCE WIDGET ─────────── */} 3755 {fbAdsData.connected ? ( 3756
3757
3758
3759
3760 3761
3762
3763

Facebook Ads Performance

3764

3765 {fbAdsData.preset === 'today' ? 'আজকের' : fbAdsData.preset === 'last_7d' ? 'গত ৭ দিন' : fbAdsData.preset === 'this_month' ? 'এই মাস' : fbAdsData.preset === 'last_30d' ? 'গত ৩০ দিন' : 'এই বছর'} ডেটা 3766

3767
3768
3769 📡 Live 3770
3771
3772
3773 {[ 3774 { label: 'Ad Spend', value: `৳${fbAdsData.spend.toLocaleString()}`, sub: `$${(fbAdsData.spend_usd || 0).toFixed(2)}`, color: '#f87171', bg: 'bg-red-50', br: 'border-red-100', icon: '💸' }, 3775 { label: 'FB Sales', value: String(fbAdsData.purchases || 0), sub: `${fbSalesRatio.toFixed(1)}% of orders`, color: '#22c55e', bg: 'bg-green-50', br: 'border-green-100', icon: '🛍' }, 3776 { label: 'ROAS', value: fbAdsData.roas > 0 ? `${fbAdsData.roas.toFixed(1)}x` : '—', sub: fbAdsData.roas >= 8 ? '✅ Good' : fbAdsData.roas >= 5 ? '⚠️ Avg' : fbAdsData.roas > 0 ? '❌ Low' : 'No data', color: fbAdsData.roas >= 5 ? '#22c55e' : '#f59e0b', bg: fbAdsData.roas >= 5 ? 'bg-green-50' : 'bg-amber-50', br: fbAdsData.roas >= 5 ? 'border-green-100' : 'border-amber-100', icon: '📈' }, 3777 { label: 'Cost/Sale', value: fbAdsData.cpa > 0 ? `৳${fbAdsData.cpa.toLocaleString()}` : '—', sub: fbAdsData.cpa > 0 && fbAdsData.cpa < 500 ? '✅ Good CPA' : fbAdsData.cpa >= 500 ? '⚠️ High' : '—', color: fbAdsData.cpa > 0 && fbAdsData.cpa < 500 ? '#22c55e' : '#f59e0b', bg: 'bg-slate-50', br: 'border-slate-100', icon: '🎯' }, 3778 { label: 'Clicks', value: (fbAdsData.clicks || 0) >= 1000 ? `${((fbAdsData.clicks || 0) / 1000).toFixed(1)}K` : String(fbAdsData.clicks || 0), sub: `CTR ${(fbAdsData.ctr || 0).toFixed(2)}%`, color: '#0ea5e9', bg: 'bg-sky-50', br: 'border-sky-100', icon: '🖱' }, 3779 { label: 'Impressions', value: (fbAdsData.impressions || 0) >= 1000000 ? `${((fbAdsData.impressions || 0) / 1000000).toFixed(1)}M` : (fbAdsData.impressions || 0) >= 1000 ? `${((fbAdsData.impressions || 0) / 1000).toFixed(1)}K` : String(fbAdsData.impressions || 0), sub: `CPC $${(fbAdsData.cpc || 0).toFixed(2)}`, color: '#8b5cf6', bg: 'bg-violet-50', br: 'border-violet-100', icon: '👁' }, 3780 ].map(m => ( 3781
3782

{m.icon} {m.label}

3783

{m.value}

3784
3785 ))} 3786
3787
3788
3789
3790

📊 Sales Attribution — FB Ads vs Organic

3791

Facebook Ads থেকে কত % order এসেছে

3792
3793
3794

{fbSalesRatio.toFixed(1)}%

3795

FB Ads থেকে

3796
3797
3798
3799
0 ? 2 : 0)}%`, background: 'linear-gradient(90deg,#1877F2,#60a5fa)', transition: 'width 0.7s ease' }} /> 3800
3801
3802 📘 FB: {fbAdsData.purchases} sales · ৳{fbAdsData.spend.toLocaleString()} cost 3803 Total confirmed: {confirmedOrdersCount} 3804
3805 {fbAdsCostBDT > 0 && fbAdsData.purchases > 0 && (() => { 3806 const fbRevEst = Math.round(confirmedOrderValue * (fbSalesRatio / 100)); 3807 const fbNet = fbRevEst - fbAdsCostBDT; 3808 return ( 3809
3810 {[ 3811 { label: 'Est. FB Revenue', val: `৳${fbRevEst.toLocaleString()}`, color: '#4ade80' }, 3812 { label: 'FB Ad Cost', val: `৳${fbAdsCostBDT.toLocaleString()}`, color: '#f87171' }, 3813 { label: 'FB Net', val: `${fbNet >= 0 ? '+' : '-'}৳${Math.abs(fbNet).toLocaleString()}`, color: fbNet >= 0 ? '#4ade80' : '#f87171' }, 3814 ].map(r => ( 3815
3816

{r.label}

3817

{r.val}

3818
3819 ))} 3820
3821 ); 3822 })()} 3823
3824
3825
3826 ) : ( 3827
3828
3829
3830

Facebook Ads সংযুক্ত করুন

3831

"Facebook Ads" ট্যাবে গিয়ে API connect করলে এখানে live spend, ROAS, sales ratio দেখা যাবে।

3832
3833
3834 )} 3835 3836 {/* ── 3 COLUMN INFO ROW ─────────────────────── */} 3837
3838 3839 {/* Order Status */} 3840
3841

3842 3843 3844 3845 Order Status 3846

3847
3848 {[ 3849 { label: 'Pending', count: pendingOrders, color: '#f59e0b', bg: '#fef3c7' }, 3850 { label: 'Confirmed', count: orders.filter(o => o.status === 'Confirmed Order').length, color: '#3b82f6', bg: '#dbeafe' }, 3851 { label: 'Completed', count: completedOrders, color: '#059669', bg: '#d1fae5' }, 3852 { label: 'Cancelled', count: cancelledOrders, color: '#ef4444', bg: '#fee2e2' }, 3853 ].map(s => ( 3854
3855
3856 {s.label} 3857 {s.count} 3858
3859
3860
0 ? (s.count / totalOrders) * 100 : 0}%`, background: s.color }} /> 3861
3862
3863 ))} 3864
3865
3866 3867 {/* Traffic & Engagement */} 3868
3869

3870 3871 3872 3873 Traffic & Engagement 3874

3875
3876 {[ 3877 { label: 'Page Views', value: pageViews.toLocaleString(), sub: `+${todayPageViews} today`, color: '#3b82f6' }, 3878 { label: 'Product Views', value: contentViews.toLocaleString(), sub: `${products.length} products`, color: '#7c3aed' }, 3879 { label: 'Checkouts Started', value: initiatedCheckouts.toLocaleString(), sub: `${totalLeads} incomplete`, color: '#059669' }, 3880 ].map(t => ( 3881
3882
3883

{t.label}

3884

{t.sub}

3885
3886

{t.value}

3887
3888 ))} 3889
3890
3891 3892 {/* Catalogue */} 3893
3894

3895 3896 3897 3898 Catalogue 3899

3900
3901 {[ 3902 { label: 'Products', value: products.length, color: '#3b82f6' }, 3903 { label: 'Featured', value: products.filter(p => p.isFeatured).length, color: '#7c3aed' }, 3904 { label: 'Reviews', value: testimonials.length, color: '#f59e0b' }, 3905 { label: 'Free Delivery', value: products.filter(p => p.freeDeliveryEnabled).length, color: '#059669' }, 3906 ].map(s => ( 3907
3908

{s.value}

3909

{s.label}

3910
3911 ))} 3912
3913
3914 3915
3916 3917 3918 {/* ── Analytics Report ─────────────────────────────────── */} 3919 {(() => { 3920 // ── Date range bounds for selected period ────────────── 3921 const now = new Date(); 3922 const todayStr = now.toISOString().slice(0, 10); 3923 let rangeStart: Date, rangeEnd: Date; 3924 3925 if (analyticsPeriod === 'today') { 3926 rangeStart = new Date(todayStr); 3927 rangeEnd = new Date(todayStr + 'T23:59:59'); 3928 } else if (analyticsPeriod === 'week') { 3929 const dow = now.getDay(); 3930 rangeStart = new Date(now); rangeStart.setDate(now.getDate() - dow); 3931 rangeEnd = new Date(now); rangeEnd.setDate(rangeStart.getDate() + 6); 3932 } else if (analyticsPeriod === 'month') { 3933 rangeStart = new Date(now.getFullYear(), now.getMonth(), 1); 3934 rangeEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59); 3935 } else if (analyticsPeriod === 'year') { 3936 rangeStart = new Date(now.getFullYear(), 0, 1); 3937 rangeEnd = new Date(now.getFullYear(), 11, 31, 23, 59, 59); 3938 } else { 3939 rangeStart = analyticsStart ? new Date(analyticsStart) : new Date(now.getFullYear(), 0, 1); 3940 rangeEnd = analyticsEnd ? new Date(analyticsEnd + 'T23:59:59') : new Date(); 3941 } 3942 3943 // ── Helper: get Date from order ──────────────────────── 3944 const getDate = (o: Order) => new Date((o as any).date || (o as any).created_at || ''); 3945 3946 // ── Filtered orders for selected period ──────────────── 3947 const filtered = orders.filter(o => { 3948 const d = getDate(o); 3949 return !isNaN(d.getTime()) && d >= rangeStart && d <= rangeEnd; 3950 }); 3951 3952 // ── KPI Stats ────────────────────────────────────────── 3953 const totalAmt = filtered.reduce((s, o) => s + Number(o.total || 0), 0); 3954 const avgVal = filtered.length ? totalAmt / filtered.length : 0; 3955 const cancelled = filtered.filter(o => o.status === 'cancelled').length; 3956 3957 // ── Daily breakdown (for bar chart) ─────────────────── 3958 const dayMap: Record = {}; 3959 filtered.forEach(o => { 3960 const d = getDate(o); 3961 if (isNaN(d.getTime())) return; 3962 const key = d.toISOString().slice(0, 10); 3963 if (!dayMap[key]) dayMap[key] = { orders: 0, revenue: 0 }; 3964 dayMap[key].orders++; 3965 dayMap[key].revenue += Number(o.total || 0); 3966 }); 3967 const days = Object.keys(dayMap).sort(); 3968 const maxDayOrders = Math.max(...days.map(d => dayMap[d].orders), 1); 3969 3970 // ── Monthly breakdown table ──────────────────────────── 3971 const monthMap: Record = {}; 3972 orders.forEach(o => { 3973 const d = getDate(o); 3974 if (isNaN(d.getTime())) return; 3975 const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; 3976 if (!monthMap[key]) monthMap[key] = { orders: 0, revenue: 0 }; 3977 monthMap[key].orders++; 3978 monthMap[key].revenue += Number(o.total || 0); 3979 }); 3980 const months = Object.keys(monthMap).sort().reverse().slice(0, 12); 3981 const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 3982 3983 // ── Yearly breakdown table ───────────────────────────── 3984 const yearMap: Record = {}; 3985 orders.forEach(o => { 3986 const d = getDate(o); 3987 if (isNaN(d.getTime())) return; 3988 const key = String(d.getFullYear()); 3989 if (!yearMap[key]) yearMap[key] = { orders: 0, revenue: 0 }; 3990 yearMap[key].orders++; 3991 yearMap[key].revenue += Number(o.total || 0); 3992 }); 3993 const years = Object.keys(yearMap).sort().reverse(); 3994 3995 // ── SVG bar chart constants ──────────────────────────── 3996 const W = 900; const H = 180; 3997 const pL = 38; const pR = 12; const pT = 14; const pB = 36; 3998 const iW = W - pL - pR; const iH = H - pT - pB; 3999 const numBars = Math.max(days.length, 1); 4000 const slot = iW / numBars; 4001 const bW = Math.min(slot * 0.7, 40); 4002 4003 const PERIODS: { key: typeof analyticsPeriod; label: string }[] = [ 4004 { key: 'today', label: 'Today' }, 4005 { key: 'week', label: 'This Week' }, 4006 { key: 'month', label: 'This Month' }, 4007 { key: 'year', label: 'This Year' }, 4008 { key: 'custom', label: 'Custom' }, 4009 ]; 4010 4011 return ( 4012
4013 {/* Header */} 4014
4015
4016
4017

4018 4019 4020 4021 Sales Analytics 4022

4023

4024 Orders & revenue report — {filtered.length} orders in selected period 4025

4026
4027 {/* Period tabs */} 4028
4029 {PERIODS.map(p => ( 4030 4038 ))} 4039
4040
4041 4042 {/* Custom date pickers */} 4043 {analyticsPeriod === 'custom' && ( 4044
4045
4046 4047 From 4048 setAnalyticsStart(e.target.value)} 4050 className="border border-slate-200 rounded-xl px-3 py-1.5 text-sm font-bold text-slate-700 focus:ring-2 focus:ring-blue-500 outline-none" /> 4051
4052
4053 To 4054 setAnalyticsEnd(e.target.value)} 4056 min={analyticsStart} 4057 className="border border-slate-200 rounded-xl px-3 py-1.5 text-sm font-bold text-slate-700 focus:ring-2 focus:ring-blue-500 outline-none" /> 4058
4059 4060 {analyticsStart && analyticsEnd ? `${filtered.length} orders found` : 'Select a date range'} 4061 4062
4063 )} 4064
4065 4066
4067 {/* KPI Cards */} 4068
4069 {[ 4070 { label: 'Orders', value: String(filtered.length), icon: ClipboardList, color: 'blue', bg: 'bg-blue-50', text: 'text-blue-600' }, 4071 { label: 'Revenue', value: `৳${totalAmt.toLocaleString()}`, icon: CreditCard, color: 'green', bg: 'bg-green-50', text: 'text-green-600' }, 4072 { label: 'Avg Order', value: `৳${Math.round(avgVal).toLocaleString()}`, icon: TrendingUp, color: 'purple', bg: 'bg-purple-50', text: 'text-purple-600' }, 4073 { label: 'Cancelled', value: String(cancelled), icon: XCircle, color: 'red', bg: 'bg-red-50', text: 'text-red-600' }, 4074 ].map(c => ( 4075
4076 4077

{c.value}

4078

{c.label}

4079
4080 ))} 4081
4082 4083 {/* Daily Orders Bar Chart */} 4084
4085

4086 4087 Daily Orders — {analyticsPeriod === 'today' ? 'Today' : analyticsPeriod === 'week' ? 'This Week' : analyticsPeriod === 'month' ? 'This Month' : analyticsPeriod === 'year' ? 'This Year' : 'Custom Range'} 4088

4089 {days.length === 0 ? ( 4090
4091 No orders in this period 4092
4093 ) : ( 4094
4095 4096 4097 4098 4099 4100 4101 4102 {[0, 0.5, 1].map(r => { 4103 const y = pT + iH * (1 - r); 4104 return ( 4105 4106 4107 4108 {Math.round(maxDayOrders * r)} 4109 4110 4111 ); 4112 })} 4113 {days.map((day, i) => { 4114 const cnt = dayMap[day].orders; 4115 const bH = Math.max((cnt / maxDayOrders) * iH, 4); 4116 const x = pL + i * slot + (slot - bW) / 2; 4117 const y = pT + iH - bH; 4118 const isToday = day === todayStr; 4119 const d = new Date(day); 4120 const label = analyticsPeriod === 'year' 4121 ? monthNames[d.getMonth()] 4122 : `${d.getDate()}/${d.getMonth() + 1}`; 4123 return ( 4124 4125 4127 4129 {cnt} 4130 4131 {(numBars <= 31 || i % 4 === 0) && ( 4132 4133 {label} 4134 4135 )} 4136 4137 {`${day}: ${cnt} orders · ৳${dayMap[day].revenue.toLocaleString()}`} 4138 4139 4140 ); 4141 })} 4142 4143 4144
4145 )} 4146
4147 4148 {/* Monthly + Yearly tables side by side */} 4149
4150 {/* Monthly Breakdown */} 4151
4152

4153 4154 Monthly Breakdown 4155

4156
4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 {months.length === 0 ? ( 4167 4168 ) : months.map((m, idx) => { 4169 const [yr, mo] = m.split('-'); 4170 const isThisMonth = m === `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; 4171 return ( 4172 4173 4177 4178 4179 4180 ); 4181 })} 4182 4183 {months.length > 0 && ( 4184 4185 4186 4187 4188 4189 4190 4191 )} 4192
MonthOrdersRevenue
No data
4174 {monthNames[parseInt(mo) - 1]} {yr} 4175 {isThisMonth && Current} 4176 {monthMap[m].orders}৳{monthMap[m].revenue.toLocaleString()}
Total ({months.length} months){months.reduce((s, m) => s + monthMap[m].orders, 0)}৳{months.reduce((s, m) => s + monthMap[m].revenue, 0).toLocaleString()}
4193
4194
4195 4196 {/* Yearly Breakdown */} 4197
4198

4199 4200 Yearly Summary 4201

4202
4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 {years.length === 0 ? ( 4213 4214 ) : years.map((yr, idx) => { 4215 const isThisYear = yr === String(now.getFullYear()); 4216 const maxOrd = Math.max(...years.map(y => yearMap[y].orders), 1); 4217 const pct = (yearMap[yr].orders / maxOrd) * 100; 4218 return ( 4219 4220 4229 4230 4231 4232 ); 4233 })} 4234 4235
YearOrdersRevenue
No data
4221
4222 {yr} 4223 {isThisYear && Current} 4224
4225
4226
4227
4228
{yearMap[yr].orders}৳{yearMap[yr].revenue.toLocaleString()}
4236
4237
4238
4239 {/* ── Profit & Loss Card ───────────────────────── */} 4240 {(() => { 4241 // Build a lookup map: productId → priceConfigs 4242 const productMap: Record = {}; 4243 products.forEach(p => { productMap[p.id] = p; }); 4244 4245 let totalRevenue = 0; 4246 let totalCost = 0; 4247 let itemsWithCost = 0; 4248 let itemsTotal = 0; 4249 4250 filtered.forEach(o => { 4251 const orderTotal = Number(o.total || 0); 4252 const deliveryCost = Number((o as any).deliveryCharge || (o as any).delivery_charge || 0); 4253 4254 totalRevenue += orderTotal; 4255 totalCost += deliveryCost; // Add delivery cost to total cost 4256 4257 const items = Array.isArray(o.items) ? o.items : []; 4258 items.forEach((item: any) => { 4259 itemsTotal++; 4260 const prod = productMap[item.productId]; 4261 if (!prod) return; 4262 4263 // Parse priceConfigs if it's a string 4264 let configs: Record = {}; 4265 try { 4266 configs = (typeof prod.priceConfigs === 'string') 4267 ? JSON.parse(prod.priceConfigs) 4268 : (prod.priceConfigs ?? prod.price_configs ?? {}); 4269 } catch (e) { 4270 configs = prod.price_configs ?? {}; 4271 } 4272 4273 // Find matching size key 4274 const sizeKey = item.selectedSize || item.size 4275 ? Object.keys(configs).find(k => k === (item.selectedSize || item.size) || k.includes(item.selectedSize || item.size)) 4276 : Object.keys(configs)[0]; 4277 4278 const cfg = sizeKey ? configs[sizeKey] : null; 4279 const mainBal = cfg?.mainBalance ?? 0; 4280 4281 if (mainBal > 0) { 4282 totalCost += Number(mainBal) * Number(item.quantity || 1); 4283 itemsWithCost++; 4284 } 4285 }); 4286 }); 4287 4288 const grossProfit = totalRevenue - totalCost; 4289 const profitPct = totalRevenue > 0 ? (grossProfit / totalRevenue) * 100 : 0; 4290 const isProfit = grossProfit >= 0; 4291 const costPct = totalRevenue > 0 ? Math.min((totalCost / totalRevenue) * 100, 100) : 0; 4292 const noCostItems = itemsTotal - itemsWithCost; 4293 4294 return ( 4295
4296
4297
4298

4299 {isProfit 4300 ? 4301 : } 4302 4303 Profit & Loss Report 4304 4305

4306

4307 Based on Main Balance (cost) vs MRP revenue · {filtered.length} orders 4308

4309
4310
4311 {isProfit ? '📈 Profit' : '📉 Loss'} {Math.abs(profitPct).toFixed(1)}% 4312
4313
4314 4315 {/* 4 P&L stat cards */} 4316
4317 {[ 4318 { label: 'Total Revenue', val: `৳${totalRevenue.toLocaleString()}`, sub: 'From orders (MRP)', color: 'bg-blue-100 text-blue-700' }, 4319 { label: 'Total Cost', val: `৳${totalCost.toLocaleString()}`, sub: 'Main Balance × Qty + Delivery', color: 'bg-slate-100 text-slate-700' }, 4320 { label: isProfit ? 'Gross Profit' : 'Gross Loss', val: `৳${Math.abs(grossProfit).toLocaleString()}`, sub: 'Revenue − Cost', color: isProfit ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700' }, 4321 { label: 'Profit Margin', val: `${profitPct >= 0 ? '+' : ''}${profitPct.toFixed(1)}%`, sub: 'Profit ÷ Revenue', color: isProfit ? 'bg-emerald-100 text-emerald-700' : 'bg-orange-100 text-orange-700' }, 4322 ].map(c => ( 4323
4324

{c.val}

4325

{c.label}

4326

{c.sub}

4327
4328 ))} 4329
4330 4331 {/* Revenue vs Cost breakdown bar */} 4332
4333
4334 Cost ({costPct.toFixed(0)}%) 4335 Profit ({(100 - costPct).toFixed(0)}%) 4336
4337
4338
4339
4340
4341
4342 ৳{totalCost.toLocaleString()} (Cost + Delivery) 4343 ৳{Math.abs(grossProfit).toLocaleString()} {isProfit ? 'profit' : 'loss'} 4344
4345
4346 4347 {noCostItems > 0 && ( 4348

4349 ⚠️ {noCostItems} item{noCostItems > 1 ? 's' : ''} have no Main Balance set — cost may be understated. Go to Products → Variations to set Main Balance. 4350

4351 )} 4352
4353 ); 4354 })()} 4355
4356
4357 ); 4358 })()} 4359 4360 4361 4362 4363 {(() => { 4364 const hourly = Array(24).fill(0); 4365 orders.forEach(o => { 4366 const raw = (o as any).date || (o as any).created_at || (o as any).confirmedAt; 4367 if (!raw) return; 4368 const d = new Date(raw); 4369 if (!isNaN(d.getTime())) hourly[d.getHours()]++; 4370 }); 4371 const maxVal = Math.max(...hourly, 1); 4372 const total24 = hourly.reduce((a, b) => a + b, 0); 4373 const peakHr = hourly.indexOf(Math.max(...hourly)); 4374 const W = 900; const H = 200; 4375 const pL = 38; const pR = 12; const pT = 14; const pB = 38; 4376 const iW = W - pL - pR; const iH = H - pT - pB; 4377 const slot = iW / 24; const bW = slot * 0.62; 4378 const fmt = (h: number) => { 4379 const s = h < 12 ? 'AM' : 'PM'; 4380 return `${h === 0 ? 12 : h > 12 ? h - 12 : h}${s}`; 4381 }; 4382 return ( 4383
4384
4385
4386

4387 4388 4389 4390 24-Hour Orders Timeline 4391

4392

4393 {total24} total order{total24 !== 1 ? 's' : ''} · Peak: {fmt(peakHr)}–{fmt((peakHr + 1) % 24)} ({hourly[peakHr]} orders) 4394

4395
4396
4397 Peak 4398 Active 4399 Empty 4400
4401
4402 4403
4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 {/* Grid lines + Y labels */} 4417 {[0, 0.25, 0.5, 0.75, 1].map(r => { 4418 const y = pT + iH * (1 - r); 4419 return ( 4420 4421 4422 4423 {Math.round(maxVal * r)} 4424 4425 4426 ); 4427 })} 4428 4429 {/* Bars + labels */} 4430 {hourly.map((cnt, h) => { 4431 const bH = cnt > 0 ? Math.max((cnt / maxVal) * iH, 6) : 0; 4432 const x = pL + h * slot + (slot - bW) / 2; 4433 const y = pT + iH - bH; 4434 const pk = h === peakHr && cnt > 0; 4435 return ( 4436 4437 {cnt === 0 4438 ? 4439 : 4440 } 4441 {cnt > 0 && ( 4442 4443 {cnt} 4444 4445 )} 4446 {h % 3 === 0 && ( 4447 4448 {fmt(h)} 4449 4450 )} 4451 4452 {`${fmt(h)} – ${fmt((h + 1) % 24)}: ${cnt} order${cnt !== 1 ? 's' : ''}`} 4453 4454 4455 ); 4456 })} 4457 4458 {/* X axis baseline */} 4459 4460 4461
4462 4463 {/* Mini color-coded hour strip */} 4464
4465 {hourly.map((cnt, h) => ( 4466
= maxVal * 0.6 ? 'bg-red-500' 4470 : cnt >= maxVal * 0.3 ? 'bg-orange-400' : 'bg-orange-200' 4471 }`} /> 4472 ))} 4473
4474
4475 ); 4476 })()} 4477 4478
4479 )} 4480 4481 {activeTab === 'orders' && canAccess('orders') && (