feat: implement lazy loading for transaction items, optimize data fetching in Transaction service and controller
This commit is contained in:
parent
910be15abc
commit
0344648b74
@ -13,6 +13,7 @@
|
|||||||
use App\Services\Admin\Manage\TransactionService;
|
use App\Services\Admin\Manage\TransactionService;
|
||||||
use App\Services\Admin\Master\CustomerService;
|
use App\Services\Admin\Master\CustomerService;
|
||||||
use App\Services\Admin\Master\Product\ProductVariantService;
|
use App\Services\Admin\Master\Product\ProductVariantService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
use Inertia\Response;
|
use Inertia\Response;
|
||||||
@ -106,6 +107,13 @@ public function updateStatus(Order $transaction): RedirectResponse
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function items(Order $transaction): JsonResponse
|
||||||
|
{
|
||||||
|
return response()->json([
|
||||||
|
'items' => $this->service->getItems($transaction),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function getEmployees()
|
private function getEmployees()
|
||||||
{
|
{
|
||||||
return User::query()
|
return User::query()
|
||||||
|
|||||||
@ -41,6 +41,10 @@ public function __construct(
|
|||||||
|
|
||||||
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?User $user = null): LengthAwarePaginator
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?User $user = null): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
|
$itemsCountQuery = '(SELECT COUNT(*) FROM order_items WHERE order_items.order_id = orders.id AND order_items.deleted_at IS NULL)';
|
||||||
|
$totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM order_items WHERE order_items.order_id = orders.id AND order_items.deleted_at IS NULL)';
|
||||||
|
$productNamesQuery = '(SELECT GROUP_CONCAT(DISTINCT p.name ORDER BY p.name SEPARATOR \', \') FROM order_items oi JOIN product_variants pv ON pv.id = oi.product_variant_id JOIN products p ON p.id = pv.product_id WHERE oi.order_id = orders.id AND oi.deleted_at IS NULL)';
|
||||||
|
|
||||||
$paginator = Order::query()
|
$paginator = Order::query()
|
||||||
->select(['id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'tiktok_order_id', 'shopee_order_id', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at'])
|
->select(['id', 'created_by_id', 'customer_id', 'marketing_id', 'order_number', 'channel', 'price_type', 'status', 'payment_type', 'tiktok_order_id', 'shopee_order_id', 'subtotal', 'discount', 'nego_price', 'total_amount', 'cogs', 'notes', 'created_at'])
|
||||||
->with([
|
->with([
|
||||||
@ -49,10 +53,10 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
'customer:id,name',
|
'customer:id,name',
|
||||||
'marketing:id',
|
'marketing:id',
|
||||||
'marketing.userProfile:id,user_id,full_name',
|
'marketing.userProfile:id,user_id,full_name',
|
||||||
'orderItems:id,order_id,product_variant_id,stock_quality,quantity,unit_price,subtotal',
|
|
||||||
'orderItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock',
|
|
||||||
'orderItems.productVariant.product:id,name',
|
|
||||||
])
|
])
|
||||||
|
->selectRaw("{$itemsCountQuery} as items_count")
|
||||||
|
->selectRaw("{$totalQtyQuery} as total_qty")
|
||||||
|
->selectRaw("{$productNamesQuery} as product_names")
|
||||||
->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id))
|
->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id))
|
||||||
->when($search, function ($q) use ($search) {
|
->when($search, function ($q) use ($search) {
|
||||||
$q->whereHas('orderItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
$q->whereHas('orderItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
|
||||||
@ -84,7 +88,19 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
? $this->s3Service->getTemporaryUrl($orderMedia->getPath('thumb'))
|
? $this->s3Service->getTemporaryUrl($orderMedia->getPath('thumb'))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
$order->orderItems->each(function (OrderItem $item) {
|
$order->profit = $order->total_amount - $order->cogs;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $paginator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(Order $order): \Illuminate\Support\Collection
|
||||||
|
{
|
||||||
|
return $order->orderItems()
|
||||||
|
->select(['id', 'order_id', 'product_variant_id', 'stock_quality', 'quantity', 'unit_price', 'subtotal'])
|
||||||
|
->with(['productVariant:id,product_id,name', 'productVariant.product:id,name'])
|
||||||
|
->get()
|
||||||
|
->each(function (OrderItem $item) {
|
||||||
if (! $item->productVariant) {
|
if (! $item->productVariant) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -97,11 +113,6 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
|
|||||||
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
|
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
|
||||||
: null;
|
: null;
|
||||||
});
|
});
|
||||||
|
|
||||||
$order->profit = $order->total_amount - $order->cogs;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $paginator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSummary(array $filters = [], ?User $user = null): array
|
public function getSummary(array $filters = [], ?User $user = null): array
|
||||||
|
|||||||
@ -4,14 +4,14 @@ ## Date
|
|||||||
2026-08-14
|
2026-08-14
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
Optimize page load performance for raw materials, products, purchases, and cutting list pages by implementing lazy loading for child/variant data in card-based layouts.
|
Optimize page load performance for raw materials, products, purchases, cutting, and transaction list pages by implementing lazy loading for child/variant data in card-based layouts.
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
Pages with 100+ records loaded all child data eagerly (variants, items, materials), causing slow initial page loads.
|
Pages with 100+ records loaded all child data eagerly (variants, items, materials), causing slow initial page loads.
|
||||||
|
|
||||||
## Solution Pattern
|
## Solution Pattern
|
||||||
1. **Backend**: Remove eager-loaded relationships from paginated query
|
1. **Backend**: Remove eager-loaded relationships from paginated query
|
||||||
2. **Backend**: Add SQL subqueries for summary data (`variants_count`, `total_qty`, `material_name`, `unit`, `materials_count`, `total_usage`, `product_name`, `cutting_result`)
|
2. **Backend**: Add SQL subqueries for summary data (`variants_count`, `total_qty`, `material_name`, `unit`, `materials_count`, `total_usage`, `product_name`, `cutting_result`, `items_count`, `product_names`)
|
||||||
3. **Backend**: Add new controller method returning JSON for lazy load
|
3. **Backend**: Add new controller method returning JSON for lazy load
|
||||||
4. **Frontend**: Use `useCardTableExpand(false)` for default collapsed state
|
4. **Frontend**: Use `useCardTableExpand(false)` for default collapsed state
|
||||||
5. **Frontend**: Fetch child data via `fetch()` on expand, store in `Record<number, T[]>` state
|
5. **Frontend**: Fetch child data via `fetch()` on expand, store in `Record<number, T[]>` state
|
||||||
@ -19,6 +19,15 @@ ## Solution Pattern
|
|||||||
|
|
||||||
## Files Modified
|
## Files Modified
|
||||||
|
|
||||||
|
### Transaction (this session)
|
||||||
|
- `app/Services/Admin/Manage/TransactionService.php` - `paginated()` no longer eager loads `orderItems`; added `getItems()` method
|
||||||
|
- `app/Http/Controllers/Admin/Manage/TransactionController.php` - added `items()` method
|
||||||
|
- `routes/web.php` - added `GET transactions/{transaction}/items` route
|
||||||
|
- `resources/js/pages/admin/manage/transaction/columns.tsx` - `Transaction` type: `items_count`, `total_qty`, `product_names`
|
||||||
|
- `resources/js/pages/admin/manage/transaction/transaction-card.tsx` - uses summary data from props
|
||||||
|
- `resources/js/pages/admin/manage/transaction/transaction-sub-row.tsx` - accepts `items` & `isLoading` props
|
||||||
|
- `resources/js/pages/admin/manage/transaction/index.tsx` - lazy loading with `fetchItems()`, print fetches items on demand
|
||||||
|
|
||||||
### Cutting (this session)
|
### Cutting (this session)
|
||||||
- `app/Services/Admin/Manage/CuttingService.php` - `paginated()` no longer eager loads `cuttingResults`, `cuttingMaterials`, `cuttingMaterialCombinations`; added `getMaterials()` and `getCombinations()` methods
|
- `app/Services/Admin/Manage/CuttingService.php` - `paginated()` no longer eager loads `cuttingResults`, `cuttingMaterials`, `cuttingMaterialCombinations`; added `getMaterials()` and `getCombinations()` methods
|
||||||
- `app/Http/Controllers/Admin/Manage/CuttingController.php` - added `materials()` method
|
- `app/Http/Controllers/Admin/Manage/CuttingController.php` - added `materials()` method
|
||||||
@ -46,6 +55,7 @@ ## Routes Added
|
|||||||
```php
|
```php
|
||||||
Route::get('purchases/{purchase}/items', [PurchaseController::class, 'items'])->name('purchases.items')->middleware('permission:purchases.view');
|
Route::get('purchases/{purchase}/items', [PurchaseController::class, 'items'])->name('purchases.items')->middleware('permission:purchases.view');
|
||||||
Route::get('cuttings/{cutting}/materials', [CuttingController::class, 'materials'])->name('cuttings.materials')->middleware('permission:cuttings.view');
|
Route::get('cuttings/{cutting}/materials', [CuttingController::class, 'materials'])->name('cuttings.materials')->middleware('permission:cuttings.view');
|
||||||
|
Route::get('transactions/{transaction}/items', [TransactionController::class, 'items'])->name('transactions.items')->middleware('permission:orders.view');
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Implementation Details
|
## Key Implementation Details
|
||||||
@ -54,8 +64,9 @@ ## Key Implementation Details
|
|||||||
- Frontend caches loaded items to avoid re-fetching on repeated expand/collapse
|
- Frontend caches loaded items to avoid re-fetching on repeated expand/collapse
|
||||||
- Loading state shown while items are being fetched
|
- Loading state shown while items are being fetched
|
||||||
- Show pages (e.g., `show.tsx`) pass full data directly to sub-row components
|
- Show pages (e.g., `show.tsx`) pass full data directly to sub-row components
|
||||||
|
- Transaction print feature fetches items on demand if not cached
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
- PHP syntax check: OK
|
- PHP syntax check: OK
|
||||||
- TypeScript: Only pre-existing errors in `use-*-draft.ts` files (unrelated)
|
- TypeScript: Only pre-existing errors in `use-*-draft.ts`, `create.tsx`, `edit.tsx` files (unrelated)
|
||||||
- Route cache cleared
|
- Route cache cleared
|
||||||
|
|||||||
@ -53,6 +53,9 @@ export type Transaction = {
|
|||||||
photo_url: string | null;
|
photo_url: string | null;
|
||||||
photo_conversion_url: string | null;
|
photo_conversion_url: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
items_count: number;
|
||||||
|
total_qty: number;
|
||||||
|
product_names: string | null;
|
||||||
created_by: {
|
created_by: {
|
||||||
id: number;
|
id: number;
|
||||||
user_profile: {
|
user_profile: {
|
||||||
@ -69,7 +72,7 @@ export type Transaction = {
|
|||||||
full_name: string;
|
full_name: string;
|
||||||
};
|
};
|
||||||
} | null;
|
} | null;
|
||||||
order_items: TransactionItem[];
|
order_items?: TransactionItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TransactionForEdit = {
|
export type TransactionForEdit = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Head, Link, router, usePage } from '@inertiajs/react';
|
import { Head, Link, router, usePage } from '@inertiajs/react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Bluetooth, Cable, Plus, Printer, Unplug } from 'lucide-react';
|
import { Bluetooth, Cable, Plus, Printer, Unplug } from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { CardTable } from '@/components/data-display';
|
import { CardTable } from '@/components/data-display';
|
||||||
import { DatePicker } from '@/components/inputs';
|
import { DatePicker } from '@/components/inputs';
|
||||||
@ -41,8 +41,9 @@ import {
|
|||||||
index as transactionIndex,
|
index as transactionIndex,
|
||||||
edit as transactionEdit,
|
edit as transactionEdit,
|
||||||
updateStatus as transactionUpdateStatus,
|
updateStatus as transactionUpdateStatus,
|
||||||
|
items as transactionItems,
|
||||||
} from '@/routes/admin/manage/transactions';
|
} from '@/routes/admin/manage/transactions';
|
||||||
import type { Transaction } from './columns';
|
import type { Transaction, TransactionItem } from './columns';
|
||||||
import { TransactionCardRow } from './transaction-card';
|
import { TransactionCardRow } from './transaction-card';
|
||||||
import { TransactionItemSubRow } from './transaction-sub-row';
|
import { TransactionItemSubRow } from './transaction-sub-row';
|
||||||
import { TransactionSummaryCard } from './transaction-summary-card';
|
import { TransactionSummaryCard } from './transaction-summary-card';
|
||||||
@ -100,7 +101,9 @@ export default function TransactionIndex({
|
|||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const { name: appName, address: appAddress } = usePage().props as unknown as { name: string; address: string };
|
const { name: appName, address: appAddress } = usePage().props as unknown as { name: string; address: string };
|
||||||
const [deleting, setDeleting] = useState<Transaction | null>(null);
|
const [deleting, setDeleting] = useState<Transaction | null>(null);
|
||||||
const expand = useCardTableExpand(true);
|
const [loadedItems, setLoadedItems] = useState<Record<number, TransactionItem[]>>({});
|
||||||
|
const [loadingItems, setLoadingItems] = useState<Record<number, boolean>>({});
|
||||||
|
const expand = useCardTableExpand(false);
|
||||||
const printer = useThermalPrinter();
|
const printer = useThermalPrinter();
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
@ -149,6 +152,29 @@ export default function TransactionIndex({
|
|||||||
[filterOptions.employees, filters.created_by_id],
|
[filterOptions.employees, filters.created_by_id],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fetchItems = useCallback((transaction: Transaction) => {
|
||||||
|
if (loadedItems[transaction.id] || loadingItems[transaction.id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingItems((prev) => ({ ...prev, [transaction.id]: true }));
|
||||||
|
|
||||||
|
fetch(transactionItems.url(transaction.id))
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
setLoadedItems((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[transaction.id]: data.items ?? [],
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoadedItems((prev) => ({ ...prev, [transaction.id]: [] }));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoadingItems((prev) => ({ ...prev, [transaction.id]: false }));
|
||||||
|
});
|
||||||
|
}, [loadedItems, loadingItems]);
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
if (!deleting) {
|
if (!deleting) {
|
||||||
return;
|
return;
|
||||||
@ -170,44 +196,62 @@ export default function TransactionIndex({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rupiah = (n: number) => `Rp ${n.toLocaleString('id-ID')}`;
|
const printWithItems = (items: TransactionItem[]) => {
|
||||||
|
const rupiah = (n: number) => `Rp ${n.toLocaleString('id-ID')}`;
|
||||||
|
|
||||||
const items = (transaction.order_items ?? []).map((item) => ({
|
const mappedItems = items.map((item) => ({
|
||||||
product_name: item.product_variant?.product?.name ?? '-',
|
product_name: item.product_variant?.product?.name ?? '-',
|
||||||
variant_name: item.product_variant?.name ?? '-',
|
variant_name: item.product_variant?.name ?? '-',
|
||||||
quantity: `${Number(item.quantity)}`,
|
quantity: `${Number(item.quantity)}`,
|
||||||
unit_price: rupiah(Number(item.unit_price)),
|
unit_price: rupiah(Number(item.unit_price)),
|
||||||
subtotal: rupiah(Number(item.subtotal)),
|
subtotal: rupiah(Number(item.subtotal)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cashierName =
|
const cashierName =
|
||||||
transaction.created_by?.user_profile?.full_name ?? '-';
|
transaction.created_by?.user_profile?.full_name ?? '-';
|
||||||
|
|
||||||
const data = encodeOrderReceipt(
|
const data = encodeOrderReceipt(
|
||||||
{
|
{
|
||||||
order_number: transaction.order_number,
|
order_number: transaction.order_number,
|
||||||
created_at: format(new Date(transaction.created_at), 'dd/MM/yyyy HH:mm'),
|
created_at: format(new Date(transaction.created_at), 'dd/MM/yyyy HH:mm'),
|
||||||
customer_name: transaction.customer?.name ?? null,
|
customer_name: transaction.customer?.name ?? null,
|
||||||
cashier_name: cashierName,
|
cashier_name: cashierName,
|
||||||
items,
|
items: mappedItems,
|
||||||
subtotal: rupiah(Number(transaction.subtotal)),
|
subtotal: rupiah(Number(transaction.subtotal)),
|
||||||
discount: rupiah(Number(transaction.discount)),
|
discount: rupiah(Number(transaction.discount)),
|
||||||
nego_price: transaction.nego_price != null && Number(transaction.nego_price) > 0
|
nego_price: transaction.nego_price != null && Number(transaction.nego_price) > 0
|
||||||
? rupiah(Number(transaction.nego_price))
|
? rupiah(Number(transaction.nego_price))
|
||||||
: null,
|
: null,
|
||||||
total_amount: rupiah(Number(transaction.total_amount)),
|
total_amount: rupiah(Number(transaction.total_amount)),
|
||||||
notes: transaction.notes ?? null,
|
notes: transaction.notes ?? null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
storeName: appName,
|
storeName: appName,
|
||||||
storeAddress: appAddress,
|
storeAddress: appAddress,
|
||||||
paperWidth,
|
paperWidth,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
printer.print(data).catch((err: Error) => {
|
printer.print(data).catch((err: Error) => {
|
||||||
alert(`Gagal mencetak: ${err.message}`);
|
alert(`Gagal mencetak: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedItems = loadedItems[transaction.id];
|
||||||
|
if (cachedItems) {
|
||||||
|
printWithItems(cachedItems);
|
||||||
|
} else {
|
||||||
|
fetch(transactionItems.url(transaction.id))
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
const items = data.items ?? [];
|
||||||
|
setLoadedItems((prev) => ({ ...prev, [transaction.id]: items }));
|
||||||
|
printWithItems(items);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Gagal memuat item transaksi.');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterToolbar = (
|
const filterToolbar = (
|
||||||
@ -495,7 +539,14 @@ export default function TransactionIndex({
|
|||||||
data={transactions.data}
|
data={transactions.data}
|
||||||
getItemKey={(t) => t.id}
|
getItemKey={(t) => t.id}
|
||||||
expandedKeys={expand.expandedKeys}
|
expandedKeys={expand.expandedKeys}
|
||||||
onToggleExpand={expand.toggleExpand}
|
onToggleExpand={(key) => {
|
||||||
|
const t = transactions.data.find((r) => r.id === key);
|
||||||
|
const isCurrentlyExpanded = expand.expandedKeys === 'all' || expand.expandedKeys.has(key);
|
||||||
|
if (t && !isCurrentlyExpanded) {
|
||||||
|
fetchItems(t);
|
||||||
|
}
|
||||||
|
expand.toggleExpand(key);
|
||||||
|
}}
|
||||||
searchValue={search}
|
searchValue={search}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
|
|
||||||
@ -528,7 +579,11 @@ export default function TransactionIndex({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
renderSubContent={(transaction) => (
|
renderSubContent={(transaction) => (
|
||||||
<TransactionItemSubRow transaction={transaction} />
|
<TransactionItemSubRow
|
||||||
|
transaction={transaction}
|
||||||
|
items={loadedItems[transaction.id] ?? []}
|
||||||
|
isLoading={loadingItems[transaction.id] ?? false}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -47,19 +47,10 @@ export function TransactionCardRow({
|
|||||||
onPrint,
|
onPrint,
|
||||||
}: TransactionCardRowParams) {
|
}: TransactionCardRowParams) {
|
||||||
const { can } = useCan();
|
const { can } = useCan();
|
||||||
const items = transaction.order_items ?? [];
|
const variantCount = transaction.items_count ?? 0;
|
||||||
const variantCount = items.length;
|
const totalQty = transaction.total_qty ?? 0;
|
||||||
const productNames = [
|
const productNamesStr = transaction.product_names ?? '';
|
||||||
...new Set(
|
const productNames = productNamesStr ? productNamesStr.split(', ') : [];
|
||||||
items
|
|
||||||
.map((item) => item.product_variant?.product?.name)
|
|
||||||
.filter(Boolean),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
const totalQty = items.reduce(
|
|
||||||
(sum, item) => sum + Number(item.quantity),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
const statusBadgeClass =
|
const statusBadgeClass =
|
||||||
STATUS_BADGE_CLASSES[transaction.status] ?? STATUS_BADGE_CLASSES.pending;
|
STATUS_BADGE_CLASSES[transaction.status] ?? STATUS_BADGE_CLASSES.pending;
|
||||||
|
|
||||||
|
|||||||
@ -9,10 +9,18 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { formatNumber } from '@/lib/format';
|
import { formatNumber } from '@/lib/format';
|
||||||
import { formatCurrency } from '@/lib/utils';
|
import { formatCurrency } from '@/lib/utils';
|
||||||
import type { Transaction } from './columns';
|
import type { Transaction, TransactionItem } from './columns';
|
||||||
|
|
||||||
export function TransactionItemSubRow({ transaction }: { transaction: Transaction }) {
|
export function TransactionItemSubRow({
|
||||||
const items = transaction.order_items ?? [];
|
transaction,
|
||||||
|
items: loadedItems,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
transaction: Transaction;
|
||||||
|
items: TransactionItem[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}) {
|
||||||
|
const items = loadedItems ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 overflow-x-auto">
|
<div className="space-y-4 overflow-x-auto">
|
||||||
@ -33,7 +41,16 @@ export function TransactionItemSubRow({ transaction }: { transaction: Transactio
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.length === 0 ? (
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={7}
|
||||||
|
className="text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
Memuat item...
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : items.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={7}
|
colSpan={7}
|
||||||
|
|||||||
@ -74,6 +74,7 @@
|
|||||||
|
|
||||||
Route::resource('transactions', TransactionController::class)->except(['show'])->middleware('permission:orders.view|orders.create|orders.update|orders.delete');
|
Route::resource('transactions', TransactionController::class)->except(['show'])->middleware('permission:orders.view|orders.create|orders.update|orders.delete');
|
||||||
Route::patch('transactions/{transaction}/status', [TransactionController::class, 'updateStatus'])->name('transactions.updateStatus')->middleware('permission:orders.update');
|
Route::patch('transactions/{transaction}/status', [TransactionController::class, 'updateStatus'])->name('transactions.updateStatus')->middleware('permission:orders.update');
|
||||||
|
Route::get('transactions/{transaction}/items', [TransactionController::class, 'items'])->name('transactions.items')->middleware('permission:orders.view');
|
||||||
|
|
||||||
Route::resource('restocks', RestockController::class)->except(['show'])->middleware('permission:restocks.view|restocks.create|restocks.update|restocks.delete');
|
Route::resource('restocks', RestockController::class)->except(['show'])->middleware('permission:restocks.view|restocks.create|restocks.update|restocks.delete');
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user