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;
use App\Http\Controllers\Controller;
use App\Services\System\DashboardService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class AnalysisController extends Controller
{
public function __construct(
private readonly DashboardService $dashboardService,
) {}
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', [
'filters' => [
'search' => $request->query('search', ''),
'status' => $request->query('status', ''),
'start_date' => $request->query('start_date', ''),
'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();
$endOfMonth = $now->copy()->endOfMonth();
$startOfDay = Carbon::today()->startOfDay();
$endOfDay = Carbon::today()->endOfDay();
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(),
'topCustomers' => $this->dashboardService->getTopCustomers(),
'topProducts' => $this->dashboardService->getTopProducts(),
'purchaseSummary' => $this->dashboardService->getPurchaseSummary(),
'purchaseSummary' => $this->dashboardService->getPurchaseSummary($startOfDay, $endOfDay),
'cuttingSummary' => $this->dashboardService->getCuttingSummary(),
'cuttingByStatus' => $this->dashboardService->getCuttingByStatus(),
'orderStats' => $this->dashboardService->getOrderStats(),
'revenueSummary' => $this->dashboardService->getRevenueSummary(),
'revenueSummary' => $this->dashboardService->getRevenueSummary($startOfDay, $endOfDay),
'marketplaceSummary' => $this->dashboardService->getMarketplaceSummary(),
'cashAccounts' => $this->dashboardService->getCashAccounts(),
'cashSummary' => $this->dashboardService->getCashSummary($startOfMonth, $endOfMonth),
'cashSummary' => $this->dashboardService->getCashSummary($startOfDay, $endOfDay),
'monthlyCashFlow' => $this->dashboardService->getMonthlyCashFlow(),
'monthlyExpenses' => $this->dashboardService->getMonthlyExpenses($startOfMonth, $endOfMonth),
'kasbonSummary' => $this->dashboardService->getKasbonSummary(),
'monthlyExpenses' => $this->dashboardService->getMonthlyExpenses($startOfDay, $endOfDay),
'kasbonSummary' => $this->dashboardService->getKasbonSummary($startOfDay, $endOfDay),
'payrollSummary' => $this->dashboardService->getPayrollSummary($startOfMonth, $endOfMonth),
'leaveRequestSummary' => $this->dashboardService->getLeaveRequestSummary($startOfMonth, $endOfMonth),
'employeeSummary' => $this->dashboardService->getEmployeeSummary(),
'attendanceToday' => $this->dashboardService->getAttendanceToday(),
'attendanceToday' => $this->dashboardService->getAttendanceToday($startOfDay, $endOfDay),
'monthlyRevenueTrend' => $this->dashboardService->getMonthlyRevenueTrend(),
'monthlyPurchaseTrend' => $this->dashboardService->getMonthlyPurchaseTrend(),
]);

View File

@ -167,10 +167,13 @@ public function getTopProducts(): array
->toArray();
}
public function getPurchaseSummary(): array
public function getPurchaseSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
$purchaseSummary = Purchase::query()
->selectRaw('COUNT(*) as total_purchases, SUM(total) as total_spent, SUM(discount) as total_discount')
$query = Purchase::query();
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();
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()
->completed()
->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 = Order::query()->completed();
if ($startDate && $endDate) {
$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();
$totalMarketplaceFees = Order::query()
->completed()
->whereNotNull('marketplace_settings_snapshot')
->get()
$feesQuery = Order::query()->completed()->whereNotNull('marketplace_settings_snapshot');
if ($startDate && $endDate) {
$feesQuery->whereBetween('created_at', [$startDate, $endDate]);
}
$totalMarketplaceFees = $feesQuery->get()
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
return [
@ -326,11 +332,13 @@ public function getCashAccounts(): array
->toArray();
}
public function getCashSummary(Carbon $startOfMonth, Carbon $endOfMonth): array
public function getCashSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
$summary = CashTransaction::query()
->whereBetween('created_at', [$startOfMonth, $endOfMonth])
->selectRaw("
$query = CashTransaction::query();
if ($startDate && $endDate) {
$query->whereBetween('created_at', [$startDate, $endDate]);
}
$summary = $query->selectRaw("
COUNT(*) as total_transactions,
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
@ -378,11 +386,13 @@ public function getMonthlyCashFlow(): array
return $result->toArray();
}
public function getMonthlyExpenses(Carbon $startOfMonth, Carbon $endOfMonth): array
public function getMonthlyExpenses(?Carbon $startDate = null, ?Carbon $endDate = null): array
{
$monthlyExpenses = Expense::query()
->whereBetween('created_at', [$startOfMonth, $endOfMonth])
->selectRaw('COALESCE(SUM(amount), 0) as total, COUNT(*) as count')
$query = Expense::query();
if ($startDate && $endDate) {
$query->whereBetween('created_at', [$startDate, $endDate]);
}
$monthlyExpenses = $query->selectRaw('COALESCE(SUM(amount), 0) as total, COUNT(*) as count')
->first();
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()
->pending()
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
->first();
$pendingQuery = EmployeeAdvance::query()->pending();
$approvedQuery = EmployeeAdvance::query()->approved();
$paidQuery = EmployeeAdvance::query()->paid();
$employeeQuery = EmployeeAdvance::query();
$approved = EmployeeAdvance::query()
->approved()
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
->first();
if ($startDate && $endDate) {
$pendingQuery->whereBetween('created_at', [$startDate, $endDate]);
$approvedQuery->whereBetween('created_at', [$startDate, $endDate]);
$paidQuery->whereBetween('created_at', [$startDate, $endDate]);
$employeeQuery->whereBetween('created_at', [$startDate, $endDate]);
}
$paid = EmployeeAdvance::query()
->paid()
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
->first();
$totalEmployees = EmployeeAdvance::query()
->distinct('employee_id')
->count('employee_id');
$pending = $pendingQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first();
$approved = $approvedQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first();
$paid = $paidQuery->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')->first();
$totalEmployees = $employeeQuery->distinct('employee_id')->count('employee_id');
return [
'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()
->where(function ($query) use ($startOfMonth, $endOfMonth) {
$query->whereBetween('start_date', [$startOfMonth, $endOfMonth])
->orWhereBetween('end_date', [$startOfMonth, $endOfMonth]);
})
->selectRaw("
$query = LeaveRequest::query();
if ($startDate && $endDate) {
$query->where(function ($query) use ($startDate, $endDate) {
$query->whereBetween('start_date', [$startDate, $endDate])
->orWhereBetween('end_date', [$startDate, $endDate]);
});
}
$leaveRequestSummary = $query->selectRaw("
COUNT(*) as total,
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,
@ -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();
$present = Attendance::query()
->where('attendance_date', $today)
->distinct('employee_id')
->count('employee_id');
if ($startDate && $endDate) {
$present = Attendance::query()
->whereBetween('attendance_date', [$startDate, $endDate])
->count();
// Karyawan yang sedang cuti hari ini (approved & aktif hari ini)
$onLeave = LeaveRequest::query()
->approved()
->where('start_date', '<=', $today)
->where('end_date', '>=', $today)
->distinct('employee_id')
->count('employee_id');
$onLeave = LeaveRequest::query()
->approved()
->where(function ($query) use ($startDate, $endDate) {
$query->whereBetween('start_date', [$startDate, $endDate])
->orWhereBetween('end_date', [$startDate, $endDate]);
})
->count();
// Absent = tidak hadir & tidak sedang cuti
$notPresent = max(0, $totalEmployees - $present);
$absent = max(0, $notPresent - $onLeave);
$days = (int) $startDate->copy()->startOfDay()->diffInDays($endDate->copy()->startOfDay()) + 1;
$totalPossible = $totalEmployees * $days;
$absent = max(0, $totalPossible - $present - $onLeave);
return [
'total_employees' => $totalEmployees,
'present' => $present,
'absent' => $absent,
'on_leave' => $onLeave,
];
return [
'total_employees' => (int) $totalPossible,
'present' => (int) $present,
'absent' => (int) $absent,
'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

View File

@ -1,4 +1,8 @@
<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 {
Card,
CardContent,
@ -7,61 +11,134 @@ import {
CardDescription,
} from '@/components/ui/card';
import AdminLayout from '@/layouts/AdminLayout.vue';
import { Head } from '@inertiajs/vue3';
import { Head, router } from '@inertiajs/vue3';
import { computed, ref, watch } from 'vue';
import admin from '@/routes/admin';
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
import type { DataTableFilterDef } from '@/types/data-table';
import {
useDataTableQuery,
useDataTableQuerySync,
} from '@/composables/useDataTableQuery';
import { Presentation } from '@lucide/vue';
AlertTriangle,
Banknote,
Clock,
CreditCard,
FileText,
Package,
ShoppingCart,
TrendingDown,
TrendingUp,
UserCheck,
Calendar,
} from '@lucide/vue';
const props = defineProps<{
filters: {
search: string;
status?: string;
start_date?: 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 } =
useDataTableQuery({
url: admin.analysis.url(),
initial: { ...props.filters },
filterKeys: ['status'],
});
const hasActiveFilters = computed(() => !!startDate.value || !!endDate.value);
useDataTableQuerySync(() => props.filters, syncFromServer);
function formatRupiah(value: number): string {
return 'Rp ' + value.toLocaleString('id-ID');
}
const filterDefs = computed<DataTableFilterDef[]>(() => [
{
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);
const totalCashBalance = computed(() => {
return props.cashAccounts.reduce((sum, acc) => sum + acc.balance, 0);
});
watch(
() => props.filters.search,
(value) => {
search.value = value ?? '';
},
);
function applyFilters() {
router.get(
admin.analysis.url({
start_date: startDate.value,
end_date: endDate.value,
}),
{},
{
preserveState: true,
preserveScroll: true,
},
);
}
function clearFilters() {
startDate.value = '';
endDate.value = '';
applyFilters();
}
watch([startDate, endDate], () => {
applyFilters();
});
</script>
<template>
@ -69,16 +146,297 @@ watch(
<AdminLayout>
<div class="flex flex-col gap-6">
<!-- Header -->
<!-- Header & Filter -->
<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">
<h2 class="text-2xl font-bold tracking-tight">Analisa</h2>
<p class="text-sm text-muted-foreground">
Halaman analisa data dan performa toko.
Halaman analisa data, performa toko, dan inventori.
</p>
</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>
</AdminLayout>

View File

@ -1,5 +1,4 @@
<script setup lang="ts">
import LowStockCard from '@/components/card/LowStockCard.vue';
import StatCard from '@/components/card/StatCard.vue';
import {
Card,
@ -20,14 +19,9 @@ import {
import AdminLayout from '@/layouts/AdminLayout.vue';
import { Head } from '@inertiajs/vue3';
import {
AlertTriangle,
Banknote,
Clock,
CreditCard,
FileText,
Moon,
Package,
ShoppingCart,
Sun,
Sunrise,
TrendingDown,
@ -38,28 +32,6 @@ import { VisAxis, VisDonut, VisGroupedBar, VisXYContainer } from '@unovis/vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
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<{
name: string;
total_amount: number;
@ -165,12 +137,6 @@ interface DashboardProps {
paid_count: number;
unpaid_count: number;
};
leaveRequestSummary: {
total: number;
pending: number;
approved: number;
rejected: number;
};
employeeSummary: {
total: number;
by_status: Array<{
@ -419,73 +385,6 @@ const totalCashBalance = computed(() => {
<!-- Row 1: Stock & Finance Summary -->
<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 -->
<StatCard
title="Kehadiran"
@ -524,7 +423,7 @@ const totalCashBalance = computed(() => {
main-label="Total Saldo"
:main-value="formatRupiah(totalCashBalance)"
:sub-label="
cashSummary.total_transactions + ' transaksi bulan ini'
cashSummary.total_transactions + ' transaksi hari ini'
"
:items="[
{
@ -539,52 +438,6 @@ const totalCashBalance = computed(() => {
: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
title="Total Pengeluaran"
:icon="TrendingDown"
@ -641,25 +494,6 @@ const totalCashBalance = computed(() => {
/>
</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 -->
<div class="grid gap-4 md:grid-cols-3">
<!-- Top 5 Suppliers Chart -->
@ -761,7 +595,9 @@ const totalCashBalance = computed(() => {
:domain-line="false"
:grid-line="false"
:num-ticks="productBarData.length"
:tick-values="productBarData.map(d => d.name)"
:tick-values="
productBarData.map((d) => d.name)
"
/>
<VisAxis
type="y"
@ -790,8 +626,11 @@ const totalCashBalance = computed(() => {
</div>
</CardContent>
<CardFooter class="flex-col items-start gap-2 text-sm">
<div class="flex gap-2 font-medium leading-none text-muted-foreground">
Menampilkan 5 produk dengan penjualan tertinggi <TrendingUp class="h-4 w-4 text-green-500" />
<div
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>
</CardFooter>
</Card>