feat: enhance analysis page with additional revenue metrics and improve data handling
This commit is contained in:
parent
f7c13826eb
commit
6627422655
@ -70,7 +70,7 @@ protected function formattedRemainingAmount(): Attribute
|
||||
protected function statusLabel(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->status->label(),
|
||||
get: fn () => $this->status?->label(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\OrderStatus;
|
||||
use App\Enums\PriceType;
|
||||
use App\Enums\RawMaterialUnit;
|
||||
use App\Models\Attendance;
|
||||
use App\Models\CashAccount;
|
||||
use App\Models\Employee;
|
||||
@ -34,7 +36,7 @@ public function getAttendanceStats(?string $startDate, ?string $endDate): array
|
||||
$current->addDay();
|
||||
}
|
||||
|
||||
$totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count();
|
||||
$totalEmployees = Employee::whereHas('user', fn($q) => $q->where('is_active', true))->count();
|
||||
|
||||
$present = Attendance::whereBetween('attendance_date', [$start, $end])->count();
|
||||
|
||||
@ -128,24 +130,24 @@ public function getCashOverview(): array
|
||||
return [
|
||||
'total_balance' => $cashAccount->balance,
|
||||
'total_transactions' => (clone $transactions)->count(),
|
||||
'total_deposit' => (clone $transactions)->where('type', 'deposit')->sum('amount'),
|
||||
'total_withdrawal' => (clone $transactions)->where('type', 'withdrawal')->sum('amount'),
|
||||
'total_deposit' => (int) (clone $transactions)->where('type', 'deposit')->sum('amount'),
|
||||
'total_withdrawal' => (int) (clone $transactions)->where('type', 'withdrawal')->sum('amount'),
|
||||
];
|
||||
}
|
||||
|
||||
public function getRawMaterialStock(): array
|
||||
{
|
||||
$prices = RawMaterialPrice::select('stock', 'price')
|
||||
$prices = RawMaterialPrice::select('stock', 'price', 'raw_material_id')
|
||||
->with('rawMaterial:id,unit')
|
||||
->get();
|
||||
|
||||
$totalStock = $prices->sum('stock');
|
||||
$totalValue = $prices->sum(fn ($p) => $p->stock * $p->price);
|
||||
$totalValue = $prices->sum(fn($p) => $p->stock * $p->price);
|
||||
|
||||
$byUnit = [
|
||||
'yard' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === 'yard')->sum('stock'),
|
||||
'meter' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === 'meter')->sum('stock'),
|
||||
'kilogram' => $prices->filter(fn ($p) => $p->rawMaterial?->unit === 'kg')->sum('stock'),
|
||||
'yard' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::YARD)->sum('stock'),
|
||||
'meter' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::METER)->sum('stock'),
|
||||
'kilogram' => $prices->filter(fn($p) => $p->rawMaterial?->unit === RawMaterialUnit::KG)->sum('stock'),
|
||||
];
|
||||
|
||||
return [
|
||||
@ -157,8 +159,8 @@ public function getRawMaterialStock(): array
|
||||
|
||||
public function getProductStock(): array
|
||||
{
|
||||
$variants = ProductVariant::select('stock', 'reject_stock', 'retail_stock')
|
||||
->with('product:id,name')
|
||||
$variants = ProductVariant::select('id', 'product_id', 'stock', 'reject_stock', 'retail_stock')
|
||||
->with(['product:id,name', 'productPrices' => fn ($q) => $q->where('type', PriceType::CAPITAL)])
|
||||
->get();
|
||||
|
||||
$totalStock = $variants->sum('stock');
|
||||
@ -166,9 +168,9 @@ public function getProductStock(): array
|
||||
$totalRetail = $variants->sum('retail_stock');
|
||||
|
||||
$totalValue = $variants->sum(function ($v) {
|
||||
$retailPrice = $v->productPrices()->where('type', 'retail')->first()?->price ?? 0;
|
||||
$capitalPrice = $v->productPrices->first()?->price ?? 0;
|
||||
|
||||
return $v->stock * $retailPrice;
|
||||
return $v->stock * $capitalPrice;
|
||||
});
|
||||
|
||||
return [
|
||||
@ -191,15 +193,19 @@ public function getRevenueSummary(?string $startDate, ?string $endDate): array
|
||||
->selectRaw('COUNT(*) as total_orders')
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
|
||||
->selectRaw('COALESCE(SUM(discount), 0) as total_discount')
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) - COALESCE(SUM(total_amount), 0) as total_marketplace_fees')
|
||||
->selectRaw('COALESCE(SUM(discount), 0) as total_deduction')
|
||||
->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net')
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse")
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail")
|
||||
->first();
|
||||
|
||||
return [
|
||||
'total_revenue' => (int) $stats->total_revenue,
|
||||
'total_discount' => (int) $stats->total_discount,
|
||||
'total_marketplace_fees' => (int) $stats->total_marketplace_fees,
|
||||
'total_deduction' => (int) $stats->total_deduction,
|
||||
'net' => (int) $stats->net,
|
||||
'net_warehouse' => (int) $stats->net_warehouse,
|
||||
'net_retail' => (int) $stats->net_retail,
|
||||
'total_orders' => (int) $stats->total_orders,
|
||||
'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0,
|
||||
];
|
||||
@ -214,14 +220,15 @@ public function getMonthlyRevenue(?string $startDate, ?string $endDate): array
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||
->selectRaw('COALESCE(SUM(total_amount) - SUM(discount), 0) as net')
|
||||
->selectRaw('0 as net_warehouse')
|
||||
->selectRaw('0 as net_retail')
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN price_type != 'retail' THEN total_amount ELSE 0 END), 0) as net_warehouse")
|
||||
->selectRaw("COALESCE(SUM(CASE WHEN price_type = 'retail' THEN total_amount ELSE 0 END), 0) as net_retail")
|
||||
->selectRaw('COALESCE(SUM(discount), 0) as deduction')
|
||||
->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();
|
||||
->get()
|
||||
->map(fn($item) => $item->only(['month', 'total', 'net', 'net_warehouse', 'net_retail', 'deduction']));
|
||||
|
||||
return $monthly->toArray();
|
||||
return $monthly->values()->toArray();
|
||||
}
|
||||
|
||||
public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate): array
|
||||
@ -236,9 +243,10 @@ public function getMonthlyRevenueByChannel(?string $startDate, ?string $endDate)
|
||||
->selectRaw("SUM(CASE WHEN channel = 'tiktok' THEN total_amount ELSE 0 END) as tiktok")
|
||||
->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();
|
||||
->get()
|
||||
->map(fn($item) => $item->only(['month', 'store', 'shopee', 'tiktok']));
|
||||
|
||||
return $monthly->toArray();
|
||||
return $monthly->values()->toArray();
|
||||
}
|
||||
|
||||
public function getRevenueByPaymentType(?string $startDate, ?string $endDate): array
|
||||
@ -251,7 +259,7 @@ public function getRevenueByPaymentType(?string $startDate, ?string $endDate): a
|
||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total')
|
||||
->groupBy('payment_type')
|
||||
->get()
|
||||
->map(fn ($item) => [
|
||||
->map(fn($item) => [
|
||||
'payment_type' => $item->payment_type,
|
||||
'label' => $item->payment_type->label(),
|
||||
'total' => (int) $item->total,
|
||||
@ -288,7 +296,7 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array
|
||||
$expenseMonthly = Expense::query();
|
||||
$this->applyDateFilter($expenseMonthly, $startDate, $endDate);
|
||||
|
||||
$expenseByMonth = (clone $expenseMonthly)
|
||||
$expenseByMonth = (clone $expenseMonthly)->toBase()
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as expense')
|
||||
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||
@ -298,7 +306,7 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array
|
||||
$purchaseMonthly = Purchase::query();
|
||||
$this->applyDateFilter($purchaseMonthly, $startDate, $endDate);
|
||||
|
||||
$purchaseByMonth = (clone $purchaseMonthly)
|
||||
$purchaseByMonth = (clone $purchaseMonthly)->toBase()
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw('COALESCE(SUM(total), 0) as purchase')
|
||||
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||
@ -308,30 +316,30 @@ public function getMonthlyExpense(?string $startDate, ?string $endDate): array
|
||||
$advanceMonthly = EmployeeAdvance::where('status', 'paid');
|
||||
$this->applyDateFilter($advanceMonthly, $startDate, $endDate);
|
||||
|
||||
$advanceByMonth = (clone $advanceMonthly)
|
||||
$advanceByMonth = (clone $advanceMonthly)->toBase()
|
||||
->selectRaw("DATE_FORMAT(created_at, '%b %Y') as month")
|
||||
->selectRaw('COALESCE(SUM(amount), 0) as advance')
|
||||
->groupBy(DB::raw("DATE_FORMAT(created_at, '%Y-%m')"), DB::raw("DATE_FORMAT(created_at, '%b %Y')"))
|
||||
->get()
|
||||
->keyBy('month');
|
||||
|
||||
$allMonths = collect();
|
||||
$allMonths = [];
|
||||
foreach ([$expenseByMonth, $purchaseByMonth, $advanceByMonth] as $data) {
|
||||
foreach ($data as $month => $row) {
|
||||
if (! $allMonths->has($month)) {
|
||||
if (! array_key_exists($month, $allMonths)) {
|
||||
$allMonths[$month] = ['month' => $month, 'total' => 0, 'purchase' => 0, 'expense' => 0, 'advance' => 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($allMonths as $month => &$row) {
|
||||
$row['purchase'] = (int) ($purchaseByMonth[$month]['purchase'] ?? 0);
|
||||
$row['expense'] = (int) ($expenseByMonth[$month]['expense'] ?? 0);
|
||||
$row['advance'] = (int) ($advanceByMonth[$month]['advance'] ?? 0);
|
||||
$row['purchase'] = (int) ($purchaseByMonth[$month]->purchase ?? 0);
|
||||
$row['expense'] = (int) ($expenseByMonth[$month]->expense ?? 0);
|
||||
$row['advance'] = (int) ($advanceByMonth[$month]->advance ?? 0);
|
||||
$row['total'] = $row['purchase'] + $row['expense'] + $row['advance'];
|
||||
}
|
||||
|
||||
return array_values($allMonths->toArray());
|
||||
return array_values($allMonths);
|
||||
}
|
||||
|
||||
public function getBusyHours(?string $startDate, ?string $endDate): array
|
||||
@ -393,7 +401,7 @@ public function getTopSuppliers(?string $startDate, ?string $endDate): array
|
||||
$query = Purchase::query();
|
||||
$this->applyDateFilter($query, $startDate, $endDate);
|
||||
|
||||
return (clone $query)
|
||||
return (clone $query)->toBase()
|
||||
->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id')
|
||||
->select('suppliers.name')
|
||||
->selectRaw('COALESCE(SUM(purchases.total), 0) as total_amount')
|
||||
@ -410,7 +418,7 @@ public function getTopCustomers(?string $startDate, ?string $endDate): array
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED)->whereNotNull('orders.customer_id');
|
||||
$this->applyDateFilter($query, $startDate, $endDate);
|
||||
|
||||
return (clone $query)
|
||||
return (clone $query)->toBase()
|
||||
->join('customers', 'orders.customer_id', '=', 'customers.id')
|
||||
->select('customers.name')
|
||||
->selectRaw('COALESCE(SUM(orders.total_amount), 0) as total_amount')
|
||||
@ -427,7 +435,7 @@ public function getTopProducts(?string $startDate, ?string $endDate): array
|
||||
$query = Order::where('orders.status', OrderStatus::COMPLETED);
|
||||
$this->applyDateFilter($query, $startDate, $endDate);
|
||||
|
||||
return (clone $query)
|
||||
return (clone $query)->toBase()
|
||||
->join('order_items', 'orders.id', '=', 'order_items.order_id')
|
||||
->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id')
|
||||
->join('products', 'product_variants.product_id', '=', 'products.id')
|
||||
@ -447,7 +455,7 @@ public function getMarketingSales(?string $startDate, ?string $endDate): array
|
||||
->whereNotNull('orders.marketing_id');
|
||||
$this->applyDateFilter($query, $startDate, $endDate);
|
||||
|
||||
return (clone $query)
|
||||
return (clone $query)->toBase()
|
||||
->join('users', 'orders.marketing_id', '=', 'users.id')
|
||||
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
||||
->leftJoin('order_items', 'orders.id', '=', 'order_items.order_id')
|
||||
@ -464,13 +472,13 @@ public function getMarketingSales(?string $startDate, ?string $endDate): array
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function applyDateFilter($query, ?string $startDate, ?string $endDate): void
|
||||
private function applyDateFilter($query, ?string $startDate, ?string $endDate, string $dateColumn = 'created_at'): void
|
||||
{
|
||||
if ($startDate) {
|
||||
$query->whereDate('created_at', '>=', $startDate);
|
||||
$query->whereDate($dateColumn, '>=', $startDate);
|
||||
}
|
||||
if ($endDate) {
|
||||
$query->whereDate('created_at', '<=', $endDate);
|
||||
$query->whereDate($dateColumn, '<=', $endDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,17 @@
|
||||
import { StatCard } from '@/components/card/stat-card';
|
||||
import { DatePicker } from '@/components/inputs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
|
||||
import { Head, router } from '@inertiajs/react';
|
||||
import {
|
||||
Banknote,
|
||||
@ -19,20 +33,6 @@ import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import { StatCard } from '@/components/card/stat-card';
|
||||
import { DatePicker } from '@/components/inputs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useCan } from '@/hooks/use-can';
|
||||
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
|
||||
|
||||
type Filters = {
|
||||
start_date?: string;
|
||||
@ -83,8 +83,10 @@ type AnalysisProps = {
|
||||
revenueSummary: {
|
||||
total_revenue: number;
|
||||
total_discount: number;
|
||||
total_marketplace_fees: number;
|
||||
total_deduction: number;
|
||||
net: number;
|
||||
net_warehouse: number;
|
||||
net_retail: number;
|
||||
total_orders: number;
|
||||
avg_order: number;
|
||||
};
|
||||
@ -264,7 +266,6 @@ export default function Analysis({
|
||||
marketingSales,
|
||||
}: AnalysisProps) {
|
||||
const { can, hasAnyRole, hasRole } = useCan();
|
||||
|
||||
const [startDate, setStartDate] = useState(initialFilters.start_date ?? '');
|
||||
const [endDate, setEndDate] = useState(initialFilters.end_date ?? '');
|
||||
const [selectedPreset, setSelectedPreset] = useState('');
|
||||
@ -497,18 +498,18 @@ export default function Analysis({
|
||||
<div className="flex flex-col sm:flex-row">
|
||||
{Object.entries(REVENUE_COLORS).filter(([key]) => {
|
||||
if (hasAnyRole(['owner', 'developer'])) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasRole('cashier')) {
|
||||
return key === 'total' || key === 'deduction';
|
||||
}
|
||||
return key === 'total' || key === 'deduction';
|
||||
}
|
||||
|
||||
return key === 'total' || key === 'net' || key === 'deduction';
|
||||
}).map(([key]) => (
|
||||
<div key={key} className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6">
|
||||
<span className="text-xs text-muted-foreground">{key === 'net_warehouse' ? 'Total Gudang' : key === 'net_retail' ? 'Total Ecer' : key === 'total' ? 'Total' : key === 'net' ? 'Bersih' : 'Potongan'}</span>
|
||||
<span className="text-sm">Rp{formatRupiah(key === 'total' ? revenueSummary.total_revenue : key === 'deduction' ? revenueSummary.total_deduction : 0)}</span>
|
||||
<span className="text-sm">Rp{formatRupiah(key === 'total' ? revenueSummary.total_revenue : key === 'net' ? revenueSummary.net : key === 'net_warehouse' ? revenueSummary.net_warehouse : key === 'net_retail' ? revenueSummary.net_retail : revenueSummary.total_deduction)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -641,6 +642,7 @@ return key === 'total' || key === 'deduction';
|
||||
<YAxis tickLine={false} axisLine={false} tickFormatter={(v) => `Rp${formatRupiahShort(v)}`} />
|
||||
<Tooltip content={<BarTooltip />} />
|
||||
<Bar dataKey="total" fill={EXPENSE_COLORS.total} radius={[4, 4, 0, 0]} name="Total" />
|
||||
<Bar dataKey="purchase" fill={EXPENSE_COLORS.purchase} radius={[4, 4, 0, 0]} name="Belanja" />
|
||||
<Bar dataKey="expense" fill={EXPENSE_COLORS.expense} radius={[4, 4, 0, 0]} name="Pengeluaran" />
|
||||
<Bar dataKey="advance" fill={EXPENSE_COLORS.advance} radius={[4, 4, 0, 0]} name="Kasbon" />
|
||||
</BarChart>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user