feat: add total products sold to revenue summary and update dashboard display for marketing roles

This commit is contained in:
Yoga Pangestu 2026-08-23 20:12:20 +07:00
parent 6a196002fb
commit 6e7a4044b6
4 changed files with 69 additions and 47 deletions

View File

@ -13,6 +13,7 @@
use App\Models\Expense;
use App\Models\LeaveRequest;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Payroll;
use App\Models\User;
use Carbon\Carbon;
@ -154,6 +155,9 @@ public function getRevenueSummary(?User $user = null): array
$gross = $totalRevenue - $totalCogs;
$net = $gross - $payrollTotal - $expenseTotal;
$orderIds = (clone $baseQuery)->pluck('id');
$totalProductsSold = (int) OrderItem::whereIn('order_id', $orderIds)->sum('quantity');
return [
'total_revenue' => $totalRevenue,
'total_discount' => (int) $stats->total_discount,
@ -164,6 +168,7 @@ public function getRevenueSummary(?User $user = null): array
'payroll_total' => $payrollTotal,
'expense_total' => $expenseTotal,
'total_orders' => (int) $stats->total_orders,
'total_products_sold' => $totalProductsSold,
];
}

View File

@ -483,6 +483,7 @@ export default function Analysis({
monthlyRetailRevenue,
}: AnalysisProps) {
const { can, hasAnyRole, hasRole } = useCan();
const isMarketing = hasAnyRole(['marketing-offline', 'marketing-online']);
const [startDate, setStartDate] = useState(initialFilters.start_date ?? '');
const [endDate, setEndDate] = useState(initialFilters.end_date ?? '');
const [selectedPreset, setSelectedPreset] = useState('');
@ -591,8 +592,8 @@ export default function Analysis({
return { statCards: 1, revenue: 2, revenueByChannel: 3, orderPie: 4, totalOrder: 5, topProducts: 6, topCustomers: 7 };
}
if (hasRole('marketing')) {
return { statCards: 1, revenue: 2, revenueByChannel: 3, orderPie: 4, totalOrder: 5, revenueTrend: 6, topProducts: 7, topCustomers: 8 };
if (isMarketing) {
return { statCards: 1, revenueByChannel: 3, orderPie: 4, totalOrder: 5, revenueTrend: 6, topProducts: 7, topCustomers: 8 };
}
return {};
@ -753,7 +754,7 @@ export default function Analysis({
</Card>
)}
{can('analysis.revenue') && (
{can('analysis.revenue') && !isMarketing && (
<Card className="py-0" style={{ order: sectionOrder.revenue ?? 99 }}>
<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!">
@ -911,7 +912,7 @@ export default function Analysis({
</div>
)}
{can('analysis.revenue') && (
{can('analysis.revenue') && !isMarketing && (
<Card className="py-0" style={{ order: 3 }}>
<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!">

View File

@ -47,7 +47,8 @@ export function TransactionCardRow({
onUpdateStatus,
onPrint,
}: TransactionCardRowParams) {
const { can } = useCan();
const { can, hasAnyRole } = useCan();
const isMarketing = hasAnyRole(['marketing-offline', 'marketing-online']);
const [refundDialogOpen, setRefundDialogOpen] = useState(false);
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
const variantCount = transaction.items_count ?? 0;
@ -188,18 +189,22 @@ export function TransactionCardRow({
</span>
{formatCurrency(transaction.total_amount)}
</span>
<span className={transaction.profit >= 0 ? 'text-green-600' : 'text-red-600'}>
<span className="font-normal text-muted-foreground">
Laba:{' '}
{!isMarketing && (
<span className={transaction.profit >= 0 ? 'text-green-600' : 'text-red-600'}>
<span className="font-normal text-muted-foreground">
Laba:{' '}
</span>
{formatCurrency(transaction.profit)}
</span>
{formatCurrency(transaction.profit)}
</span>
<span>
<span className="text-muted-foreground">
HPP:{' '}
)}
{!isMarketing && (
<span>
<span className="text-muted-foreground">
HPP:{' '}
</span>
{formatCurrency(transaction.cogs)}
</span>
{formatCurrency(transaction.cogs)}
</span>
)}
</div>
{transaction.photo_url && (

View File

@ -53,6 +53,7 @@ type DashboardProps = {
payroll_total: number;
expense_total: number;
total_orders: number;
total_products_sold: number;
};
expenseSummary: {
total: number;
@ -97,7 +98,8 @@ export default function Dashboard({
isOnLeave,
canCheckIn,
}: DashboardProps) {
const { can, hasRole } = useCan();
const { can, hasRole, hasAnyRole } = useCan();
const isMarketing = hasAnyRole(['marketing-offline', 'marketing-online']);
const { auth } = usePage().props as { auth: { user?: { username?: string } } };
const [currentTime, setCurrentTime] = useState(new Date());
const [showCamera, setShowCamera] = useState(false);
@ -336,37 +338,46 @@ export default function Dashboard({
mainLabel="Total"
mainValue={`Rp${formatRupiah(revenueSummary.total_revenue)}`}
subLabel={`${revenueSummary.total_orders} transaksi selesai`}
items={[
{
label: 'Kotor',
value: `Rp${formatRupiah(revenueSummary.gross)}`,
},
{
label: 'Bersih',
value: `Rp${formatRupiah(revenueSummary.net)}`,
},
{
label: 'Gaji',
value: `Rp${formatRupiah(revenueSummary.payroll_total)}`,
},
{
label: 'Pengeluaran',
value: `Rp${formatRupiah(revenueSummary.expense_total)}`,
},
{
label: 'Potongan',
value: `Rp${formatRupiah(revenueSummary.total_deduction)}`,
},
{
label: 'Diskon',
value: `Rp${formatRupiah(revenueSummary.total_discount)}`,
},
{
label: 'HPP',
value: `Rp${formatRupiah(revenueSummary.total_cogs)}`,
},
]}
cols={visibleStatsCount === 1 ? 4 : visibleStatsCount === 2 ? 3 : 2}
items={
isMarketing
? [
{
label: 'Produk Terjual',
value: revenueSummary.total_products_sold.toLocaleString('id-ID'),
},
]
: [
{
label: 'Kotor',
value: `Rp${formatRupiah(revenueSummary.gross)}`,
},
{
label: 'Bersih',
value: `Rp${formatRupiah(revenueSummary.net)}`,
},
{
label: 'Gaji',
value: `Rp${formatRupiah(revenueSummary.payroll_total)}`,
},
{
label: 'Pengeluaran',
value: `Rp${formatRupiah(revenueSummary.expense_total)}`,
},
{
label: 'Potongan',
value: `Rp${formatRupiah(revenueSummary.total_deduction)}`,
},
{
label: 'Diskon',
value: `Rp${formatRupiah(revenueSummary.total_discount)}`,
},
{
label: 'HPP',
value: `Rp${formatRupiah(revenueSummary.total_cogs)}`,
},
]
}
cols={isMarketing ? 2 : visibleStatsCount === 1 ? 4 : visibleStatsCount === 2 ? 3 : 2}
/>
)}