diff --git a/app/Http/Controllers/Admin/Manage/TransactionController.php b/app/Http/Controllers/Admin/Manage/TransactionController.php index d2312a0..9c2588e 100644 --- a/app/Http/Controllers/Admin/Manage/TransactionController.php +++ b/app/Http/Controllers/Admin/Manage/TransactionController.php @@ -26,6 +26,7 @@ public function index(PaginatedRequest $request): Response ...$request->validatedWithDefaults(), filters: $filters, ), + 'summary' => $this->service->getSummary($filters), 'filters' => $filters, 'filterOptions' => $this->service->getFilterOptions(), ]); diff --git a/app/Services/Admin/Manage/TransactionService.php b/app/Services/Admin/Manage/TransactionService.php index 9ed6658..737acf4 100644 --- a/app/Services/Admin/Manage/TransactionService.php +++ b/app/Services/Admin/Manage/TransactionService.php @@ -89,6 +89,30 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = return $paginator; } + public function getSummary(array $filters = []): array + { + $query = Order::query() + ->selectRaw('COUNT(*) as total_orders') + ->selectRaw('COALESCE(SUM(subtotal), 0) as total_subtotal') + ->selectRaw('COALESCE(SUM(discount), 0) as total_discount') + ->selectRaw('COALESCE(SUM(total_amount), 0) as total_amount') + ->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status)) + ->when($filters['channel'] ?? null, fn($q, $channel) => $q->where('channel', $channel)) + ->when($filters['payment_type'] ?? null, fn($q, $paymentType) => $q->where('payment_type', $paymentType)) + ->when($filters['customer_id'] ?? null, fn($q, $customerId) => $q->where('customer_id', $customerId)) + ->when($filters['marketing_id'] ?? null, fn($q, $marketingId) => $q->where('marketing_id', $marketingId)) + ->when($filters['created_by_id'] ?? null, fn($q, $createdById) => $q->where('created_by_id', $createdById)) + ->first(); + + return [ + 'total_orders' => $query->total_orders, + 'total_subtotal' => $query->total_subtotal, + 'total_discount' => $query->total_discount, + 'total_amount' => $query->total_amount, + 'net_total' => $query->total_amount, + ]; + } + public function getFilterOptions(): array { return [ diff --git a/app/Services/Concerns/HasStockAdjustment.php b/app/Services/Concerns/HasStockAdjustment.php index 0bf54f3..868ad69 100644 --- a/app/Services/Concerns/HasStockAdjustment.php +++ b/app/Services/Concerns/HasStockAdjustment.php @@ -5,6 +5,7 @@ use App\Enums\ProductStockQuality; use App\Models\ProductVariant; use Illuminate\Database\Eloquent\Model; +use Illuminate\Validation\ValidationException; trait HasStockAdjustment { @@ -18,6 +19,17 @@ private function adjustStock(Model $model, string $field, int $quantity, int $si if ($sign > 0) { $model->increment($field, $quantity); } else { + if ($model->{$field} < $quantity) { + $label = match ($field) { + 'stock' => 'stok bagus', + 'reject_stock' => 'stok reject', + 'retail_stock' => 'stok ecer', + default => $field, + }; + throw ValidationException::withMessages([ + 'stock' => "Stok {$label} tidak mencukupi. Tersedia: {$model->{$field}}, dibutuhkan: {$quantity}.", + ]); + } $model->decrement($field, $quantity); } } diff --git a/resources/js/pages/admin/manage/transaction/index.tsx b/resources/js/pages/admin/manage/transaction/index.tsx index daf32ae..b463069 100644 --- a/resources/js/pages/admin/manage/transaction/index.tsx +++ b/resources/js/pages/admin/manage/transaction/index.tsx @@ -33,6 +33,7 @@ import { import type { Transaction } from './columns'; import { TransactionCardRow } from './transaction-card'; import { TransactionItemSubRow } from './transaction-sub-row'; +import { TransactionSummaryCard } from './transaction-summary-card'; type FilterOption = { id: number; @@ -52,6 +53,13 @@ type Props = { per_page: number; total: number; }; + summary: { + total_orders: number; + total_subtotal: number; + total_discount: number; + total_amount: number; + net_total: number; + }; filters: { status?: string; channel?: string; @@ -71,6 +79,7 @@ type Props = { export default function TransactionIndex({ transactions, + summary, filters, filterOptions, }: Props) { @@ -333,6 +342,8 @@ export default function TransactionIndex({ } /> + + t.id} diff --git a/resources/js/pages/admin/manage/transaction/transaction-card.tsx b/resources/js/pages/admin/manage/transaction/transaction-card.tsx index ef0b2cd..baa7b3e 100644 --- a/resources/js/pages/admin/manage/transaction/transaction-card.tsx +++ b/resources/js/pages/admin/manage/transaction/transaction-card.tsx @@ -1,4 +1,3 @@ -import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; import { RowActions } from '@/components/row-actions'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -6,6 +5,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { useCan } from '@/hooks/use-can'; import { formatDateTime, formatNumber } from '@/lib/format'; import { formatCurrency } from '@/lib/utils'; +import { ChevronDown, Pencil, Trash2 } from 'lucide-react'; import type { Transaction } from './columns'; const STATUS_BADGE_CLASSES: Record = { @@ -70,7 +70,7 @@ export function TransactionCardRow({ {index}. -

+

{transaction.order_number}

)} {transaction.notes && ( - + {transaction.notes} )} diff --git a/resources/js/pages/admin/manage/transaction/transaction-summary-card.tsx b/resources/js/pages/admin/manage/transaction/transaction-summary-card.tsx new file mode 100644 index 0000000..5c720af --- /dev/null +++ b/resources/js/pages/admin/manage/transaction/transaction-summary-card.tsx @@ -0,0 +1,90 @@ +import { Card, CardContent } from '@/components/ui/card'; +import { formatCurrency } from '@/lib/utils'; +import { + Banknote, + CircleDollarSign, + FileText, + Percent, + TrendingUp, +} from 'lucide-react'; + +type Summary = { + total_orders: number; + total_subtotal: number; + total_discount: number; + total_amount: number; + net_total: number; +}; + +type TransactionSummaryCardProps = { + summary: Summary; +}; + +const summaryItems = [ + { + key: 'total_orders', + label: 'Total Pesanan', + icon: FileText, + color: 'bg-blue-100', + iconColor: 'text-blue-600', + format: (value: number) => value.toLocaleString('id-ID'), + }, + { + key: 'total_subtotal', + label: 'Subtotal', + icon: Banknote, + color: 'bg-emerald-100', + iconColor: 'text-emerald-600', + format: formatCurrency, + }, + { + key: 'total_discount', + label: 'Diskon', + icon: Percent, + color: 'bg-amber-100', + iconColor: 'text-amber-600', + format: formatCurrency, + }, + { + key: 'total_amount', + label: 'Total Uang', + icon: CircleDollarSign, + color: 'bg-purple-100', + iconColor: 'text-purple-600', + format: formatCurrency, + }, + { + key: 'net_total', + label: 'Total Bersih', + icon: TrendingUp, + color: 'bg-sky-100', + iconColor: 'text-sky-600', + format: formatCurrency, + }, +] as const; + +export function TransactionSummaryCard({ summary }: TransactionSummaryCardProps) { + return ( +
+ {summaryItems.map((item) => ( + + +
+ +
+
+

+ {item.label} +

+

+ {item.format(summary[item.key] as number)} +

+
+
+
+ ))} +
+ ); +}