feat: add monthly retail revenue calculation and display in analysis component
This commit is contained in:
parent
a301e96f33
commit
42f7214efa
@ -45,6 +45,7 @@ public function index(Request $request): Response
|
|||||||
$marketingSales = $this->service->getMarketingSales($startDate, $endDate, $user);
|
$marketingSales = $this->service->getMarketingSales($startDate, $endDate, $user);
|
||||||
$orderStats = $this->service->getOrderStats($startDate, $endDate, $user);
|
$orderStats = $this->service->getOrderStats($startDate, $endDate, $user);
|
||||||
$revenueTrend = $this->service->getRevenueTrend($startDate, $endDate, $user);
|
$revenueTrend = $this->service->getRevenueTrend($startDate, $endDate, $user);
|
||||||
|
$monthlyRetailRevenue = $this->service->getMonthlyRetailRevenue($startDate, $endDate, $user);
|
||||||
|
|
||||||
return Inertia::render('admin/analysis/index', [
|
return Inertia::render('admin/analysis/index', [
|
||||||
'filters' => [
|
'filters' => [
|
||||||
@ -72,6 +73,7 @@ public function index(Request $request): Response
|
|||||||
'marketingSales' => $marketingSales,
|
'marketingSales' => $marketingSales,
|
||||||
'orderStats' => $orderStats,
|
'orderStats' => $orderStats,
|
||||||
'revenueTrend' => $revenueTrend,
|
'revenueTrend' => $revenueTrend,
|
||||||
|
'monthlyRetailRevenue' => $monthlyRetailRevenue,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -445,6 +445,75 @@ public function getMonthlyRevenue(?string $startDate, ?string $endDate, ?User $u
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMonthlyRetailRevenue(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||||
|
{
|
||||||
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
$this->applyDateFilter($query, $startDate, $endDate, 'orders.created_at');
|
||||||
|
|
||||||
|
if ($user && $this->isMarketingUser($user)) {
|
||||||
|
$query->where('orders.marketing_id', $user->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user && $this->isCashierUser($user)) {
|
||||||
|
$query->whereIn('orders.created_by_id', $this->getCashierUserIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
$retailOrderIds = OrderItem::where('stock_quality', 'retail')
|
||||||
|
->pluck('order_id')
|
||||||
|
->unique();
|
||||||
|
|
||||||
|
$query->whereIn('orders.id', $retailOrderIds);
|
||||||
|
|
||||||
|
$monthly = (clone $query)
|
||||||
|
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||||
|
->selectRaw('COALESCE(SUM(discount), 0) as discount')
|
||||||
|
->selectRaw('COALESCE(SUM(nego_price), 0) as deduction')
|
||||||
|
->selectRaw('COALESCE(SUM(cogs), 0) as cogs')
|
||||||
|
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||||
|
->orderBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"))
|
||||||
|
->get()
|
||||||
|
->keyBy('month');
|
||||||
|
|
||||||
|
$summary = (clone $query)
|
||||||
|
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||||
|
->selectRaw('COALESCE(SUM(discount), 0) as discount')
|
||||||
|
->selectRaw('COALESCE(SUM(nego_price), 0) as deduction')
|
||||||
|
->selectRaw('COALESCE(SUM(cogs), 0) as cogs')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$gross = (int) $summary->total - (int) $summary->cogs;
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($monthly as $month => $row) {
|
||||||
|
$total = (int) $row->total;
|
||||||
|
$discount = (int) $row->discount;
|
||||||
|
$deduction = (int) $row->deduction;
|
||||||
|
$cogs = (int) $row->cogs;
|
||||||
|
$itemGross = $total - $cogs;
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'month' => $month,
|
||||||
|
'total' => $total,
|
||||||
|
'gross' => $itemGross,
|
||||||
|
'discount' => $discount,
|
||||||
|
'deduction' => $deduction,
|
||||||
|
'cogs' => $cogs,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'monthly' => $result,
|
||||||
|
'summary' => [
|
||||||
|
'total' => (int) $summary->total,
|
||||||
|
'discount' => (int) $summary->discount,
|
||||||
|
'deduction' => (int) $summary->deduction,
|
||||||
|
'cogs' => (int) $summary->cogs,
|
||||||
|
'gross' => $gross,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate, ?User $user = null): array
|
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate, ?User $user = null): array
|
||||||
{
|
{
|
||||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||||
|
|||||||
@ -203,6 +203,23 @@ type AnalysisProps = {
|
|||||||
date: string;
|
date: string;
|
||||||
qty: number;
|
qty: number;
|
||||||
}>;
|
}>;
|
||||||
|
monthlyRetailRevenue: {
|
||||||
|
monthly: Array<{
|
||||||
|
month: string;
|
||||||
|
total: number;
|
||||||
|
gross: number;
|
||||||
|
discount: number;
|
||||||
|
deduction: number;
|
||||||
|
cogs: number;
|
||||||
|
}>;
|
||||||
|
summary: {
|
||||||
|
total: number;
|
||||||
|
discount: number;
|
||||||
|
deduction: number;
|
||||||
|
cogs: number;
|
||||||
|
gross: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const revenueChartConfig = (() => {
|
const revenueChartConfig = (() => {
|
||||||
@ -219,6 +236,19 @@ const revenueChartConfig = (() => {
|
|||||||
|
|
||||||
const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const;
|
const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const;
|
||||||
|
|
||||||
|
const retailRevenueChartConfig = (() => {
|
||||||
|
const colors = generateRandomColors(5);
|
||||||
|
return {
|
||||||
|
total: { label: 'Total', color: colors[0] },
|
||||||
|
gross: { label: 'Keuntungan Kotor', color: colors[1] },
|
||||||
|
deduction: { label: 'Potongan Nego', color: colors[2] },
|
||||||
|
discount: { label: 'Diskon', color: colors[3] },
|
||||||
|
cogs: { label: 'HPP', color: colors[4] },
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const retailRevenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross'] as const;
|
||||||
|
|
||||||
const expenseChartConfig = (() => {
|
const expenseChartConfig = (() => {
|
||||||
const colors = generateRandomColors(4);
|
const colors = generateRandomColors(4);
|
||||||
return {
|
return {
|
||||||
@ -450,12 +480,14 @@ export default function Analysis({
|
|||||||
marketingSales,
|
marketingSales,
|
||||||
orderStats,
|
orderStats,
|
||||||
revenueTrend,
|
revenueTrend,
|
||||||
|
monthlyRetailRevenue,
|
||||||
}: AnalysisProps) {
|
}: AnalysisProps) {
|
||||||
const { can, hasAnyRole, hasRole } = useCan();
|
const { can, hasAnyRole, hasRole } = useCan();
|
||||||
const [startDate, setStartDate] = useState(initialFilters.start_date ?? '');
|
const [startDate, setStartDate] = useState(initialFilters.start_date ?? '');
|
||||||
const [endDate, setEndDate] = useState(initialFilters.end_date ?? '');
|
const [endDate, setEndDate] = useState(initialFilters.end_date ?? '');
|
||||||
const [selectedPreset, setSelectedPreset] = useState('');
|
const [selectedPreset, setSelectedPreset] = useState('');
|
||||||
const [activeRevenueKey, setActiveRevenueKey] = useState<keyof typeof revenueChartConfig>('total');
|
const [activeRevenueKey, setActiveRevenueKey] = useState<keyof typeof revenueChartConfig>('total');
|
||||||
|
const [activeRetailRevenueKey, setActiveRetailRevenueKey] = useState<keyof typeof retailRevenueChartConfig>('total');
|
||||||
const [activeExpenseKey, setActiveExpenseKey] = useState<keyof typeof expenseChartConfig>('total');
|
const [activeExpenseKey, setActiveExpenseKey] = useState<keyof typeof expenseChartConfig>('total');
|
||||||
const [activeStockKey, setActiveStockKey] = useState<'good' | 'reject' | 'retail'>('good');
|
const [activeStockKey, setActiveStockKey] = useState<'good' | 'reject' | 'retail'>('good');
|
||||||
|
|
||||||
@ -878,6 +910,70 @@ export default function Analysis({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{can('analysis.revenue') && (
|
||||||
|
<Card className="py-0" style={{ order: (sectionOrder.revenueByChannel ?? 99) + 0.5 }}>
|
||||||
|
<CardHeader className="flex flex-col items-stretch border-b p-0! sm:flex-row">
|
||||||
|
<div className="flex flex-1 flex-col justify-center gap-1 px-6 pt-4 pb-3 sm:py-0!">
|
||||||
|
<CardTitle>Pendapatan Penjualan Ecer</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row">
|
||||||
|
{retailRevenueKeys.map((key) => {
|
||||||
|
const value = key === 'total' ? monthlyRetailRevenue.summary.total : key === 'discount' ? monthlyRetailRevenue.summary.discount : key === 'gross' ? monthlyRetailRevenue.summary.gross : key === 'cogs' ? monthlyRetailRevenue.summary.cogs : monthlyRetailRevenue.summary.deduction;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
data-active={activeRetailRevenueKey === key}
|
||||||
|
className="relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-4 py-2 text-left even:border-l data-[active=true]:bg-muted/30 sm:border-t-0 sm:border-l sm:px-5 sm:py-3"
|
||||||
|
onClick={() => setActiveRetailRevenueKey(key)}
|
||||||
|
>
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{retailRevenueChartConfig[key].label}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs leading-none font-semibold sm:text-sm">
|
||||||
|
Rp{formatRupiah(value)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-2 sm:p-6">
|
||||||
|
{monthlyRetailRevenue.monthly.length > 0 ? (
|
||||||
|
<ChartContainer config={retailRevenueChartConfig} className="aspect-auto h-[250px] w-full">
|
||||||
|
<BarChart data={monthlyRetailRevenue.monthly} margin={{ left: 12, right: 12 }}>
|
||||||
|
<CartesianGrid vertical={false} />
|
||||||
|
<XAxis dataKey="month" tickLine={false} axisLine={false} tickMargin={8} />
|
||||||
|
<ChartTooltip
|
||||||
|
cursor={false}
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
className="w-[150px]"
|
||||||
|
nameKey="views"
|
||||||
|
formatter={(value, name, item, index) => (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="h-2.5 w-2.5 shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)"
|
||||||
|
style={{ "--color-bg": item.payload?.fill ?? item.color, "--color-border": item.payload?.fill ?? item.color } as React.CSSProperties}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground">{retailRevenueChartConfig[activeRetailRevenueKey]?.label ?? name}</span>
|
||||||
|
<span className="ml-auto font-mono font-medium text-foreground tabular-nums">
|
||||||
|
Rp{Number(value).toLocaleString('id-ID')}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Bar dataKey={activeRetailRevenueKey} fill={`var(--color-${activeRetailRevenueKey})`} radius={[4, 4, 0, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-[300px] items-center justify-center text-muted-foreground">Belum ada data pendapatan ecer</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{can('analysis.revenue') && (
|
{can('analysis.revenue') && (
|
||||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4" style={{ order: sectionOrder.orderPie ?? 99 }}>
|
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4" style={{ order: sectionOrder.orderPie ?? 99 }}>
|
||||||
<DashboardPieChart
|
<DashboardPieChart
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user