feat: enhance dashboard metrics by adding attendance, cash overview, revenue summary, and expense summary
This commit is contained in:
parent
993a87cc66
commit
1dc274cce1
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Services\System\DashboardService;
|
use App\Services\System\DashboardService;
|
||||||
use Carbon\Carbon;
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
|
|
||||||
@ -16,33 +15,17 @@ public function __construct(
|
|||||||
|
|
||||||
public function index(): Response
|
public function index(): Response
|
||||||
{
|
{
|
||||||
$now = Carbon::now();
|
|
||||||
$startOfMonth = $now->copy()->startOfMonth();
|
|
||||||
$endOfMonth = $now->copy()->endOfMonth();
|
|
||||||
|
|
||||||
$startOfDay = Carbon::today()->startOfDay();
|
|
||||||
$endOfDay = Carbon::today()->endOfDay();
|
|
||||||
|
|
||||||
return Inertia::render('admin/Dashboard', [
|
return Inertia::render('admin/Dashboard', [
|
||||||
|
'attendance' => $this->dashboardService->getAttendance(),
|
||||||
|
'cashOverview' => $this->dashboardService->getCashOverview(),
|
||||||
|
'revenueSummary' => $this->dashboardService->getRevenueSummary(),
|
||||||
|
'expenseSummary' => $this->dashboardService->getExpenseSummary(),
|
||||||
|
|
||||||
'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($startOfDay, $endOfDay),
|
|
||||||
'cuttingSummary' => $this->dashboardService->getCuttingSummary(),
|
|
||||||
'cuttingByStatus' => $this->dashboardService->getCuttingByStatus(),
|
|
||||||
'orderStats' => $this->dashboardService->getOrderStats(),
|
'orderStats' => $this->dashboardService->getOrderStats(),
|
||||||
'revenueSummary' => $this->dashboardService->getRevenueSummary($startOfDay, $endOfDay),
|
|
||||||
'marketplaceSummary' => $this->dashboardService->getMarketplaceSummary(),
|
|
||||||
'cashAccounts' => $this->dashboardService->getCashAccounts(),
|
|
||||||
'cashSummary' => $this->dashboardService->getCashSummary($startOfDay, $endOfDay),
|
|
||||||
'monthlyCashFlow' => $this->dashboardService->getMonthlyCashFlow(),
|
|
||||||
'monthlyExpenses' => $this->dashboardService->getMonthlyExpenses($startOfDay, $endOfDay),
|
|
||||||
'kasbonSummary' => $this->dashboardService->getKasbonSummary($startOfDay, $endOfDay),
|
|
||||||
'payrollSummary' => $this->dashboardService->getPayrollSummary($startOfMonth, $endOfMonth),
|
|
||||||
'employeeSummary' => $this->dashboardService->getEmployeeSummary(),
|
|
||||||
'attendanceToday' => $this->dashboardService->getAttendanceToday($startOfDay, $endOfDay),
|
|
||||||
'monthlyRevenueTrend' => $this->dashboardService->getMonthlyRevenueTrend(),
|
|
||||||
'monthlyPurchaseTrend' => $this->dashboardService->getMonthlyPurchaseTrend(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,119 +2,133 @@
|
|||||||
|
|
||||||
namespace App\Services\System;
|
namespace App\Services\System;
|
||||||
|
|
||||||
use App\Enums\CuttingStatus;
|
|
||||||
use App\Enums\EmploymentStatus;
|
|
||||||
use App\Enums\OrderChannel;
|
use App\Enums\OrderChannel;
|
||||||
use App\Enums\OrderStatus;
|
use App\Enums\OrderStatus;
|
||||||
use App\Enums\PaymentType;
|
use App\Enums\PaymentType;
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
use App\Models\CashAccount;
|
use App\Models\CashAccount;
|
||||||
use App\Models\CashTransaction;
|
use App\Models\CashTransaction;
|
||||||
use App\Models\Category;
|
|
||||||
use App\Models\Cutting;
|
|
||||||
use App\Models\CuttingResult;
|
|
||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
use App\Models\Expense;
|
use App\Models\Expense;
|
||||||
use App\Models\LeaveRequest;
|
use App\Models\LeaveRequest;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
use App\Models\Payroll;
|
|
||||||
use App\Models\PayrollPeriod;
|
|
||||||
use App\Models\Product;
|
|
||||||
use App\Models\ProductVariant;
|
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
use App\Models\RawMaterialPrice;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class DashboardService
|
class DashboardService
|
||||||
{
|
{
|
||||||
public function getRawMaterialStock(): array
|
public function getAttendance(): array
|
||||||
{
|
{
|
||||||
$stockSummary = RawMaterialPrice::query()
|
$totalEmployees = Employee::query()->count();
|
||||||
->selectRaw('SUM(stock) as total_stock, SUM(stock * price) as total_value')
|
$today = Carbon::today();
|
||||||
->first();
|
|
||||||
|
|
||||||
$stockByUnit = RawMaterialPrice::query()
|
$present = Attendance::query()
|
||||||
->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id')
|
->where('attendance_date', $today)
|
||||||
->selectRaw('raw_materials.unit, SUM(raw_material_prices.stock) as total_stock')
|
->distinct('employee_id')
|
||||||
->groupBy('raw_materials.unit')
|
->count('employee_id');
|
||||||
->get()
|
|
||||||
->mapWithKeys(fn ($item) => [
|
$onLeave = LeaveRequest::query()
|
||||||
$item->unit => (float) $item->total_stock,
|
->approved()
|
||||||
])
|
->where('start_date', '<=', $today)
|
||||||
->toArray();
|
->where('end_date', '>=', $today)
|
||||||
|
->distinct('employee_id')
|
||||||
|
->count('employee_id');
|
||||||
|
|
||||||
|
$absent = max(0, $totalEmployees - $present - $onLeave);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'total_stock' => (float) ($stockSummary->total_stock ?? 0),
|
'total_employees' => $totalEmployees,
|
||||||
'total_value' => (int) ($stockSummary->total_value ?? 0),
|
'percentage' => $totalEmployees > 0 ? round($present / $totalEmployees * 100) : 0,
|
||||||
'by_unit' => $stockByUnit,
|
'present' => $present,
|
||||||
|
'absent' => $absent,
|
||||||
|
'on_leave' => $onLeave,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getProductStock(): array
|
public function getCashOverview(): array
|
||||||
{
|
{
|
||||||
$variantSummary = ProductVariant::query()
|
$totalBalance = CashAccount::query()->sum('balance');
|
||||||
->selectRaw('SUM(stock) as total_stock, SUM(reject_stock) as total_reject, COUNT(*) as total_variants')
|
|
||||||
|
$summary = CashTransaction::query()
|
||||||
|
->whereDate('created_at', Carbon::today())
|
||||||
|
->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
|
||||||
|
")
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
$totalStock = (int) ($variantSummary->total_stock ?? 0);
|
|
||||||
|
|
||||||
$totalValue = Cutting::query()
|
|
||||||
->whereNotNull('cost_per_unit')
|
|
||||||
->join('cutting_results', 'cuttings.id', '=', 'cutting_results.cutting_id')
|
|
||||||
->selectRaw('SUM(cutting_results.warehouse_stock * cuttings.cost_per_unit) as total_value')
|
|
||||||
->value('total_value');
|
|
||||||
|
|
||||||
$totalProducts = Product::query()->count();
|
|
||||||
$totalCategories = Category::query()->count();
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'total_stock' => $totalStock,
|
'total_balance' => (int) $totalBalance,
|
||||||
'total_reject' => (int) ($variantSummary->total_reject ?? 0),
|
'total_transactions' => (int) ($summary->total_transactions ?? 0),
|
||||||
'total_value' => (int) ($totalValue ?? 0),
|
'total_deposit' => (int) ($summary->total_deposit ?? 0),
|
||||||
'total_variants' => (int) ($variantSummary->total_variants ?? 0),
|
'total_withdrawal' => (int) ($summary->total_withdrawal ?? 0),
|
||||||
'total_products' => $totalProducts,
|
|
||||||
'total_categories' => $totalCategories,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLowStockProducts(): array
|
public function getRevenueSummary(): array
|
||||||
{
|
{
|
||||||
return ProductVariant::query()
|
$revenueSummary = Order::query()
|
||||||
->where('stock', '<=', ProductVariant::minStock())
|
->completed()
|
||||||
->join('products', 'product_variants.product_id', '=', 'products.id')
|
->whereDate('created_at', Carbon::today())
|
||||||
->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, product_variants.stock")
|
->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')
|
||||||
->orderBy('stock')
|
->first();
|
||||||
->limit(5)
|
|
||||||
|
$totalMarketplaceFees = Order::query()
|
||||||
|
->completed()
|
||||||
|
->whereNotNull('marketplace_settings_snapshot')
|
||||||
|
->whereDate('created_at', Carbon::today())
|
||||||
->get()
|
->get()
|
||||||
->map(fn ($item) => [
|
->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
|
||||||
'name' => $item->full_name,
|
|
||||||
'stock' => (int) $item->stock,
|
return [
|
||||||
])
|
'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0),
|
||||||
->toArray();
|
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
|
||||||
|
'total_marketplace_fees' => $totalMarketplaceFees,
|
||||||
|
'total_potongan' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees,
|
||||||
|
'total_shipping' => (int) ($revenueSummary->total_shipping ?? 0),
|
||||||
|
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
|
||||||
|
'avg_order' => (int) ($revenueSummary->avg_order ?? 0),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLowStockMaterials(): array
|
public function getExpenseSummary(): array
|
||||||
{
|
{
|
||||||
return RawMaterialPrice::query()
|
$today = Carbon::today();
|
||||||
->where('stock', '<=', 5)
|
|
||||||
->join('raw_materials', 'raw_material_prices.raw_material_id', '=', 'raw_materials.id')
|
$purchase = Purchase::query()
|
||||||
->selectRaw("CONCAT(raw_materials.name, ' - ', raw_material_prices.variant) as full_name, raw_material_prices.stock, raw_materials.unit")
|
->whereDate('created_at', $today)
|
||||||
->orderBy('stock')
|
->selectRaw('COALESCE(SUM(total), 0) as total')
|
||||||
->limit(5)
|
->first();
|
||||||
->get()
|
|
||||||
->map(fn ($item) => [
|
$expenses = Expense::query()
|
||||||
'name' => $item->full_name,
|
->whereDate('created_at', $today)
|
||||||
'stock' => (float) $item->stock,
|
->selectRaw('COALESCE(SUM(amount), 0) as total')
|
||||||
'unit' => $item->unit,
|
->first();
|
||||||
])
|
|
||||||
->toArray();
|
$employeeAdvance = EmployeeAdvance::query()
|
||||||
|
->whereDate('created_at', $today)
|
||||||
|
->selectRaw('COALESCE(SUM(amount), 0) as total')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$purchaseTotal = (int) ($purchase->total ?? 0);
|
||||||
|
$expenseTotal = (int) ($expenses->total ?? 0);
|
||||||
|
$advanceTotal = (int) ($employeeAdvance->total ?? 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => $purchaseTotal + $expenseTotal + $advanceTotal,
|
||||||
|
'purchase_total' => $purchaseTotal,
|
||||||
|
'expense_total' => $expenseTotal,
|
||||||
|
'advance_total' => $advanceTotal,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTopSuppliers(): array
|
public function getTopSuppliers(): array
|
||||||
{
|
{
|
||||||
return Purchase::query()
|
return Purchase::query()
|
||||||
|
->whereDate('purchases.created_at', Carbon::today())
|
||||||
->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id')
|
->join('suppliers', 'purchases.supplier_id', '=', 'suppliers.id')
|
||||||
->selectRaw('suppliers.name, SUM(purchases.total) as total_amount, COUNT(purchases.id) as purchase_count')
|
->selectRaw('suppliers.name, SUM(purchases.total) as total_amount, COUNT(purchases.id) as purchase_count')
|
||||||
->groupBy('suppliers.id', 'suppliers.name')
|
->groupBy('suppliers.id', 'suppliers.name')
|
||||||
@ -133,6 +147,7 @@ public function getTopCustomers(): array
|
|||||||
{
|
{
|
||||||
return Order::query()
|
return Order::query()
|
||||||
->completed()
|
->completed()
|
||||||
|
->whereDate('orders.created_at', Carbon::today())
|
||||||
->join('customers', 'orders.customer_id', '=', 'customers.id')
|
->join('customers', 'orders.customer_id', '=', 'customers.id')
|
||||||
->selectRaw('customers.name, SUM(orders.total_amount) as total_amount, COUNT(orders.id) as order_count')
|
->selectRaw('customers.name, SUM(orders.total_amount) as total_amount, COUNT(orders.id) as order_count')
|
||||||
->groupBy('customers.id', 'customers.name')
|
->groupBy('customers.id', 'customers.name')
|
||||||
@ -154,6 +169,7 @@ public function getTopProducts(): array
|
|||||||
->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id')
|
->join('product_variants', 'order_items.product_variant_id', '=', 'product_variants.id')
|
||||||
->join('products', 'product_variants.product_id', '=', 'products.id')
|
->join('products', 'product_variants.product_id', '=', 'products.id')
|
||||||
->where('orders.status', OrderStatus::COMPLETED)
|
->where('orders.status', OrderStatus::COMPLETED)
|
||||||
|
->whereDate('orders.created_at', Carbon::today())
|
||||||
->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, SUM(order_items.quantity) as total_qty, SUM(order_items.subtotal) as total_revenue")
|
->selectRaw("CONCAT(products.name, ' - ', product_variants.name) as full_name, SUM(order_items.quantity) as total_qty, SUM(order_items.subtotal) as total_revenue")
|
||||||
->groupBy('order_items.product_variant_id', 'products.name', 'product_variants.name')
|
->groupBy('order_items.product_variant_id', 'products.name', 'product_variants.name')
|
||||||
->orderByDesc('total_qty')
|
->orderByDesc('total_qty')
|
||||||
@ -167,57 +183,10 @@ public function getTopProducts(): array
|
|||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPurchaseSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
|
||||||
{
|
|
||||||
$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 [
|
|
||||||
'total_purchases' => (int) ($purchaseSummary->total_purchases ?? 0),
|
|
||||||
'total_spent' => (int) ($purchaseSummary->total_spent ?? 0),
|
|
||||||
'total_discount' => (int) ($purchaseSummary->total_discount ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCuttingSummary(): array
|
|
||||||
{
|
|
||||||
$totalCost = Cutting::query()
|
|
||||||
->selectRaw('COALESCE(SUM(total_material_cost), 0) + COALESCE(SUM(sewing_cost), 0) + COALESCE(SUM(other_cost), 0) as total_cost')
|
|
||||||
->value('total_cost');
|
|
||||||
|
|
||||||
$totalResults = CuttingResult::query()
|
|
||||||
->selectRaw('SUM(cutting_result) as total_cutting, SUM(warehouse_stock) as total_warehouse, SUM(cutting_reject) as total_reject')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total_cost' => (int) ($totalCost ?? 0),
|
|
||||||
'total_cutting' => (int) ($totalResults->total_cutting ?? 0),
|
|
||||||
'total_warehouse' => (int) ($totalResults->total_warehouse ?? 0),
|
|
||||||
'total_reject' => (int) ($totalResults->total_reject ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCuttingByStatus(): array
|
|
||||||
{
|
|
||||||
return Cutting::query()
|
|
||||||
->selectRaw('status, COUNT(*) as count')
|
|
||||||
->groupBy('status')
|
|
||||||
->get()
|
|
||||||
->map(fn ($item) => [
|
|
||||||
'status' => $item->status instanceof CuttingStatus ? $item->status->value : $item->status,
|
|
||||||
'label' => $item->status instanceof CuttingStatus ? $item->status->label() : CuttingStatus::from($item->status)->label(),
|
|
||||||
'count' => (int) $item->count,
|
|
||||||
])
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getOrderStats(): array
|
public function getOrderStats(): array
|
||||||
{
|
{
|
||||||
$byChannel = Order::query()
|
$byChannel = Order::query()
|
||||||
|
->whereDate('created_at', Carbon::today())
|
||||||
->selectRaw('channel, COUNT(*) as count, SUM(total_amount) as total')
|
->selectRaw('channel, COUNT(*) as count, SUM(total_amount) as total')
|
||||||
->groupBy('channel')
|
->groupBy('channel')
|
||||||
->get()
|
->get()
|
||||||
@ -229,6 +198,7 @@ public function getOrderStats(): array
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$byPaymentType = Order::query()
|
$byPaymentType = Order::query()
|
||||||
|
->whereDate('created_at', Carbon::today())
|
||||||
->selectRaw('payment_type, COUNT(*) as count, SUM(total_amount) as total')
|
->selectRaw('payment_type, COUNT(*) as count, SUM(total_amount) as total')
|
||||||
->groupBy('payment_type')
|
->groupBy('payment_type')
|
||||||
->get()
|
->get()
|
||||||
@ -241,6 +211,7 @@ public function getOrderStats(): array
|
|||||||
|
|
||||||
$byMarketing = Order::query()
|
$byMarketing = Order::query()
|
||||||
->whereNotNull('marketing_id')
|
->whereNotNull('marketing_id')
|
||||||
|
->whereDate('orders.created_at', Carbon::today())
|
||||||
->join('users', 'orders.marketing_id', '=', 'users.id')
|
->join('users', 'orders.marketing_id', '=', 'users.id')
|
||||||
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
||||||
->selectRaw('COALESCE(user_profiles.full_name, users.username) as name, COUNT(*) as count, SUM(orders.total_amount) as total')
|
->selectRaw('COALESCE(user_profiles.full_name, users.username) as name, COUNT(*) as count, SUM(orders.total_amount) as total')
|
||||||
@ -255,6 +226,7 @@ public function getOrderStats(): array
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$byStatus = Order::query()
|
$byStatus = Order::query()
|
||||||
|
->whereDate('created_at', Carbon::today())
|
||||||
->selectRaw('status, COUNT(*) as count')
|
->selectRaw('status, COUNT(*) as count')
|
||||||
->groupBy('status')
|
->groupBy('status')
|
||||||
->get()
|
->get()
|
||||||
@ -271,375 +243,4 @@ public function getOrderStats(): array
|
|||||||
'by_status' => $byStatus->toArray(),
|
'by_status' => $byStatus->toArray(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRevenueSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
|
||||||
{
|
|
||||||
$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();
|
|
||||||
|
|
||||||
$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 [
|
|
||||||
'total_revenue' => (int) ($revenueSummary->total_revenue ?? 0),
|
|
||||||
'total_discount' => (int) ($revenueSummary->total_discount ?? 0),
|
|
||||||
'total_marketplace_fees' => $totalMarketplaceFees,
|
|
||||||
'total_potongan' => (int) ($revenueSummary->total_discount ?? 0) + $totalMarketplaceFees,
|
|
||||||
'total_shipping' => (int) ($revenueSummary->total_shipping ?? 0),
|
|
||||||
'total_orders' => (int) ($revenueSummary->total_orders ?? 0),
|
|
||||||
'avg_order' => (int) ($revenueSummary->avg_order ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMarketplaceSummary(): array
|
|
||||||
{
|
|
||||||
$marketplaceOrders = Order::query()
|
|
||||||
->completed()
|
|
||||||
->whereIn('channel', [OrderChannel::SHOPEE, OrderChannel::TIKTOK])
|
|
||||||
->whereNotNull('marketplace_settings_snapshot')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$totalRevenue = $marketplaceOrders->sum('total_amount');
|
|
||||||
$totalFees = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['total_fee_amount'] ?? 0));
|
|
||||||
$totalNet = $marketplaceOrders->sum(fn ($order) => (int) ($order->marketplace_settings_snapshot['net_amount'] ?? 0));
|
|
||||||
$totalOrders = $marketplaceOrders->count();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total_revenue' => (int) $totalRevenue,
|
|
||||||
'total_fees' => (int) $totalFees,
|
|
||||||
'total_net' => (int) $totalNet,
|
|
||||||
'total_orders' => (int) $totalOrders,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCashAccounts(): array
|
|
||||||
{
|
|
||||||
return CashAccount::query()
|
|
||||||
->selectRaw('name, balance')
|
|
||||||
->get()
|
|
||||||
->map(fn ($item) => [
|
|
||||||
'name' => $item->name,
|
|
||||||
'balance' => (int) $item->balance,
|
|
||||||
])
|
|
||||||
->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCashSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
|
||||||
{
|
|
||||||
$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
|
|
||||||
")
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total_transactions' => (int) ($summary->total_transactions ?? 0),
|
|
||||||
'total_deposit' => (int) ($summary->total_deposit ?? 0),
|
|
||||||
'total_withdrawal' => (int) ($summary->total_withdrawal ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMonthlyCashFlow(): array
|
|
||||||
{
|
|
||||||
$months = collect();
|
|
||||||
for ($monthOffset = 5; $monthOffset >= 0; $monthOffset--) {
|
|
||||||
$date = Carbon::now()->subMonths($monthOffset);
|
|
||||||
$months->push([
|
|
||||||
'year' => $date->year,
|
|
||||||
'month' => $date->month,
|
|
||||||
'label' => $date->translatedFormat('M Y'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $months->map(function ($month) {
|
|
||||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
|
||||||
$end = $start->copy()->endOfMonth();
|
|
||||||
|
|
||||||
$monthlyTransactions = CashTransaction::query()
|
|
||||||
->whereBetween('created_at', [$start, $end])
|
|
||||||
->selectRaw("
|
|
||||||
COALESCE(SUM(CASE WHEN type = 'deposit' THEN amount ELSE 0 END), 0) as deposits,
|
|
||||||
COALESCE(SUM(CASE WHEN type = 'withdrawal' THEN amount ELSE 0 END), 0) as withdrawals
|
|
||||||
")
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'label' => $month['label'],
|
|
||||||
'deposits' => (int) $monthlyTransactions->deposits,
|
|
||||||
'withdrawals' => (int) $monthlyTransactions->withdrawals,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $result->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMonthlyExpenses(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
|
||||||
{
|
|
||||||
$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 [
|
|
||||||
'total' => (int) ($monthlyExpenses->total ?? 0),
|
|
||||||
'count' => (int) ($monthlyExpenses->count ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getKasbonSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
|
||||||
{
|
|
||||||
$pendingQuery = EmployeeAdvance::query()->pending();
|
|
||||||
$approvedQuery = EmployeeAdvance::query()->approved();
|
|
||||||
$paidQuery = EmployeeAdvance::query()->paid();
|
|
||||||
$employeeQuery = EmployeeAdvance::query();
|
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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' => [
|
|
||||||
'count' => (int) ($pending->count ?? 0),
|
|
||||||
'total' => (int) ($pending->total ?? 0),
|
|
||||||
],
|
|
||||||
'approved' => [
|
|
||||||
'count' => (int) ($approved->count ?? 0),
|
|
||||||
'total' => (int) ($approved->total ?? 0),
|
|
||||||
],
|
|
||||||
'paid' => [
|
|
||||||
'count' => (int) ($paid->count ?? 0),
|
|
||||||
'total' => (int) ($paid->total ?? 0),
|
|
||||||
],
|
|
||||||
'total' => (int) ($pending->total ?? 0) + (int) ($approved->total ?? 0) + (int) ($paid->total ?? 0),
|
|
||||||
'total_employees' => $totalEmployees,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPayrollSummary(Carbon $startOfMonth, Carbon $endOfMonth): array
|
|
||||||
{
|
|
||||||
$period = PayrollPeriod::query()
|
|
||||||
->where('year', $startOfMonth->year)
|
|
||||||
->where('month', $startOfMonth->month)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (! $period) {
|
|
||||||
return [
|
|
||||||
'has_period' => false,
|
|
||||||
'total_employees' => 0,
|
|
||||||
'total_amount' => 0,
|
|
||||||
'paid_amount' => 0,
|
|
||||||
'unpaid_amount' => 0,
|
|
||||||
'paid_count' => 0,
|
|
||||||
'unpaid_count' => 0,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$payrolls = Payroll::query()
|
|
||||||
->where('payroll_period_id', $period->id)
|
|
||||||
->selectRaw("
|
|
||||||
COUNT(*) as total_employees,
|
|
||||||
COALESCE(SUM(total_amount), 0) as total_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN status = 'paid' THEN total_amount ELSE 0 END), 0) as paid_amount,
|
|
||||||
COALESCE(SUM(CASE WHEN status = 'unpaid' THEN total_amount ELSE 0 END), 0) as unpaid_amount,
|
|
||||||
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid_count,
|
|
||||||
SUM(CASE WHEN status = 'unpaid' THEN 1 ELSE 0 END) as unpaid_count
|
|
||||||
")
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'has_period' => true,
|
|
||||||
'period_status' => $period->status->value,
|
|
||||||
'total_employees' => (int) ($payrolls->total_employees ?? 0),
|
|
||||||
'total_amount' => (int) ($payrolls->total_amount ?? 0),
|
|
||||||
'paid_amount' => (int) ($payrolls->paid_amount ?? 0),
|
|
||||||
'unpaid_amount' => (int) ($payrolls->unpaid_amount ?? 0),
|
|
||||||
'paid_count' => (int) ($payrolls->paid_count ?? 0),
|
|
||||||
'unpaid_count' => (int) ($payrolls->unpaid_count ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLeaveRequestSummary(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
|
||||||
{
|
|
||||||
$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,
|
|
||||||
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count
|
|
||||||
")
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total' => (int) ($leaveRequestSummary->total ?? 0),
|
|
||||||
'pending' => (int) ($leaveRequestSummary->pending_count ?? 0),
|
|
||||||
'approved' => (int) ($leaveRequestSummary->approved_count ?? 0),
|
|
||||||
'rejected' => (int) ($leaveRequestSummary->rejected_count ?? 0),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEmployeeSummary(): array
|
|
||||||
{
|
|
||||||
$total = Employee::query()->count();
|
|
||||||
|
|
||||||
$byStatus = Employee::query()
|
|
||||||
->selectRaw('employment_status, COUNT(*) as count')
|
|
||||||
->groupBy('employment_status')
|
|
||||||
->get()
|
|
||||||
->map(fn ($item) => [
|
|
||||||
'status' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->value : $item->employment_status,
|
|
||||||
'label' => $item->employment_status instanceof EmploymentStatus ? $item->employment_status->label() : EmploymentStatus::from($item->employment_status)->label(),
|
|
||||||
'count' => (int) $item->count,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total' => $total,
|
|
||||||
'by_status' => $byStatus->toArray(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getAttendanceToday(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
|
||||||
{
|
|
||||||
$totalEmployees = Employee::query()->count();
|
|
||||||
|
|
||||||
if ($startDate && $endDate) {
|
|
||||||
$present = Attendance::query()
|
|
||||||
->whereBetween('attendance_date', [$startDate, $endDate])
|
|
||||||
->count();
|
|
||||||
|
|
||||||
$onLeave = LeaveRequest::query()
|
|
||||||
->approved()
|
|
||||||
->where(function ($query) use ($startDate, $endDate) {
|
|
||||||
$query->whereBetween('start_date', [$startDate, $endDate])
|
|
||||||
->orWhereBetween('end_date', [$startDate, $endDate]);
|
|
||||||
})
|
|
||||||
->count();
|
|
||||||
|
|
||||||
$days = (int) $startDate->copy()->startOfDay()->diffInDays($endDate->copy()->startOfDay()) + 1;
|
|
||||||
$totalPossible = $totalEmployees * $days;
|
|
||||||
$absent = max(0, $totalPossible - $present - $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
|
|
||||||
{
|
|
||||||
$months = collect();
|
|
||||||
for ($monthOffset = 5; $monthOffset >= 0; $monthOffset--) {
|
|
||||||
$date = Carbon::now()->subMonths($monthOffset);
|
|
||||||
$months->push([
|
|
||||||
'year' => $date->year,
|
|
||||||
'month' => $date->month,
|
|
||||||
'label' => $date->translatedFormat('M Y'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $months->map(function ($month) {
|
|
||||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
|
||||||
$end = $start->copy()->endOfMonth();
|
|
||||||
|
|
||||||
$monthlyRevenue = Order::query()
|
|
||||||
->completed()
|
|
||||||
->whereBetween('created_at', [$start, $end])
|
|
||||||
->selectRaw('COALESCE(SUM(total_amount), 0) as total, COUNT(*) as count')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'label' => $month['label'],
|
|
||||||
'total' => (int) $monthlyRevenue->total,
|
|
||||||
'count' => (int) $monthlyRevenue->count,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $result->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMonthlyPurchaseTrend(): array
|
|
||||||
{
|
|
||||||
$months = collect();
|
|
||||||
for ($monthOffset = 5; $monthOffset >= 0; $monthOffset--) {
|
|
||||||
$date = Carbon::now()->subMonths($monthOffset);
|
|
||||||
$months->push([
|
|
||||||
'year' => $date->year,
|
|
||||||
'month' => $date->month,
|
|
||||||
'label' => $date->translatedFormat('M Y'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $months->map(function ($month) {
|
|
||||||
$start = Carbon::create($month['year'], $month['month'], 1)->startOfMonth();
|
|
||||||
$end = $start->copy()->endOfMonth();
|
|
||||||
|
|
||||||
$monthlyPurchases = Purchase::query()
|
|
||||||
->whereBetween('created_at', [$start, $end])
|
|
||||||
->selectRaw('COALESCE(SUM(total), 0) as total, COUNT(*) as count')
|
|
||||||
->first();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'label' => $month['label'],
|
|
||||||
'total' => (int) $monthlyPurchases->total,
|
|
||||||
'count' => (int) $monthlyPurchases->count,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $result->toArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Head } from '@inertiajs/vue3';
|
||||||
|
import {
|
||||||
|
Banknote,
|
||||||
|
Clock,
|
||||||
|
Moon,
|
||||||
|
Sun,
|
||||||
|
Sunrise,
|
||||||
|
TrendingDown,
|
||||||
|
TrendingUp,
|
||||||
|
UserCheck,
|
||||||
|
} from '@lucide/vue';
|
||||||
|
import { VisAxis, VisDonut, VisGroupedBar, VisXYContainer } from '@unovis/vue';
|
||||||
|
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import StatCard from '@/components/card/StatCard.vue';
|
import StatCard from '@/components/card/StatCard.vue';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -17,21 +30,40 @@ import {
|
|||||||
componentToString,
|
componentToString,
|
||||||
} from '@/components/ui/chart';
|
} from '@/components/ui/chart';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { formatRupiah } from '@/lib/rupiah';
|
||||||
import {
|
|
||||||
Banknote,
|
|
||||||
Clock,
|
|
||||||
Moon,
|
|
||||||
Sun,
|
|
||||||
Sunrise,
|
|
||||||
TrendingDown,
|
|
||||||
TrendingUp,
|
|
||||||
UserCheck,
|
|
||||||
} from '@lucide/vue';
|
|
||||||
import { VisAxis, VisDonut, VisGroupedBar, VisXYContainer } from '@unovis/vue';
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
|
// Row 1: Stat Cards
|
||||||
|
attendance: {
|
||||||
|
total_employees: number;
|
||||||
|
percentage: number;
|
||||||
|
present: number;
|
||||||
|
absent: number;
|
||||||
|
on_leave: number;
|
||||||
|
};
|
||||||
|
cashOverview: {
|
||||||
|
total_balance: number;
|
||||||
|
total_transactions: number;
|
||||||
|
total_deposit: number;
|
||||||
|
total_withdrawal: number;
|
||||||
|
};
|
||||||
|
revenueSummary: {
|
||||||
|
total_revenue: number;
|
||||||
|
total_discount: number;
|
||||||
|
total_marketplace_fees: number;
|
||||||
|
total_potongan: number;
|
||||||
|
total_shipping: number;
|
||||||
|
total_orders: number;
|
||||||
|
avg_order: number;
|
||||||
|
};
|
||||||
|
expenseSummary: {
|
||||||
|
total: number;
|
||||||
|
purchase_total: number;
|
||||||
|
expense_total: number;
|
||||||
|
advance_total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Row 2: Top 5 Charts
|
||||||
topSuppliers: Array<{
|
topSuppliers: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
@ -47,22 +79,8 @@ interface DashboardProps {
|
|||||||
total_qty: number;
|
total_qty: number;
|
||||||
total_revenue: number;
|
total_revenue: number;
|
||||||
}>;
|
}>;
|
||||||
purchaseSummary: {
|
|
||||||
total_purchases: number;
|
// Row 3: Order Stats
|
||||||
total_spent: number;
|
|
||||||
total_discount: number;
|
|
||||||
};
|
|
||||||
cuttingSummary: {
|
|
||||||
total_cost: number;
|
|
||||||
total_cutting: number;
|
|
||||||
total_warehouse: number;
|
|
||||||
total_reject: number;
|
|
||||||
};
|
|
||||||
cuttingByStatus: Array<{
|
|
||||||
status: string;
|
|
||||||
label: string;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
orderStats: {
|
orderStats: {
|
||||||
by_channel: Array<{
|
by_channel: Array<{
|
||||||
channel: string;
|
channel: string;
|
||||||
@ -87,80 +105,6 @@ interface DashboardProps {
|
|||||||
count: number;
|
count: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
revenueSummary: {
|
|
||||||
total_revenue: number;
|
|
||||||
total_discount: number;
|
|
||||||
total_marketplace_fees: number;
|
|
||||||
total_potongan: number;
|
|
||||||
total_shipping: number;
|
|
||||||
total_orders: number;
|
|
||||||
avg_order: number;
|
|
||||||
};
|
|
||||||
marketplaceSummary: {
|
|
||||||
total_revenue: number;
|
|
||||||
total_fees: number;
|
|
||||||
total_net: number;
|
|
||||||
total_orders: number;
|
|
||||||
};
|
|
||||||
cashAccounts: Array<{
|
|
||||||
name: string;
|
|
||||||
balance: number;
|
|
||||||
}>;
|
|
||||||
cashSummary: {
|
|
||||||
total_transactions: number;
|
|
||||||
total_deposit: number;
|
|
||||||
total_withdrawal: number;
|
|
||||||
};
|
|
||||||
monthlyCashFlow: Array<{
|
|
||||||
label: string;
|
|
||||||
deposits: number;
|
|
||||||
withdrawals: 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;
|
|
||||||
};
|
|
||||||
payrollSummary: {
|
|
||||||
has_period: boolean;
|
|
||||||
period_status?: string;
|
|
||||||
total_employees: number;
|
|
||||||
total_amount: number;
|
|
||||||
paid_amount: number;
|
|
||||||
unpaid_amount: number;
|
|
||||||
paid_count: number;
|
|
||||||
unpaid_count: number;
|
|
||||||
};
|
|
||||||
employeeSummary: {
|
|
||||||
total: number;
|
|
||||||
by_status: Array<{
|
|
||||||
status: string;
|
|
||||||
label: string;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
attendanceToday: {
|
|
||||||
total_employees: number;
|
|
||||||
present: number;
|
|
||||||
absent: number;
|
|
||||||
on_leave: number;
|
|
||||||
};
|
|
||||||
monthlyRevenueTrend: Array<{
|
|
||||||
label: string;
|
|
||||||
total: number;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
monthlyPurchaseTrend: Array<{
|
|
||||||
label: string;
|
|
||||||
total: number;
|
|
||||||
count: number;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<DashboardProps>();
|
const props = defineProps<DashboardProps>();
|
||||||
@ -214,10 +158,6 @@ const greeting = computed(() => {
|
|||||||
return { text: 'Selamat Malam', icon: Moon };
|
return { text: 'Selamat Malam', icon: Moon };
|
||||||
});
|
});
|
||||||
|
|
||||||
function formatRupiah(value: number): string {
|
|
||||||
return 'Rp ' + value.toLocaleString('id-ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chart configs
|
// Chart configs
|
||||||
const supplierChartConfig = {
|
const supplierChartConfig = {
|
||||||
amount: {
|
amount: {
|
||||||
@ -332,31 +272,21 @@ const productBarData = computed<ProductData[]>(() => {
|
|||||||
qty: p.total_qty,
|
qty: p.total_qty,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCashBalance = computed(() => {
|
|
||||||
return props.cashAccounts.reduce((sum, acc) => sum + acc.balance, 0);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Dashboard" />
|
|
||||||
|
<Head title="Dasbor" />
|
||||||
|
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div class="flex flex-1 flex-col gap-6">
|
<div class="flex flex-1 flex-col gap-6">
|
||||||
<!-- Welcome Card -->
|
<!-- Welcome Card -->
|
||||||
<Card class="relative overflow-hidden">
|
<Card class="relative overflow-hidden">
|
||||||
<div
|
<div class="absolute inset-0 bg-linear-to-br from-primary/5 to-background" />
|
||||||
class="absolute inset-0 bg-linear-to-br from-primary/5 to-background"
|
|
||||||
/>
|
|
||||||
<CardHeader class="relative">
|
<CardHeader class="relative">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div
|
<div class="flex size-12 items-center justify-center rounded-full bg-primary/10">
|
||||||
class="flex size-12 items-center justify-center rounded-full bg-primary/10"
|
<component :is="greeting.icon" class="size-6 text-primary" />
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="greeting.icon"
|
|
||||||
class="size-6 text-primary"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle class="text-2xl font-bold">
|
<CardTitle class="text-2xl font-bold">
|
||||||
@ -370,9 +300,7 @@ const totalCashBalance = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="relative">
|
<CardContent class="relative">
|
||||||
<div
|
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
class="flex items-center gap-2 text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
<span>{{ dateStr }}</span>
|
<span>{{ dateStr }}</span>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
@ -383,252 +311,143 @@ const totalCashBalance = computed(() => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 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">
|
||||||
<!-- Attendance Today -->
|
<StatCard title="Kehadiran" :icon="UserCheck" main-label="Total Karyawan"
|
||||||
<StatCard
|
:main-value="attendance.total_employees" :sub-label="attendance.percentage + '% hadir'" :items="[
|
||||||
title="Kehadiran"
|
|
||||||
:icon="UserCheck"
|
|
||||||
main-label="Total Karyawan"
|
|
||||||
:main-value="attendanceToday.total_employees"
|
|
||||||
:sub-label="
|
|
||||||
(attendanceToday.total_employees > 0
|
|
||||||
? Math.round(
|
|
||||||
(attendanceToday.present /
|
|
||||||
attendanceToday.total_employees) *
|
|
||||||
100,
|
|
||||||
)
|
|
||||||
: 0) + '% hadir'
|
|
||||||
"
|
|
||||||
:items="[
|
|
||||||
{
|
{
|
||||||
label: 'Hadir',
|
label: 'Hadir',
|
||||||
value: attendanceToday.present,
|
value: attendance.present,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Tidak Hadir',
|
label: 'Tidak Hadir',
|
||||||
value: attendanceToday.absent,
|
value: attendance.absent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Cuti',
|
label: 'Cuti',
|
||||||
value: attendanceToday.on_leave ?? 0,
|
value: attendance.on_leave,
|
||||||
},
|
},
|
||||||
]"
|
]" />
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Cash Accounts -->
|
<StatCard title="Kas Toko" :icon="Banknote" main-label="Total Saldo"
|
||||||
<StatCard
|
:main-value="'Rp' + formatRupiah(cashOverview.total_balance)" :sub-label="cashOverview.total_transactions + ' transaksi'
|
||||||
title="Kas Toko"
|
" :items="[
|
||||||
:icon="Banknote"
|
|
||||||
main-label="Total Saldo"
|
|
||||||
:main-value="formatRupiah(totalCashBalance)"
|
|
||||||
:sub-label="
|
|
||||||
cashSummary.total_transactions + ' transaksi hari ini'
|
|
||||||
"
|
|
||||||
:items="[
|
|
||||||
{
|
{
|
||||||
label: 'Deposit',
|
label: 'Deposit',
|
||||||
value: formatRupiah(cashSummary.total_deposit),
|
value: 'Rp' + formatRupiah(cashOverview.total_deposit),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Withdrawal',
|
label: 'Withdrawal',
|
||||||
value: formatRupiah(cashSummary.total_withdrawal),
|
value: 'Rp' + formatRupiah(cashOverview.total_withdrawal),
|
||||||
},
|
},
|
||||||
]"
|
]" :cols="2" />
|
||||||
:cols="2"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<StatCard
|
<StatCard title="Pendapatan" :icon="TrendingUp" main-label="Total"
|
||||||
title="Total Pengeluaran"
|
:main-value="'Rp' + formatRupiah(revenueSummary.total_revenue)" :sub-label="revenueSummary.total_orders + ' transaksi selesai'
|
||||||
:icon="TrendingDown"
|
" :items="[
|
||||||
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),
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Total Pendapatan -->
|
|
||||||
<StatCard
|
|
||||||
title="Total Pendapatan"
|
|
||||||
:icon="TrendingUp"
|
|
||||||
main-label="Total"
|
|
||||||
:main-value="formatRupiah(revenueSummary.total_revenue)"
|
|
||||||
:sub-label="
|
|
||||||
revenueSummary.total_orders + ' transaksi selesai'
|
|
||||||
"
|
|
||||||
:items="[
|
|
||||||
{
|
{
|
||||||
label: 'Bersih',
|
label: 'Bersih',
|
||||||
value: formatRupiah(
|
value: 'Rp' + formatRupiah(
|
||||||
revenueSummary.total_revenue -
|
revenueSummary.total_revenue -
|
||||||
revenueSummary.total_potongan,
|
revenueSummary.total_potongan,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Potongan',
|
label: 'Potongan',
|
||||||
value: formatRupiah(revenueSummary.total_potongan),
|
value: 'Rp' + formatRupiah(revenueSummary.total_potongan),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Diskon',
|
label: 'Diskon',
|
||||||
value: formatRupiah(revenueSummary.total_discount),
|
value: 'Rp' + formatRupiah(revenueSummary.total_discount),
|
||||||
},
|
},
|
||||||
]"
|
]" />
|
||||||
/>
|
|
||||||
|
<StatCard title="Pengeluaran" :icon="TrendingDown" main-label="Total"
|
||||||
|
:main-value="'Rp' + formatRupiah(expenseSummary.total)" :items="[
|
||||||
|
{
|
||||||
|
label: 'Belanja',
|
||||||
|
value: 'Rp' + formatRupiah(expenseSummary.purchase_total),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pengeluaran',
|
||||||
|
value: 'Rp' + formatRupiah(expenseSummary.expense_total),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Kasbon',
|
||||||
|
value: 'Rp' + formatRupiah(expenseSummary.advance_total),
|
||||||
|
},
|
||||||
|
]" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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 -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-base">Top 5 Supplier</CardTitle>
|
<CardTitle class="text-base">Top 5 Supplier</CardTitle>
|
||||||
<CardDescription
|
<CardDescription>Berdasarkan total harga pembelian</CardDescription>
|
||||||
>Berdasarkan total harga pembelian</CardDescription
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ChartContainer
|
<ChartContainer v-if="supplierBarData.length > 0" :config="supplierChartConfig"
|
||||||
v-if="supplierBarData.length > 0"
|
class="min-h-[250px] w-full">
|
||||||
:config="supplierChartConfig"
|
|
||||||
class="min-h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<VisXYContainer :data="supplierBarData">
|
<VisXYContainer :data="supplierBarData">
|
||||||
<VisGroupedBar
|
<VisGroupedBar :x="(d: SupplierData) => d.name" :y="(d: SupplierData) => d.amount"
|
||||||
:x="(d: SupplierData) => d.name"
|
:color="supplierChartConfig.amount.color" :rounded-corners="4" bar-padding="0.1" />
|
||||||
:y="(d: SupplierData) => d.amount"
|
|
||||||
:color="supplierChartConfig.amount.color"
|
|
||||||
:rounded-corners="4"
|
|
||||||
bar-padding="0.1"
|
|
||||||
/>
|
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
v-else
|
|
||||||
class="flex h-[250px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data supplier
|
Belum ada data supplier
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Top 5 Customers Chart -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-base">Top 5 Pelanggan</CardTitle>
|
<CardTitle class="text-base">Top 5 Pelanggan</CardTitle>
|
||||||
<CardDescription
|
<CardDescription>Berdasarkan total nilai pesanan</CardDescription>
|
||||||
>Berdasarkan total nilai pesanan</CardDescription
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ChartContainer
|
<ChartContainer v-if="customerBarData.length > 0" :config="customerChartConfig"
|
||||||
v-if="customerBarData.length > 0"
|
class="min-h-[250px] w-full">
|
||||||
:config="customerChartConfig"
|
|
||||||
class="min-h-[250px] w-full"
|
|
||||||
>
|
|
||||||
<VisXYContainer :data="customerBarData">
|
<VisXYContainer :data="customerBarData">
|
||||||
<VisGroupedBar
|
<VisGroupedBar :x="(d: CustomerData) => d.name" :y="(d: CustomerData) => d.amount"
|
||||||
:x="(d: CustomerData) => d.name"
|
:color="customerChartConfig.amount.color" :rounded-corners="4" bar-padding="0.1" />
|
||||||
:y="(d: CustomerData) => d.amount"
|
|
||||||
:color="customerChartConfig.amount.color"
|
|
||||||
:rounded-corners="4"
|
|
||||||
bar-padding="0.1"
|
|
||||||
/>
|
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
v-else
|
|
||||||
class="flex h-[250px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data pelanggan
|
Belum ada data pelanggan
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Top 5 Products Chart -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-base">Top 5 Produk</CardTitle>
|
<CardTitle class="text-base">Top 5 Produk</CardTitle>
|
||||||
<CardDescription
|
<CardDescription>Berdasarkan jumlah terjual</CardDescription>
|
||||||
>Berdasarkan jumlah terjual</CardDescription
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ChartContainer
|
<ChartContainer v-if="productBarData.length > 0" :config="productChartConfig"
|
||||||
v-if="productBarData.length > 0"
|
class="min-h-[250px] w-full">
|
||||||
:config="productChartConfig"
|
<VisXYContainer :data="productBarData" :margin="{ left: -24 }" :y-domain="[0, undefined]">
|
||||||
class="min-h-[250px] w-full"
|
<VisGroupedBar :x="(d: ProductData) => d.name" :y="(d: ProductData) => d.qty"
|
||||||
>
|
:color="productChartConfig.qty.color" :rounded-corners="10" bar-padding="0.1" />
|
||||||
<VisXYContainer
|
<VisAxis type="x" :x="(d: ProductData) => d.name" :tick-line="false"
|
||||||
:data="productBarData"
|
:domain-line="false" :grid-line="false" :num-ticks="productBarData.length"
|
||||||
:margin="{ left: -24 }"
|
:tick-values="productBarData.map((d) => d.name)
|
||||||
:y-domain="[0, undefined]"
|
" />
|
||||||
>
|
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" />
|
||||||
<VisGroupedBar
|
|
||||||
:x="(d: ProductData) => d.name"
|
|
||||||
:y="(d: ProductData) => d.qty"
|
|
||||||
:color="productChartConfig.qty.color"
|
|
||||||
:rounded-corners="10"
|
|
||||||
bar-padding="0.1"
|
|
||||||
/>
|
|
||||||
<VisAxis
|
|
||||||
type="x"
|
|
||||||
:x="(d: ProductData) => d.name"
|
|
||||||
:tick-line="false"
|
|
||||||
:domain-line="false"
|
|
||||||
:grid-line="false"
|
|
||||||
:num-ticks="productBarData.length"
|
|
||||||
:tick-values="
|
|
||||||
productBarData.map((d) => d.name)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<VisAxis
|
|
||||||
type="y"
|
|
||||||
:num-ticks="3"
|
|
||||||
:tick-line="false"
|
|
||||||
:domain-line="false"
|
|
||||||
/>
|
|
||||||
<ChartTooltip />
|
<ChartTooltip />
|
||||||
<ChartCrosshair
|
<ChartCrosshair :template="componentToString(
|
||||||
:template="
|
|
||||||
componentToString(
|
|
||||||
productChartConfig,
|
productChartConfig,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
{ hideLabel: true },
|
{ hideLabel: true },
|
||||||
)
|
)
|
||||||
"
|
" color="#0000" />
|
||||||
color="#0000"
|
|
||||||
/>
|
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
v-else
|
|
||||||
class="flex h-[250px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data produk
|
Belum ada data produk
|
||||||
</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
|
<div class="flex gap-2 leading-none font-medium text-muted-foreground">
|
||||||
class="flex gap-2 leading-none font-medium text-muted-foreground"
|
|
||||||
>
|
|
||||||
Menampilkan 5 produk dengan penjualan tertinggi
|
Menampilkan 5 produk dengan penjualan tertinggi
|
||||||
<TrendingUp class="h-4 w-4 text-green-500" />
|
<TrendingUp class="h-4 w-4 text-green-500" />
|
||||||
</div>
|
</div>
|
||||||
@ -636,384 +455,177 @@ const totalCashBalance = computed(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Row 4: Order Stats Charts -->
|
|
||||||
<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">
|
||||||
<!-- Order by Channel -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-base"
|
<CardTitle class="text-base">Pesanan per Channel</CardTitle>
|
||||||
>Pesanan per Channel</CardTitle
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div v-if="orderStats.by_channel.length > 0">
|
<div v-if="orderStats.by_channel.length > 0">
|
||||||
<ChartContainer
|
<ChartContainer :config="channelChartConfig" class="min-h-[200px] w-full">
|
||||||
:config="channelChartConfig"
|
|
||||||
class="min-h-[200px] w-full"
|
|
||||||
>
|
|
||||||
<VisXYContainer :data="orderStats.by_channel">
|
<VisXYContainer :data="orderStats.by_channel">
|
||||||
<VisDonut
|
<VisDonut :value="(
|
||||||
:value="
|
|
||||||
(
|
|
||||||
d: (typeof orderStats.by_channel)[number],
|
d: (typeof orderStats.by_channel)[number],
|
||||||
) => d.count
|
) => d.count
|
||||||
"
|
" :color="orderStats.by_channel.map(
|
||||||
:color="
|
|
||||||
orderStats.by_channel.map(
|
|
||||||
(_, i) =>
|
(_, i) =>
|
||||||
channelColors[
|
channelColors[
|
||||||
i % channelColors.length
|
i % channelColors.length
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div
|
<div class="mt-3 flex flex-wrap justify-center gap-3">
|
||||||
class="mt-3 flex flex-wrap justify-center gap-3"
|
<div v-for="(
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(
|
|
||||||
item, index
|
item, index
|
||||||
) in orderStats.by_channel"
|
) in orderStats.by_channel" :key="item.channel" class="flex items-center gap-1.5">
|
||||||
:key="item.channel"
|
<span class="size-2.5 rounded-full" :style="{
|
||||||
class="flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="size-2.5 rounded-full"
|
|
||||||
:style="{
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
channelColors[
|
channelColors[
|
||||||
index % channelColors.length
|
index % channelColors.length
|
||||||
],
|
],
|
||||||
}"
|
}" />
|
||||||
/>
|
<span class="text-xs text-muted-foreground">{{ item.label }}</span>
|
||||||
<span
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
>{{ item.label }}</span
|
|
||||||
>
|
|
||||||
<span class="text-xs font-medium">{{
|
<span class="text-xs font-medium">{{
|
||||||
item.count
|
item.count
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="flex h-[200px] items-center justify-center text-muted-foreground">
|
||||||
v-else
|
|
||||||
class="flex h-[200px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data
|
Belum ada data
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Order by Payment Type -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-base"
|
<CardTitle class="text-base">Pesanan per Pembayaran</CardTitle>
|
||||||
>Pesanan per Pembayaran</CardTitle
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div v-if="orderStats.by_payment_type.length > 0">
|
<div v-if="orderStats.by_payment_type.length > 0">
|
||||||
<ChartContainer
|
<ChartContainer :config="paymentChartConfig" class="min-h-[200px] w-full">
|
||||||
:config="paymentChartConfig"
|
<VisXYContainer :data="orderStats.by_payment_type">
|
||||||
class="min-h-[200px] w-full"
|
<VisDonut :value="(
|
||||||
>
|
|
||||||
<VisXYContainer
|
|
||||||
:data="orderStats.by_payment_type"
|
|
||||||
>
|
|
||||||
<VisDonut
|
|
||||||
:value="
|
|
||||||
(
|
|
||||||
d: (typeof orderStats.by_payment_type)[number],
|
d: (typeof orderStats.by_payment_type)[number],
|
||||||
) => d.count
|
) => d.count
|
||||||
"
|
" :color="orderStats.by_payment_type.map(
|
||||||
:color="
|
|
||||||
orderStats.by_payment_type.map(
|
|
||||||
(_, i) =>
|
(_, i) =>
|
||||||
paymentColors[
|
paymentColors[
|
||||||
i % paymentColors.length
|
i % paymentColors.length
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div
|
<div class="mt-3 flex flex-wrap justify-center gap-3">
|
||||||
class="mt-3 flex flex-wrap justify-center gap-3"
|
<div v-for="(
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(
|
|
||||||
item, index
|
item, index
|
||||||
) in orderStats.by_payment_type"
|
) in orderStats.by_payment_type" :key="item.payment_type"
|
||||||
:key="item.payment_type"
|
class="flex items-center gap-1.5">
|
||||||
class="flex items-center gap-1.5"
|
<span class="size-2.5 rounded-full" :style="{
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="size-2.5 rounded-full"
|
|
||||||
:style="{
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
paymentColors[
|
paymentColors[
|
||||||
index % paymentColors.length
|
index % paymentColors.length
|
||||||
],
|
],
|
||||||
}"
|
}" />
|
||||||
/>
|
<span class="text-xs text-muted-foreground">{{ item.label }}</span>
|
||||||
<span
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
>{{ item.label }}</span
|
|
||||||
>
|
|
||||||
<span class="text-xs font-medium">{{
|
<span class="text-xs font-medium">{{
|
||||||
item.count
|
item.count
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="flex h-[200px] items-center justify-center text-muted-foreground">
|
||||||
v-else
|
|
||||||
class="flex h-[200px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data
|
Belum ada data
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Order by Marketing -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-base"
|
<CardTitle class="text-base">Pesanan per Marketing</CardTitle>
|
||||||
>Pesanan per Marketing</CardTitle
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div v-if="orderStats.by_marketing.length > 0">
|
<div v-if="orderStats.by_marketing.length > 0">
|
||||||
<ChartContainer
|
<ChartContainer :config="marketingChartConfig" class="min-h-[200px] w-full">
|
||||||
:config="marketingChartConfig"
|
|
||||||
class="min-h-[200px] w-full"
|
|
||||||
>
|
|
||||||
<VisXYContainer :data="orderStats.by_marketing">
|
<VisXYContainer :data="orderStats.by_marketing">
|
||||||
<VisDonut
|
<VisDonut :value="(
|
||||||
:value="
|
|
||||||
(
|
|
||||||
d: (typeof orderStats.by_marketing)[number],
|
d: (typeof orderStats.by_marketing)[number],
|
||||||
) => d.count
|
) => d.count
|
||||||
"
|
" :color="orderStats.by_marketing.map(
|
||||||
:color="
|
|
||||||
orderStats.by_marketing.map(
|
|
||||||
(_, i) =>
|
(_, i) =>
|
||||||
marketingColors[
|
marketingColors[
|
||||||
i %
|
i %
|
||||||
marketingColors.length
|
marketingColors.length
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div
|
<div class="mt-3 flex flex-wrap justify-center gap-3">
|
||||||
class="mt-3 flex flex-wrap justify-center gap-3"
|
<div v-for="(
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(
|
|
||||||
item, index
|
item, index
|
||||||
) in orderStats.by_marketing"
|
) in orderStats.by_marketing" :key="item.name" class="flex items-center gap-1.5">
|
||||||
:key="item.name"
|
<span class="size-2.5 rounded-full" :style="{
|
||||||
class="flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="size-2.5 rounded-full"
|
|
||||||
:style="{
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
marketingColors[
|
marketingColors[
|
||||||
index %
|
index %
|
||||||
marketingColors.length
|
marketingColors.length
|
||||||
],
|
],
|
||||||
}"
|
}" />
|
||||||
/>
|
<span class="text-xs text-muted-foreground">{{ item.name }}</span>
|
||||||
<span
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
>{{ item.name }}</span
|
|
||||||
>
|
|
||||||
<span class="text-xs font-medium">{{
|
<span class="text-xs font-medium">{{
|
||||||
item.count
|
item.count
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="flex h-[200px] items-center justify-center text-muted-foreground">
|
||||||
v-else
|
|
||||||
class="flex h-[200px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data
|
Belum ada data
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Order by Status -->
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle class="text-base"
|
<CardTitle class="text-base">Pesanan per Status</CardTitle>
|
||||||
>Pesanan per Status</CardTitle
|
|
||||||
>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div v-if="orderStats.by_status.length > 0">
|
<div v-if="orderStats.by_status.length > 0">
|
||||||
<ChartContainer
|
<ChartContainer :config="statusChartConfig" class="min-h-[200px] w-full">
|
||||||
:config="statusChartConfig"
|
|
||||||
class="min-h-[200px] w-full"
|
|
||||||
>
|
|
||||||
<VisXYContainer :data="orderStats.by_status">
|
<VisXYContainer :data="orderStats.by_status">
|
||||||
<VisDonut
|
<VisDonut :value="(
|
||||||
:value="
|
|
||||||
(
|
|
||||||
d: (typeof orderStats.by_status)[number],
|
d: (typeof orderStats.by_status)[number],
|
||||||
) => d.count
|
) => d.count
|
||||||
"
|
" :color="orderStats.by_status.map(
|
||||||
:color="
|
|
||||||
orderStats.by_status.map(
|
|
||||||
(_, i) =>
|
(_, i) =>
|
||||||
statusColors[
|
statusColors[
|
||||||
i % statusColors.length
|
i % statusColors.length
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
"
|
" />
|
||||||
/>
|
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div
|
<div class="mt-3 flex flex-wrap justify-center gap-3">
|
||||||
class="mt-3 flex flex-wrap justify-center gap-3"
|
<div v-for="(
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="(
|
|
||||||
item, index
|
item, index
|
||||||
) in orderStats.by_status"
|
) in orderStats.by_status" :key="item.status" class="flex items-center gap-1.5">
|
||||||
:key="item.status"
|
<span class="size-2.5 rounded-full" :style="{
|
||||||
class="flex items-center gap-1.5"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="size-2.5 rounded-full"
|
|
||||||
:style="{
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
statusColors[
|
statusColors[
|
||||||
index % statusColors.length
|
index % statusColors.length
|
||||||
],
|
],
|
||||||
}"
|
}" />
|
||||||
/>
|
<span class="text-xs text-muted-foreground">{{ item.label }}</span>
|
||||||
<span
|
|
||||||
class="text-xs text-muted-foreground"
|
|
||||||
>{{ item.label }}</span
|
|
||||||
>
|
|
||||||
<span class="text-xs font-medium">{{
|
<span class="text-xs font-medium">{{
|
||||||
item.count
|
item.count
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-else class="flex h-[200px] items-center justify-center text-muted-foreground">
|
||||||
v-else
|
|
||||||
class="flex h-[200px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Row 8: Monthly Trends -->
|
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
|
||||||
<!-- Monthly Revenue Trend -->
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="text-base"
|
|
||||||
>Tren Pendapatan 6 Bulan</CardTitle
|
|
||||||
>
|
|
||||||
<CardDescription
|
|
||||||
>Total pendapatan per bulan</CardDescription
|
|
||||||
>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div
|
|
||||||
v-if="monthlyRevenueTrend.length > 0"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="item in monthlyRevenueTrend"
|
|
||||||
:key="item.label"
|
|
||||||
class="flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="w-16 text-xs text-muted-foreground"
|
|
||||||
>{{ item.label }}</span
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-4 flex-1 overflow-hidden rounded-full bg-muted"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-green-500 transition-all"
|
|
||||||
:style="{
|
|
||||||
width: `${Math.max(5, (item.total / Math.max(...monthlyRevenueTrend.map((r) => r.total))) * 100)}%`,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="w-24 text-right text-xs font-medium"
|
|
||||||
>{{ formatRupiah(item.total) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex h-[150px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Monthly Purchase Trend -->
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="text-base"
|
|
||||||
>Tren Pembelian 6 Bulan</CardTitle
|
|
||||||
>
|
|
||||||
<CardDescription
|
|
||||||
>Total pembelian per bulan</CardDescription
|
|
||||||
>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div
|
|
||||||
v-if="monthlyPurchaseTrend.length > 0"
|
|
||||||
class="space-y-2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="item in monthlyPurchaseTrend"
|
|
||||||
:key="item.label"
|
|
||||||
class="flex items-center gap-3"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="w-16 text-xs text-muted-foreground"
|
|
||||||
>{{ item.label }}</span
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-4 flex-1 overflow-hidden rounded-full bg-muted"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="h-full rounded-full bg-blue-500 transition-all"
|
|
||||||
:style="{
|
|
||||||
width: `${Math.max(5, (item.total / Math.max(...monthlyPurchaseTrend.map((r) => r.total))) * 100)}%`,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="w-24 text-right text-xs font-medium"
|
|
||||||
>{{ formatRupiah(item.total) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex h-[150px] items-center justify-center text-muted-foreground"
|
|
||||||
>
|
|
||||||
Belum ada data
|
Belum ada data
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user