feat: enable dynamic date range filtering for all dashboard and analysis metrics

This commit is contained in:
Yoga Pangestu 2026-06-23 14:28:25 +07:00
parent 41ee835f1f
commit 993a87cc66
5 changed files with 542 additions and 293 deletions

View File

@ -3,19 +3,40 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Services\System\DashboardService;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
class AnalysisController extends Controller class AnalysisController extends Controller
{ {
public function __construct(
private readonly DashboardService $dashboardService,
) {}
public function index(Request $request): Response public function index(Request $request): Response
{ {
$startDate = $request->query('start_date') ? Carbon::parse($request->query('start_date'))->startOfDay() : null;
$endDate = $request->query('end_date') ? Carbon::parse($request->query('end_date'))->endOfDay() : null;
return Inertia::render('admin/Analysis', [ return Inertia::render('admin/Analysis', [
'filters' => [ 'filters' => [
'search' => $request->query('search', ''), 'start_date' => $request->query('start_date', ''),
'status' => $request->query('status', ''), 'end_date' => $request->query('end_date', ''),
], ],
'rawMaterialStock' => $this->dashboardService->getRawMaterialStock(),
'productStock' => $this->dashboardService->getProductStock(),
'kasbonSummary' => $this->dashboardService->getKasbonSummary($startDate, $endDate),
'leaveRequestSummary' => $this->dashboardService->getLeaveRequestSummary($startDate, $endDate),
'lowStockProducts' => $this->dashboardService->getLowStockProducts(),
'lowStockMaterials' => $this->dashboardService->getLowStockMaterials(),
'attendanceToday' => $this->dashboardService->getAttendanceToday($startDate, $endDate),
'cashAccounts' => $this->dashboardService->getCashAccounts(),
'cashSummary' => $this->dashboardService->getCashSummary($startDate, $endDate),
'purchaseSummary' => $this->dashboardService->getPurchaseSummary($startDate, $endDate),
'monthlyExpenses' => $this->dashboardService->getMonthlyExpenses($startDate, $endDate),
'revenueSummary' => $this->dashboardService->getRevenueSummary($startDate, $endDate),
]); ]);
} }
} }

View File

@ -20,29 +20,27 @@ public function index(): Response
$startOfMonth = $now->copy()->startOfMonth(); $startOfMonth = $now->copy()->startOfMonth();
$endOfMonth = $now->copy()->endOfMonth(); $endOfMonth = $now->copy()->endOfMonth();
$startOfDay = Carbon::today()->startOfDay();
$endOfDay = Carbon::today()->endOfDay();
return Inertia::render('admin/Dashboard', [ return Inertia::render('admin/Dashboard', [
'rawMaterialStock' => $this->dashboardService->getRawMaterialStock(),
'productStock' => $this->dashboardService->getProductStock(),
'lowStockProducts' => $this->dashboardService->getLowStockProducts(),
'lowStockMaterials' => $this->dashboardService->getLowStockMaterials(),
'topSuppliers' => $this->dashboardService->getTopSuppliers(), 'topSuppliers' => $this->dashboardService->getTopSuppliers(),
'topCustomers' => $this->dashboardService->getTopCustomers(), 'topCustomers' => $this->dashboardService->getTopCustomers(),
'topProducts' => $this->dashboardService->getTopProducts(), 'topProducts' => $this->dashboardService->getTopProducts(),
'purchaseSummary' => $this->dashboardService->getPurchaseSummary(), 'purchaseSummary' => $this->dashboardService->getPurchaseSummary($startOfDay, $endOfDay),
'cuttingSummary' => $this->dashboardService->getCuttingSummary(), 'cuttingSummary' => $this->dashboardService->getCuttingSummary(),
'cuttingByStatus' => $this->dashboardService->getCuttingByStatus(), 'cuttingByStatus' => $this->dashboardService->getCuttingByStatus(),
'orderStats' => $this->dashboardService->getOrderStats(), 'orderStats' => $this->dashboardService->getOrderStats(),
'revenueSummary' => $this->dashboardService->getRevenueSummary(), 'revenueSummary' => $this->dashboardService->getRevenueSummary($startOfDay, $endOfDay),
'marketplaceSummary' => $this->dashboardService->getMarketplaceSummary(), 'marketplaceSummary' => $this->dashboardService->getMarketplaceSummary(),
'cashAccounts' => $this->dashboardService->getCashAccounts(), 'cashAccounts' => $this->dashboardService->getCashAccounts(),
'cashSummary' => $this->dashboardService->getCashSummary($startOfMonth, $endOfMonth), 'cashSummary' => $this->dashboardService->getCashSummary($startOfDay, $endOfDay),
'monthlyCashFlow' => $this->dashboardService->getMonthlyCashFlow(), 'monthlyCashFlow' => $this->dashboardService->getMonthlyCashFlow(),
'monthlyExpenses' => $this->dashboardService->getMonthlyExpenses($startOfMonth, $endOfMonth), 'monthlyExpenses' => $this->dashboardService->getMonthlyExpenses($startOfDay, $endOfDay),
'kasbonSummary' => $this->dashboardService->getKasbonSummary(), 'kasbonSummary' => $this->dashboardService->getKasbonSummary($startOfDay, $endOfDay),
'payrollSummary' => $this->dashboardService->getPayrollSummary($startOfMonth, $endOfMonth), 'payrollSummary' => $this->dashboardService->getPayrollSummary($startOfMonth, $endOfMonth),
'leaveRequestSummary' => $this->dashboardService->getLeaveRequestSummary($startOfMonth, $endOfMonth),
'employeeSummary' => $this->dashboardService->getEmployeeSummary(), 'employeeSummary' => $this->dashboardService->getEmployeeSummary(),
'attendanceToday' => $this->dashboardService->getAttendanceToday(), 'attendanceToday' => $this->dashboardService->getAttendanceToday($startOfDay, $endOfDay),
'monthlyRevenueTrend' => $this->dashboardService->getMonthlyRevenueTrend(), 'monthlyRevenueTrend' => $this->dashboardService->getMonthlyRevenueTrend(),
'monthlyPurchaseTrend' => $this->dashboardService->getMonthlyPurchaseTrend(), 'monthlyPurchaseTrend' => $this->dashboardService->getMonthlyPurchaseTrend(),
]); ]);

View File

@ -167,10 +167,13 @@ public function getTopProducts(): array
->toArray(); ->toArray();
} }
public function getPurchaseSummary(): array public function getPurchaseSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{ {
$purchaseSummary = Purchase::query() $query = Purchase::query();
->selectRaw('COUNT(*) as total_purchases, SUM(total) as total_spent, SUM(discount) as total_discount') if ($startDate && $endDate) {
$query->whereBetween('created_at', [$startDate, $endDate]);
}
$purchaseSummary = $query->selectRaw('COUNT(*) as total_purchases, SUM(total) as total_spent, SUM(discount) as total_discount')
->first(); ->first();
return [ return [
@ -269,17 +272,20 @@ public function getOrderStats(): array
]; ];
} }
public function getRevenueSummary(): array public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{ {
$revenueSummary = Order::query() $query = Order::query()->completed();
->completed() if ($startDate && $endDate) {
->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, SUM(shipping_cost) as total_shipping, COUNT(*) as total_orders, AVG(total_amount) as avg_order') $query->whereBetween('created_at', [$startDate, $endDate]);
}
$revenueSummary = $query->selectRaw('SUM(total_amount) as total_revenue, SUM(discount) as total_discount, SUM(shipping_cost) as total_shipping, COUNT(*) as total_orders, AVG(total_amount) as avg_order')
->first(); ->first();
$totalMarketplaceFees = Order::query() $feesQuery = Order::query()->completed()->whereNotNull('marketplace_settings_snapshot');
->completed() if ($startDate && $endDate) {
->whereNotNull('marketplace_settings_snapshot') $feesQuery->whereBetween('created_at', [$startDate, $endDate]);
->get() }
$totalMarketplaceFees = $feesQuery->get()
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0)); ->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
return [ return [
@ -326,11 +332,13 @@ public function getCashAccounts(): array
->toArray(); ->toArray();
} }
public function getCashSummary(Carbon $startOfMonth, Carbon $endOfMonth): array public function getCashSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{ {
$summary = CashTransaction::query() $query = CashTransaction::query();
->whereBetween('created_at', [$startOfMonth, $endOfMonth]) if ($startDate && $endDate) {
->selectRaw(" $query->whereBetween('created_at', [$startDate, $endDate]);
}
$summary = $query->selectRaw("
COUNT(*) as total_transactions, COUNT(*) as total_transactions,
COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as total_deposit, COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as total_deposit,
COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as total_withdrawal COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as total_withdrawal
@ -378,11 +386,13 @@ public function getMonthlyCashFlow(): array
return $result->toArray(); return $result->toArray();
} }
public function getMonthlyExpenses(Carbon $startOfMonth, Carbon $endOfMonth): array public function getMonthlyExpenses(?Carbon $startDate = null, ?Carbon $endDate = null): array
{ {
$monthlyExpenses = Expense::query() $query = Expense::query();
->whereBetween('created_at', [$startOfMonth, $endOfMonth]) if ($startDate && $endDate) {
->selectRaw('COALESCE(SUM(amount), 0) as total, COUNT(*) as count') $query->whereBetween('created_at', [$startDate, $endDate]);
}
$monthlyExpenses = $query->selectRaw('COALESCE(SUM(amount), 0) as total, COUNT(*) as count')
->first(); ->first();
return [ return [
@ -391,26 +401,24 @@ public function getMonthlyExpenses(Carbon $startOfMonth, Carbon $endOfMonth): ar
]; ];
} }
public function getKasbonSummary(): array public function getKasbonSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{ {
$pending = EmployeeAdvance::query() $pendingQuery = EmployeeAdvance::query()->pending();
->pending() $approvedQuery = EmployeeAdvance::query()->approved();
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total') $paidQuery = EmployeeAdvance::query()->paid();
->first(); $employeeQuery = EmployeeAdvance::query();
$approved = EmployeeAdvance::query() if ($startDate && $endDate) {
->approved() $pendingQuery->whereBetween('created_at', [$startDate, $endDate]);
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total') $approvedQuery->whereBetween('created_at', [$startDate, $endDate]);
->first(); $paidQuery->whereBetween('created_at', [$startDate, $endDate]);
$employeeQuery->whereBetween('created_at', [$startDate, $endDate]);
}
$paid = EmployeeAdvance::query() $pending = $pendingQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first();
->paid() $approved = $approvedQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first();
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total') $paid = $paidQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first();
->first(); $totalEmployees = $employeeQuery->distinct('employee_id')->count('employee_id');
$totalEmployees = EmployeeAdvance::query()
->distinct('employee_id')
->count('employee_id');
return [ return [
'pending' => [ 'pending' => [
@ -473,14 +481,16 @@ public function getPayrollSummary(Carbon $startOfMonth, Carbon $endOfMonth): arr
]; ];
} }
public function getLeaveRequestSummary(Carbon $startOfMonth, Carbon $endOfMonth): array public function getLeaveRequestSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{ {
$leaveRequestSummary = LeaveRequest::query() $query = LeaveRequest::query();
->where(function ($query) use ($startOfMonth, $endOfMonth) { if ($startDate && $endDate) {
$query->whereBetween('start_date', [$startOfMonth, $endOfMonth]) $query->where(function ($query) use ($startDate, $endDate) {
->orWhereBetween('end_date', [$startOfMonth, $endOfMonth]); $query->whereBetween('start_date', [$startDate, $endDate])
}) ->orWhereBetween('end_date', [$startDate, $endDate]);
->selectRaw(" });
}
$leaveRequestSummary = $query->selectRaw("
COUNT(*) as total, COUNT(*) as total,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved_count, SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved_count,
@ -516,35 +526,58 @@ public function getEmployeeSummary(): array
]; ];
} }
public function getAttendanceToday(): array public function getAttendanceToday(?Carbon $startDate = null, ?Carbon $endDate = null): array
{ {
$today = Carbon::today();
$totalEmployees = Employee::query()->count(); $totalEmployees = Employee::query()->count();
$present = Attendance::query() if ($startDate && $endDate) {
->where('attendance_date', $today) $present = Attendance::query()
->distinct('employee_id') ->whereBetween('attendance_date', [$startDate, $endDate])
->count('employee_id'); ->count();
// Karyawan yang sedang cuti hari ini (approved & aktif hari ini) $onLeave = LeaveRequest::query()
$onLeave = LeaveRequest::query() ->approved()
->approved() ->where(function ($query) use ($startDate, $endDate) {
->where('start_date', '<=', $today) $query->whereBetween('start_date', [$startDate, $endDate])
->where('end_date', '>=', $today) ->orWhereBetween('end_date', [$startDate, $endDate]);
->distinct('employee_id') })
->count('employee_id'); ->count();
// Absent = tidak hadir & tidak sedang cuti $days = (int) $startDate->copy()->startOfDay()->diffInDays($endDate->copy()->startOfDay()) + 1;
$notPresent = max(0, $totalEmployees - $present); $totalPossible = $totalEmployees * $days;
$absent = max(0, $notPresent - $onLeave); $absent = max(0, $totalPossible - $present - $onLeave);
return [ return [
'total_employees' => $totalEmployees, 'total_employees' => (int) $totalPossible,
'present' => $present, 'present' => (int) $present,
'absent' => $absent, 'absent' => (int) $absent,
'on_leave' => $onLeave, 'on_leave' => (int) $onLeave,
]; ];
} else {
$today = Carbon::today();
$present = Attendance::query()
->where('attendance_date', $today)
->distinct('employee_id')
->count('employee_id');
$onLeave = LeaveRequest::query()
->approved()
->where('start_date', '<=', $today)
->where('end_date', '>=', $today)
->distinct('employee_id')
->count('employee_id');
$notPresent = max(0, $totalEmployees - $present);
$absent = max(0, $notPresent - $onLeave);
return [
'total_employees' => $totalEmployees,
'present' => $present,
'absent' => $absent,
'on_leave' => $onLeave,
];
}
} }
public function getMonthlyRevenueTrend(): array public function getMonthlyRevenueTrend(): array

View File

@ -1,4 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import LowStockCard from '@/components/card/LowStockCard.vue';
import StatCard from '@/components/card/StatCard.vue';
import { DatePicker } from '@/components/ui/date-picker';
import { Button } from '@/components/ui/button';
import { import {
Card, Card,
CardContent, CardContent,
@ -7,61 +11,134 @@ import {
CardDescription, CardDescription,
} from '@/components/ui/card'; } from '@/components/ui/card';
import AdminLayout from '@/layouts/AdminLayout.vue'; import AdminLayout from '@/layouts/AdminLayout.vue';
import { Head } from '@inertiajs/vue3'; import { Head, router } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import admin from '@/routes/admin'; import admin from '@/routes/admin';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import type { DataTableFilterDef } from '@/types/data-table';
import { import {
useDataTableQuery, AlertTriangle,
useDataTableQuerySync, Banknote,
} from '@/composables/useDataTableQuery'; Clock,
import { Presentation } from '@lucide/vue'; CreditCard,
FileText,
Package,
ShoppingCart,
TrendingDown,
TrendingUp,
UserCheck,
Calendar,
} from '@lucide/vue';
const props = defineProps<{ const props = defineProps<{
filters: { filters: {
search: string; start_date?: string;
status?: string; end_date?: string;
};
rawMaterialStock: {
total_stock: number;
total_value: number;
by_unit: Record<string, number>;
};
productStock: {
total_stock: number;
total_reject: number;
total_value: number;
total_variants: number;
total_products: number;
total_categories: number;
};
lowStockProducts: Array<{
name: string;
stock: number;
}>;
lowStockMaterials: Array<{
name: string;
stock: number;
unit: string;
}>;
purchaseSummary: {
total_purchases: number;
total_spent: number;
total_discount: number;
};
revenueSummary: {
total_revenue: number;
total_discount: number;
total_marketplace_fees: number;
total_potongan: number;
total_shipping: number;
total_orders: number;
avg_order: number;
};
cashAccounts: Array<{
name: string;
balance: number;
}>;
cashSummary: {
total_transactions: number;
total_deposit: number;
total_withdrawal: number;
};
monthlyExpenses: {
total: number;
count: number;
};
kasbonSummary: {
pending: { count: number; total: number };
approved: { count: number; total: number };
paid: { count: number; total: number };
total: number;
total_employees: number;
};
leaveRequestSummary: {
total: number;
pending: number;
approved: number;
rejected: number;
};
attendanceToday: {
total_employees: number;
present: number;
absent: number;
on_leave: number;
}; };
}>(); }>();
const search = ref(props.filters.search ?? ''); const startDate = ref(props.filters.start_date ?? '');
const endDate = ref(props.filters.end_date ?? '');
const { query, setSearch, setFilter, resetFilters, syncFromServer } = const hasActiveFilters = computed(() => !!startDate.value || !!endDate.value);
useDataTableQuery({
url: admin.analysis.url(),
initial: { ...props.filters },
filterKeys: ['status'],
});
useDataTableQuerySync(() => props.filters, syncFromServer); function formatRupiah(value: number): string {
return 'Rp ' + value.toLocaleString('id-ID');
}
const filterDefs = computed<DataTableFilterDef[]>(() => [ const totalCashBalance = computed(() => {
{ return props.cashAccounts.reduce((sum, acc) => sum + acc.balance, 0);
key: 'status',
label: 'Status',
type: 'select',
options: [
{ value: 'active', label: 'Aktif' },
{ value: 'inactive', label: 'Nonaktif' },
],
},
]);
const filterValues = computed(() => ({
status: query.value.status ?? '',
}));
watch(search, (value) => {
setSearch(value);
}); });
watch( function applyFilters() {
() => props.filters.search, router.get(
(value) => { admin.analysis.url({
search.value = value ?? ''; start_date: startDate.value,
}, end_date: endDate.value,
); }),
{},
{
preserveState: true,
preserveScroll: true,
},
);
}
function clearFilters() {
startDate.value = '';
endDate.value = '';
applyFilters();
}
watch([startDate, endDate], () => {
applyFilters();
});
</script> </script>
<template> <template>
@ -69,16 +146,297 @@ watch(
<AdminLayout> <AdminLayout>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<!-- Header --> <!-- Header & Filter -->
<div <div
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between" class="flex flex-col gap-4 rounded-lg border bg-card p-6 shadow-xs md:flex-row md:items-center md:justify-between"
> >
<div class="space-y-1"> <div class="space-y-1">
<h2 class="text-2xl font-bold tracking-tight">Analisa</h2> <h2 class="text-2xl font-bold tracking-tight">Analisa</h2>
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
Halaman analisa data dan performa toko. Halaman analisa data, performa toko, dan inventori.
</p> </p>
</div> </div>
<!-- Date Filters -->
<div class="flex flex-wrap items-center gap-3">
<div class="flex items-center gap-2">
<span
class="text-xs font-semibold tracking-wider text-muted-foreground uppercase"
>Mulai:</span
>
<DatePicker
v-model="startDate"
class="w-[170px]"
placeholder="Pilih tanggal"
/>
</div>
<div class="flex items-center gap-2">
<span
class="text-xs font-semibold tracking-wider text-muted-foreground uppercase"
>Sampai:</span
>
<DatePicker
v-model="endDate"
class="w-[170px]"
placeholder="Pilih tanggal"
/>
</div>
<Button
v-if="hasActiveFilters"
variant="ghost"
size="sm"
@click="clearFilters"
class="h-9 px-3"
>
Reset Filter
</Button>
</div>
</div>
<!-- Row 1: KPI Statistic Cards -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- 1. Total Pendapatan -->
<StatCard
title="Total Pendapatan"
:icon="TrendingUp"
main-label="Total"
:main-value="formatRupiah(revenueSummary.total_revenue)"
:sub-label="
revenueSummary.total_orders +
' transaksi selesai' +
(hasActiveFilters ? ' (filter)' : ' (keseluruhan)')
"
:items="[
{
label: 'Bersih',
value: formatRupiah(
revenueSummary.total_revenue -
revenueSummary.total_potongan,
),
},
{
label: 'Potongan',
value: formatRupiah(revenueSummary.total_potongan),
},
{
label: 'Diskon',
value: formatRupiah(revenueSummary.total_discount),
},
]"
/>
<!-- 2. Total Pengeluaran -->
<StatCard
title="Total Pengeluaran"
:icon="TrendingDown"
main-label="Total"
:main-value="
formatRupiah(
purchaseSummary.total_spent +
monthlyExpenses.total +
kasbonSummary.total,
)
"
:items="[
{
label: 'Belanja',
value: formatRupiah(purchaseSummary.total_spent),
},
{
label: 'Pengeluaran',
value: formatRupiah(monthlyExpenses.total),
},
{
label: 'Kasbon',
value: formatRupiah(kasbonSummary.total),
},
]"
/>
<!-- 3. Kas Toko -->
<StatCard
title="Kas Toko"
:icon="Banknote"
main-label="Total Saldo"
:main-value="formatRupiah(totalCashBalance)"
:sub-label="
cashSummary.total_transactions +
' transaksi' +
(hasActiveFilters ? ' (filter)' : ' (keseluruhan)')
"
:items="[
{
label: 'Deposit',
value: formatRupiah(cashSummary.total_deposit),
},
{
label: 'Withdrawal',
value: formatRupiah(cashSummary.total_withdrawal),
},
]"
:cols="2"
/>
<!-- 4. Kehadiran -->
<StatCard
title="Kehadiran"
:icon="UserCheck"
main-label="Slot Kehadiran"
:main-value="attendanceToday.total_employees"
:sub-label="
(attendanceToday.total_employees > 0
? Math.round(
(attendanceToday.present /
attendanceToday.total_employees) *
100,
)
: 0) + '% hadir'
"
:items="[
{
label: 'Hadir',
value: attendanceToday.present,
},
{
label: 'Tidak Hadir',
value: attendanceToday.absent,
},
{
label: 'Cuti',
value: attendanceToday.on_leave ?? 0,
},
]"
/>
<!-- 5. Bahan Baku -->
<StatCard
title="Bahan Baku"
:icon="Package"
main-label="Total Stok"
:main-value="
rawMaterialStock.total_stock.toLocaleString('id-ID')
"
:sub-label="formatRupiah(rawMaterialStock.total_value)"
:items="[
{
label: 'Yard',
value: (
rawMaterialStock.by_unit?.yard ?? 0
).toLocaleString('id-ID'),
},
{
label: 'Meter',
value: (
rawMaterialStock.by_unit?.meter ?? 0
).toLocaleString('id-ID'),
},
{
label: 'Kg',
value: (
rawMaterialStock.by_unit?.kilogram ?? 0
).toLocaleString('id-ID'),
},
]"
/>
<!-- 6. Stok Produk -->
<StatCard
title="Stok Produk"
:icon="ShoppingCart"
main-label="Total Stok"
:main-value="
productStock.total_stock.toLocaleString('id-ID')
"
:sub-label="
formatRupiah(productStock.total_value) +
(productStock.total_reject > 0
? ` (${productStock.total_reject} reject)`
: '')
"
:items="[
{
label: 'Produk',
value: productStock.total_products.toLocaleString(
'id-ID',
),
},
{
label: 'Varian',
value: productStock.total_variants.toLocaleString(
'id-ID',
),
},
{
label: 'Kategori',
value: productStock.total_categories.toLocaleString(
'id-ID',
),
},
]"
/>
<!-- 7. Kasbon Karyawan -->
<StatCard
title="Kasbon Karyawan"
:icon="CreditCard"
main-label="Total Kasbon"
:main-value="formatRupiah(kasbonSummary.total)"
:sub-label="kasbonSummary.total_employees + ' karyawan'"
:items="[
{
label: 'Menunggu',
value: formatRupiah(kasbonSummary.pending.total),
},
{
label: 'Disetujui',
value: formatRupiah(kasbonSummary.approved.total),
},
{
label: 'Dibayar',
value: formatRupiah(kasbonSummary.paid.total),
},
]"
/>
<!-- 8. Pengajuan Cuti -->
<StatCard
title="Pengajuan Cuti"
:icon="FileText"
main-label="Total Pengajuan"
:main-value="leaveRequestSummary.total"
:items="[
{
label: 'Menunggu',
value: leaveRequestSummary.pending,
},
{
label: 'Disetujui',
value: leaveRequestSummary.approved,
},
{
label: 'Ditolak',
value: leaveRequestSummary.rejected,
},
]"
/>
</div>
<!-- Row 2: Low Stock Alerts -->
<div class="grid gap-4 md:grid-cols-2">
<!-- Stok Produk Menipis -->
<LowStockCard
title="Stok Produk Menipis"
:icon="AlertTriangle"
:items="lowStockProducts"
empty-text="Semua stok produk aman"
/>
<!-- Stok Bahan Baku Menipis -->
<LowStockCard
title="Stok Bahan Baku Menipis"
:icon="AlertTriangle"
:items="lowStockMaterials"
empty-text="Semua stok bahan baku aman"
/>
</div> </div>
</div> </div>
</AdminLayout> </AdminLayout>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import LowStockCard from '@/components/card/LowStockCard.vue';
import StatCard from '@/components/card/StatCard.vue'; import StatCard from '@/components/card/StatCard.vue';
import { import {
Card, Card,
@ -20,14 +19,9 @@ import {
import AdminLayout from '@/layouts/AdminLayout.vue'; import AdminLayout from '@/layouts/AdminLayout.vue';
import { Head } from '@inertiajs/vue3'; import { Head } from '@inertiajs/vue3';
import { import {
AlertTriangle,
Banknote, Banknote,
Clock, Clock,
CreditCard,
FileText,
Moon, Moon,
Package,
ShoppingCart,
Sun, Sun,
Sunrise, Sunrise,
TrendingDown, TrendingDown,
@ -38,28 +32,6 @@ import { VisAxis, VisDonut, VisGroupedBar, VisXYContainer } from '@unovis/vue';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
interface DashboardProps { interface DashboardProps {
rawMaterialStock: {
total_stock: number;
total_value: number;
by_unit: Record<string, number>;
};
productStock: {
total_stock: number;
total_reject: number;
total_value: number;
total_variants: number;
total_products: number;
total_categories: number;
};
lowStockProducts: Array<{
name: string;
stock: number;
}>;
lowStockMaterials: Array<{
name: string;
stock: number;
unit: string;
}>;
topSuppliers: Array<{ topSuppliers: Array<{
name: string; name: string;
total_amount: number; total_amount: number;
@ -165,12 +137,6 @@ interface DashboardProps {
paid_count: number; paid_count: number;
unpaid_count: number; unpaid_count: number;
}; };
leaveRequestSummary: {
total: number;
pending: number;
approved: number;
rejected: number;
};
employeeSummary: { employeeSummary: {
total: number; total: number;
by_status: Array<{ by_status: Array<{
@ -419,73 +385,6 @@ const totalCashBalance = computed(() => {
<!-- Row 1: Stock & Finance Summary --> <!-- Row 1: Stock & Finance Summary -->
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<!-- 1. Raw Material Stock -->
<StatCard
title="Bahan Baku"
:icon="Package"
main-label="Total Stok"
:main-value="
rawMaterialStock.total_stock.toLocaleString('id-ID')
"
:sub-label="formatRupiah(rawMaterialStock.total_value)"
:items="[
{
label: 'Yard',
value: (
rawMaterialStock.by_unit?.yard ?? 0
).toLocaleString('id-ID'),
},
{
label: 'Meter',
value: (
rawMaterialStock.by_unit?.meter ?? 0
).toLocaleString('id-ID'),
},
{
label: 'Kg',
value: (
rawMaterialStock.by_unit?.kilogram ?? 0
).toLocaleString('id-ID'),
},
]"
/>
<!-- 2. Product Stock -->
<StatCard
title="Stok Produk"
:icon="ShoppingCart"
main-label="Total Stok"
:main-value="
productStock.total_stock.toLocaleString('id-ID')
"
:sub-label="
formatRupiah(productStock.total_value) +
(productStock.total_reject > 0
? ` (${productStock.total_reject} reject)`
: '')
"
:items="[
{
label: 'Produk',
value: productStock.total_products.toLocaleString(
'id-ID',
),
},
{
label: 'Varian',
value: productStock.total_variants.toLocaleString(
'id-ID',
),
},
{
label: 'Kategori',
value: productStock.total_categories.toLocaleString(
'id-ID',
),
},
]"
/>
<!-- Attendance Today --> <!-- Attendance Today -->
<StatCard <StatCard
title="Kehadiran" title="Kehadiran"
@ -524,7 +423,7 @@ const totalCashBalance = computed(() => {
main-label="Total Saldo" main-label="Total Saldo"
:main-value="formatRupiah(totalCashBalance)" :main-value="formatRupiah(totalCashBalance)"
:sub-label=" :sub-label="
cashSummary.total_transactions + ' transaksi bulan ini' cashSummary.total_transactions + ' transaksi hari ini'
" "
:items="[ :items="[
{ {
@ -539,52 +438,6 @@ const totalCashBalance = computed(() => {
:cols="2" :cols="2"
/> />
<!-- Kasbon Summary -->
<StatCard
title="Kasbon Karyawan"
:icon="CreditCard"
main-label="Total Keseluruhan"
:main-value="formatRupiah(kasbonSummary.total)"
:sub-label="kasbonSummary.total_employees + ' karyawan'"
:items="[
{
label: 'Menunggu',
value: formatRupiah(kasbonSummary.pending.total),
},
{
label: 'Disetujui',
value: formatRupiah(kasbonSummary.approved.total),
},
{
label: 'Dibayar',
value: formatRupiah(kasbonSummary.paid.total),
},
]"
/>
<!-- Leave Request Summary -->
<StatCard
title="Pengajuan Cuti Bulan Ini"
:icon="FileText"
main-label="Total Pengajuan"
:main-value="leaveRequestSummary.total"
:items="[
{
label: 'Menunggu',
value: leaveRequestSummary.pending,
},
{
label: 'Disetujui',
value: leaveRequestSummary.approved,
},
{
label: 'Ditolak',
value: leaveRequestSummary.rejected,
},
]"
/>
<!-- Total Pengeluaran -->
<StatCard <StatCard
title="Total Pengeluaran" title="Total Pengeluaran"
:icon="TrendingDown" :icon="TrendingDown"
@ -641,25 +494,6 @@ const totalCashBalance = computed(() => {
/> />
</div> </div>
<!-- Row 2: Low Stock Alerts -->
<div class="grid gap-4 md:grid-cols-2">
<!-- Low Stock Products -->
<LowStockCard
title="Stok Produk Menipis"
:icon="AlertTriangle"
:items="lowStockProducts"
empty-text="Semua stok produk aman"
/>
<!-- Low Stock Materials -->
<LowStockCard
title="Stok Bahan Baku Menipis"
:icon="AlertTriangle"
:items="lowStockMaterials"
empty-text="Semua stok bahan baku aman"
/>
</div>
<!-- Row 3: Top 5 Charts --> <!-- Row 3: Top 5 Charts -->
<div class="grid gap-4 md:grid-cols-3"> <div class="grid gap-4 md:grid-cols-3">
<!-- Top 5 Suppliers Chart --> <!-- Top 5 Suppliers Chart -->
@ -761,7 +595,9 @@ const totalCashBalance = computed(() => {
:domain-line="false" :domain-line="false"
:grid-line="false" :grid-line="false"
:num-ticks="productBarData.length" :num-ticks="productBarData.length"
:tick-values="productBarData.map(d => d.name)" :tick-values="
productBarData.map((d) => d.name)
"
/> />
<VisAxis <VisAxis
type="y" type="y"
@ -790,8 +626,11 @@ const totalCashBalance = computed(() => {
</div> </div>
</CardContent> </CardContent>
<CardFooter class="flex-col items-start gap-2 text-sm"> <CardFooter class="flex-col items-start gap-2 text-sm">
<div class="flex gap-2 font-medium leading-none text-muted-foreground"> <div
Menampilkan 5 produk dengan penjualan tertinggi <TrendingUp class="h-4 w-4 text-green-500" /> class="flex gap-2 leading-none font-medium text-muted-foreground"
>
Menampilkan 5 produk dengan penjualan tertinggi
<TrendingUp class="h-4 w-4 text-green-500" />
</div> </div>
</CardFooter> </CardFooter>
</Card> </Card>