feat: 統一庫存管理分頁 UI 與寬度規範,並更新 SKILL 規範文件
This commit is contained in:
@@ -467,23 +467,27 @@ const handleSort = (field: string) => {
|
||||
import Pagination from "@/Components/shared/Pagination";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
|
||||
// 在表格下方
|
||||
// 在表格下方(底部工具列)
|
||||
<div className="mt-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span>每頁顯示</span>
|
||||
<SearchableSelect
|
||||
value={perPage}
|
||||
onValueChange={handlePerPageChange}
|
||||
options={[
|
||||
{ label: "10", value: "10" },
|
||||
{ label: "20", value: "20" },
|
||||
{ label: "50", value: "50" },
|
||||
{ label: "100", value: "100" }
|
||||
]}
|
||||
className="w-[80px] h-8"
|
||||
showSearch={false}
|
||||
/>
|
||||
<span>筆</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span>每頁顯示</span>
|
||||
<SearchableSelect
|
||||
value={perPage}
|
||||
onValueChange={handlePerPageChange}
|
||||
options={[
|
||||
{ label: "10", value: "10" },
|
||||
{ label: "20", value: "20" },
|
||||
{ label: "50", value: "50" },
|
||||
{ label: "100", value: "100" }
|
||||
]}
|
||||
className="w-[90px] h-8" // ✅ 統一使用 90px 寬度,避免「100」顯示不全
|
||||
showSearch={false}
|
||||
/>
|
||||
<span>筆</span>
|
||||
</div>
|
||||
{/* 總筆數顯示:統一放在每頁顯示右側,使用 text-gray-500 */}
|
||||
<span className="text-sm text-gray-500">共 {data.total} 筆紀錄</span>
|
||||
</div>
|
||||
<Pagination links={data.links} />
|
||||
</div>
|
||||
|
||||
@@ -123,7 +123,7 @@ class RoleController extends Controller
|
||||
$role->syncPermissions($validated['permissions']);
|
||||
}
|
||||
|
||||
return redirect()->route('roles.index')->with('success', '角色更新成功');
|
||||
return back()->with('success', '角色更新成功');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,8 +160,13 @@ class RoleController extends Controller
|
||||
$action = $parts[1] ?? '';
|
||||
|
||||
// 特定權限遷移邏輯
|
||||
if ($permission->name === 'inventory.transfer') {
|
||||
$group = 'warehouses'; // 調撥功能移至倉庫管理下
|
||||
if ($permission->name === 'inventory.view_cost') {
|
||||
$group = 'inventory';
|
||||
}
|
||||
|
||||
// 移除不再使用的權限選項
|
||||
if (in_array($permission->name, ['inventory.count', 'inventory.transfer'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($grouped[$group])) {
|
||||
@@ -175,7 +180,10 @@ class RoleController extends Controller
|
||||
$groupDefinitions = [
|
||||
'products' => '商品資料管理',
|
||||
'warehouses' => '倉庫管理',
|
||||
'inventory' => '庫存管理',
|
||||
'inventory' => '庫存資料管理',
|
||||
'inventory_count' => '庫存盤點管理',
|
||||
'inventory_adjust' => '庫存盤調管理',
|
||||
'inventory_transfer' => '庫存調撥管理',
|
||||
'vendors' => '廠商資料管理',
|
||||
'purchase_orders' => '採購單管理',
|
||||
'goods_receipts' => '進貨單管理',
|
||||
|
||||
@@ -39,7 +39,7 @@ class AdjustDocController extends Controller
|
||||
$query->where('warehouse_id', $request->warehouse_id);
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 15);
|
||||
$perPage = $request->input('per_page', 10);
|
||||
$docs = $query->orderByDesc('created_at')
|
||||
->paginate($perPage)
|
||||
->withQueryString()
|
||||
|
||||
@@ -37,7 +37,7 @@ class CountDocController extends Controller
|
||||
|
||||
$perPage = $request->input('per_page', 10);
|
||||
if (!in_array($perPage, [10, 20, 50, 100])) {
|
||||
$perPage = 15;
|
||||
$perPage = 10;
|
||||
}
|
||||
|
||||
$countQuery = function ($query) {
|
||||
|
||||
@@ -32,8 +32,10 @@ class TransferOrderController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 10);
|
||||
$orders = $query->orderByDesc('created_at')
|
||||
->paginate(15)
|
||||
->paginate($perPage)
|
||||
->withQueryString()
|
||||
->through(function ($order) {
|
||||
return [
|
||||
'id' => (string) $order->id,
|
||||
@@ -50,7 +52,7 @@ class TransferOrderController extends Controller
|
||||
return Inertia::render('Inventory/Transfer/Index', [
|
||||
'orders' => $orders,
|
||||
'warehouses' => Warehouse::all()->map(fn($w) => ['id' => (string)$w->id, 'name' => $w->name]),
|
||||
'filters' => $request->only(['warehouse_id']),
|
||||
'filters' => $request->only(['warehouse_id', 'per_page']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ class WarehouseController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
$perPage = $request->input('per_page', 10);
|
||||
if (!in_array($perPage, [10, 20, 50, 100])) {
|
||||
$perPage = 10;
|
||||
}
|
||||
|
||||
$warehouses = $query->withSum('inventories as book_stock', 'quantity') // 帳面庫存 = 所有庫存總和
|
||||
->withSum(['inventories as available_stock' => function ($query) {
|
||||
// 可用庫存 = 庫存 > 0 且 品質正常 且 (未過期 或 無效期) 且 倉庫類型不為瑕疵倉
|
||||
@@ -44,7 +49,7 @@ class WarehouseController extends Controller
|
||||
->whereRaw('(SELECT COALESCE(SUM(quantity), 0) FROM inventories WHERE warehouse_id = ss.warehouse_id AND product_id = ss.product_id) < ss.safety_stock');
|
||||
}])
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(10)
|
||||
->paginate($perPage)
|
||||
->withQueryString();
|
||||
|
||||
// 移除原本對 is_sellable 的手動修正邏輯,現在由 type 自動過濾
|
||||
@@ -67,7 +72,7 @@ class WarehouseController extends Controller
|
||||
return Inertia::render('Warehouse/Index', [
|
||||
'warehouses' => $warehouses,
|
||||
'totals' => $totals,
|
||||
'filters' => $request->only(['search']),
|
||||
'filters' => $request->only(['search', 'per_page']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ Route::middleware('auth')->group(function () {
|
||||
});
|
||||
|
||||
// 單位管理 - 需要商品權限
|
||||
Route::middleware('permission:products.create|products.edit')->group(function () {
|
||||
Route::post('/units', [UnitController::class, 'store'])->name('units.store');
|
||||
Route::put('/units/{unit}', [UnitController::class, 'update'])->name('units.update');
|
||||
Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->name('units.destroy');
|
||||
Route::middleware('permission:products.view')->group(function () {
|
||||
Route::post('/units', [UnitController::class, 'store'])->middleware('permission:products.create')->name('units.store');
|
||||
Route::put('/units/{unit}', [UnitController::class, 'update'])->middleware('permission:products.edit')->name('units.update');
|
||||
Route::delete('/units/{unit}', [UnitController::class, 'destroy'])->middleware('permission:products.delete')->name('units.destroy');
|
||||
});
|
||||
|
||||
// 商品管理
|
||||
@@ -75,37 +75,34 @@ Route::middleware('auth')->group(function () {
|
||||
});
|
||||
|
||||
// 庫存盤點 (Stock Counting) - Global
|
||||
Route::middleware('permission:inventory.view')->group(function () {
|
||||
Route::get('/inventory/count-docs', [CountDocController::class, 'index'])->name('inventory.count.index');
|
||||
Route::get('/inventory/count-docs/{doc}', [CountDocController::class, 'show'])->name('inventory.count.show');
|
||||
|
||||
Route::middleware('permission:inventory.adjust')->group(function () {
|
||||
Route::post('/inventory/count-docs', [CountDocController::class, 'store'])->name('inventory.count.store');
|
||||
Route::put('/inventory/count-docs/{doc}', [CountDocController::class, 'update'])->name('inventory.count.update');
|
||||
Route::delete('/inventory/count-docs/{doc}', [CountDocController::class, 'destroy'])->name('inventory.count.destroy');
|
||||
Route::put('/inventory/count-docs/{doc}/reopen', [CountDocController::class, 'reopen'])->name('inventory.count.reopen');
|
||||
});
|
||||
Route::get('/inventory/count-docs/{doc}/print', [CountDocController::class, 'print'])->name('inventory.count.print');
|
||||
Route::middleware('permission:inventory_count.view')->group(function () {
|
||||
Route::get('/inventory/count-docs', [CountDocController::class, 'index'])->name('inventory.count.index');
|
||||
Route::get('/inventory/count-docs/{doc}', [CountDocController::class, 'show'])->name('inventory.count.show');
|
||||
Route::get('/inventory/count-docs/{doc}/print', [CountDocController::class, 'print'])->name('inventory.count.print');
|
||||
});
|
||||
Route::post('/inventory/count-docs', [CountDocController::class, 'store'])->middleware('permission:inventory_count.create')->name('inventory.count.store');
|
||||
Route::put('/inventory/count-docs/{doc}', [CountDocController::class, 'update'])->middleware('permission:inventory_count.edit')->name('inventory.count.update');
|
||||
Route::delete('/inventory/count-docs/{doc}', [CountDocController::class, 'destroy'])->middleware('permission:inventory_count.delete')->name('inventory.count.destroy');
|
||||
Route::put('/inventory/count-docs/{doc}/reopen', [CountDocController::class, 'reopen'])->middleware('permission:inventory_count.edit')->name('inventory.count.reopen');
|
||||
|
||||
// 庫存盤調 (Stock Adjustment) - Global
|
||||
Route::middleware('permission:inventory.adjust')->group(function () {
|
||||
Route::get('/inventory/adjust-docs', [AdjustDocController::class, 'index'])->name('inventory.adjust.index');
|
||||
Route::get('/inventory/adjust-docs/get-pending-counts', [AdjustDocController::class, 'getPendingCounts'])->name('inventory.adjust.pending-counts');
|
||||
Route::get('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'show'])->name('inventory.adjust.show');
|
||||
Route::post('/inventory/adjust-docs', [AdjustDocController::class, 'store'])->name('inventory.adjust.store');
|
||||
Route::put('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'update'])->name('inventory.adjust.update');
|
||||
Route::delete('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'destroy'])->name('inventory.adjust.destroy');
|
||||
Route::middleware('permission:inventory_adjust.view')->group(function () {
|
||||
Route::get('/inventory/adjust-docs', [AdjustDocController::class, 'index'])->name('inventory.adjust.index');
|
||||
Route::get('/inventory/adjust-docs/get-pending-counts', [AdjustDocController::class, 'getPendingCounts'])->name('inventory.adjust.pending-counts');
|
||||
Route::get('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'show'])->name('inventory.adjust.show');
|
||||
});
|
||||
Route::post('/inventory/adjust-docs', [AdjustDocController::class, 'store'])->middleware('permission:inventory_adjust.create')->name('inventory.adjust.store');
|
||||
Route::put('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'update'])->middleware('permission:inventory_adjust.edit')->name('inventory.adjust.update');
|
||||
Route::delete('/inventory/adjust-docs/{doc}', [AdjustDocController::class, 'destroy'])->middleware('permission:inventory_adjust.delete')->name('inventory.adjust.destroy');
|
||||
|
||||
// 撥補單/調撥單 (Transfer Order) - Global
|
||||
Route::middleware('permission:inventory.transfer')->group(function () {
|
||||
Route::middleware('permission:inventory_transfer.view')->group(function () {
|
||||
Route::get('/inventory/transfer-orders', [TransferOrderController::class, 'index'])->name('inventory.transfer.index');
|
||||
Route::get('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'show'])->name('inventory.transfer.show');
|
||||
Route::post('/inventory/transfer-orders', [TransferOrderController::class, 'store'])->name('inventory.transfer.store');
|
||||
Route::put('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'update'])->name('inventory.transfer.update');
|
||||
Route::delete('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'destroy'])->name('inventory.transfer.destroy');
|
||||
});
|
||||
Route::post('/inventory/transfer-orders', [TransferOrderController::class, 'store'])->middleware('permission:inventory_transfer.create')->name('inventory.transfer.store');
|
||||
Route::put('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'update'])->middleware('permission:inventory_transfer.edit')->name('inventory.transfer.update');
|
||||
Route::delete('/inventory/transfer-orders/{order}', [TransferOrderController::class, 'destroy'])->middleware('permission:inventory_transfer.delete')->name('inventory.transfer.destroy');
|
||||
Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories'])
|
||||
->middleware('permission:inventory.view')
|
||||
->name('api.warehouses.inventories');
|
||||
|
||||
@@ -35,11 +35,26 @@ class PermissionSeeder extends Seeder
|
||||
// 庫存管理
|
||||
'inventory.view',
|
||||
'inventory.view_cost', // 查看成本與價值
|
||||
'inventory.adjust',
|
||||
'inventory.count', // 庫存盤點
|
||||
'inventory.transfer', // 庫存調撥
|
||||
'inventory.delete',
|
||||
|
||||
// 庫存盤點 (Stock Counting)
|
||||
'inventory_count.view',
|
||||
'inventory_count.create',
|
||||
'inventory_count.edit',
|
||||
'inventory_count.delete',
|
||||
|
||||
// 庫存調整 (Stock Adjustment)
|
||||
'inventory_adjust.view',
|
||||
'inventory_adjust.create',
|
||||
'inventory_adjust.edit',
|
||||
'inventory_adjust.delete',
|
||||
|
||||
// 庫存調撥 (Stock Transfer)
|
||||
'inventory_transfer.view',
|
||||
'inventory_transfer.create',
|
||||
'inventory_transfer.edit',
|
||||
'inventory_transfer.delete',
|
||||
|
||||
// 進貨單管理
|
||||
'goods_receipts.view',
|
||||
'goods_receipts.create',
|
||||
@@ -118,7 +133,10 @@ class PermissionSeeder extends Seeder
|
||||
'products.view', 'products.create', 'products.edit', 'products.delete',
|
||||
'purchase_orders.view', 'purchase_orders.create', 'purchase_orders.edit',
|
||||
'purchase_orders.delete', 'purchase_orders.publish',
|
||||
'inventory.view', 'inventory.view_cost', 'inventory.adjust', 'inventory.transfer', 'inventory.delete',
|
||||
'inventory.view', 'inventory.view_cost', 'inventory.delete',
|
||||
'inventory_count.view', 'inventory_count.create', 'inventory_count.edit', 'inventory_count.delete',
|
||||
'inventory_adjust.view', 'inventory_adjust.create', 'inventory_adjust.edit', 'inventory_adjust.delete',
|
||||
'inventory_transfer.view', 'inventory_transfer.create', 'inventory_transfer.edit', 'inventory_transfer.delete',
|
||||
'goods_receipts.view', 'goods_receipts.create', 'goods_receipts.edit', 'goods_receipts.delete',
|
||||
'production_orders.view', 'production_orders.create', 'production_orders.edit', 'production_orders.delete',
|
||||
'recipes.view', 'recipes.create', 'recipes.edit', 'recipes.delete',
|
||||
@@ -134,7 +152,10 @@ class PermissionSeeder extends Seeder
|
||||
// warehouse-manager 管理庫存與倉庫
|
||||
$warehouseManager->givePermissionTo([
|
||||
'products.view',
|
||||
'inventory.view', 'inventory.adjust', 'inventory.count', 'inventory.transfer', 'inventory.delete',
|
||||
'inventory.view', 'inventory.delete',
|
||||
'inventory_count.view', 'inventory_count.create', 'inventory_count.edit', 'inventory_count.delete',
|
||||
'inventory_adjust.view', 'inventory_adjust.create', 'inventory_adjust.edit', 'inventory_adjust.delete',
|
||||
'inventory_transfer.view', 'inventory_transfer.create', 'inventory_transfer.edit', 'inventory_transfer.delete',
|
||||
'goods_receipts.view', 'goods_receipts.create', 'goods_receipts.edit', 'goods_receipts.delete',
|
||||
'production_orders.view', 'production_orders.create', 'production_orders.edit',
|
||||
'warehouses.view', 'warehouses.create', 'warehouses.edit',
|
||||
|
||||
@@ -105,21 +105,21 @@ export default function AuthenticatedLayout({
|
||||
label: "庫存盤點",
|
||||
icon: <ClipboardCheck className="h-4 w-4" />,
|
||||
route: "/inventory/count-docs",
|
||||
permission: "inventory.view",
|
||||
permission: "inventory_count.view",
|
||||
},
|
||||
{
|
||||
id: "stock-adjustment",
|
||||
label: "庫存盤調",
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
route: "/inventory/adjust-docs",
|
||||
permission: "inventory.adjust",
|
||||
permission: "inventory_adjust.view",
|
||||
},
|
||||
{
|
||||
id: "stock-transfer",
|
||||
label: "庫存調撥",
|
||||
icon: <ArrowLeftRight className="h-4 w-4" />,
|
||||
route: "/inventory/transfer-orders",
|
||||
permission: "inventory.transfer",
|
||||
permission: "inventory_transfer.view",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -70,16 +70,20 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
|
||||
const translateAction = (permissionName: string) => {
|
||||
const parts = permissionName.split('.');
|
||||
if (parts.length < 2) return permissionName;
|
||||
const action = parts[1];
|
||||
const action = parts[parts.length - 1];
|
||||
|
||||
const map: Record<string, string> = {
|
||||
'view': '檢視',
|
||||
'create': '新增',
|
||||
'edit': '編輯',
|
||||
'delete': '刪除',
|
||||
'publish': '發布',
|
||||
'publish': '發佈',
|
||||
'adjust': '調整',
|
||||
'transfer': '調撥',
|
||||
'count': '盤點',
|
||||
// 'inventory_count': '盤點', // Hide prefix
|
||||
// 'inventory_adjust': '盤調', // Hide prefix
|
||||
// 'inventory_transfer': '調撥', // Hide prefix
|
||||
'safety_stock': '安全庫存設定',
|
||||
'export': '匯出',
|
||||
'complete': '完成',
|
||||
@@ -88,7 +92,19 @@ export default function RoleEdit({ role, groupedPermissions, currentPermissions
|
||||
'activate': '啟用/停用',
|
||||
};
|
||||
|
||||
return map[action] || action;
|
||||
const actionText = map[action] || action;
|
||||
|
||||
// 處理多段式權限 (例如 inventory_count.view)
|
||||
if (parts.length >= 2) {
|
||||
const middleKey = parts[parts.length - 2];
|
||||
|
||||
// 如果中間那段有翻譯且不等於動作本身,則顯示為 "標籤: 動作"
|
||||
if (map[middleKey] && middleKey !== action) {
|
||||
return `${map[middleKey]}: ${actionText}`;
|
||||
}
|
||||
}
|
||||
|
||||
return actionText;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, useForm, router, Link } from '@inertiajs/react';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -71,7 +72,7 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState(filters.search || '');
|
||||
const [warehouseId, setWarehouseId] = useState(filters.warehouse_id || '');
|
||||
const [perPage, setPerPage] = useState(filters.per_page || '15');
|
||||
const [perPage, setPerPage] = useState(filters.per_page || '10');
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||
|
||||
// For Count Doc Selection
|
||||
@@ -115,9 +116,12 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
debouncedFilter({ search: searchQuery, warehouse_id: val, per_page: perPage });
|
||||
};
|
||||
|
||||
const handlePerPageChange = (val: string) => {
|
||||
setPerPage(val);
|
||||
debouncedFilter({ search: searchQuery, warehouse_id: warehouseId, per_page: val });
|
||||
const handlePerPageChange = (value: string) => {
|
||||
setPerPage(value);
|
||||
router.get(route('inventory.adjust.index'),
|
||||
{ ...filters, search: searchQuery, warehouse_id: warehouseId, per_page: value, page: 1 },
|
||||
{ preserveState: false, replace: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
|
||||
const confirmDelete = (id: string, e: React.MouseEvent) => {
|
||||
@@ -134,6 +138,8 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
}
|
||||
};
|
||||
|
||||
const { can } = usePermission();
|
||||
|
||||
const { data, setData, post, processing, reset } = useForm({
|
||||
count_doc_id: null as string | null,
|
||||
warehouse_id: '',
|
||||
@@ -229,7 +235,7 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Can permission="inventory.adjust">
|
||||
<Can permission="inventory_adjust.create">
|
||||
<Button
|
||||
className="flex-1 md:flex-none button-filled-primary h-9"
|
||||
onClick={() => setIsDialogOpen(true)}
|
||||
@@ -286,30 +292,55 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
<TableCell className="text-gray-500 text-sm">{doc.posted_at || '-'}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Link href={route('inventory.adjust.show', [doc.id])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
title={doc.status === 'posted' ? '查閱' : '編輯'}
|
||||
>
|
||||
{doc.status === 'posted' ? (
|
||||
<Eye className="w-4 h-4" />
|
||||
) : (
|
||||
<Pencil className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
</Link>
|
||||
{(() => {
|
||||
const isDraft = doc.status === 'draft';
|
||||
const canEdit = can('inventory_adjust.edit');
|
||||
const canView = can('inventory_adjust.view');
|
||||
|
||||
if (isDraft && canEdit) {
|
||||
return (
|
||||
<Link href={route('inventory.adjust.show', [doc.id])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
title="編輯"
|
||||
>
|
||||
<Pencil className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (canView) {
|
||||
return (
|
||||
<Link href={route('inventory.adjust.show', [doc.id])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
title="查閱"
|
||||
>
|
||||
<Eye className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
|
||||
{doc.status === 'draft' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-error"
|
||||
title="刪除"
|
||||
onClick={(e) => confirmDelete(doc.id, e)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Can permission="inventory_adjust.delete">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-error"
|
||||
title="刪除"
|
||||
onClick={(e) => confirmDelete(doc.id, e)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
</Can>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -328,12 +359,12 @@ export default function Index({ docs, warehouses, filters }: { docs: DocsPaginat
|
||||
value={perPage}
|
||||
onValueChange={handlePerPageChange}
|
||||
options={[
|
||||
{ label: "15", value: "15" },
|
||||
{ label: "30", value: "30" },
|
||||
{ label: "10", value: "10" },
|
||||
{ label: "20", value: "20" },
|
||||
{ label: "50", value: "50" },
|
||||
{ label: "100", value: "100" }
|
||||
]}
|
||||
className="w-[100px] h-8"
|
||||
className="w-[90px] h-8"
|
||||
showSearch={false}
|
||||
/>
|
||||
<span>筆</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, Link, useForm, router } from '@inertiajs/react';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -65,7 +66,10 @@ interface AdjDoc {
|
||||
}
|
||||
|
||||
export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
const { can } = usePermission();
|
||||
const isDraft = doc.status === 'draft';
|
||||
const canEdit = can('inventory_adjust.edit');
|
||||
const isReadOnly = !isDraft || !canEdit;
|
||||
|
||||
// Main Form using Inertia useForm
|
||||
const { data, setData, put, delete: destroy, processing } = useForm({
|
||||
@@ -183,7 +187,6 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
setData('action', 'save');
|
||||
put(route('inventory.adjust.update', [doc.id]), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => toast.success("草稿儲存成功"),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -199,15 +202,12 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
} as any, {
|
||||
onSuccess: () => {
|
||||
setIsPostDialogOpen(false);
|
||||
toast.success("盤調單過帳成功");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
destroy(route('inventory.adjust.destroy', [doc.id]), {
|
||||
onSuccess: () => toast.success("盤調單已刪除"),
|
||||
});
|
||||
destroy(route('inventory.adjust.destroy', [doc.id]));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -263,65 +263,69 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isDraft && (
|
||||
<Can permission="inventory.adjust">
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" disabled={processing} className="button-outlined-error">
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
刪除
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要刪除此盤調單嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
此動作將會永久移除本張草稿,且無法復原。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="button-filled-error">確認刪除</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{!isReadOnly && (
|
||||
<>
|
||||
<Can permission="inventory_adjust.delete">
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" disabled={processing} className="button-outlined-error">
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
刪除
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要刪除此盤調單嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
此動作將會永久移除本張草稿,且無法復原。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDelete} className="button-filled-error">確認刪除</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Can>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
onClick={handleSave}
|
||||
disabled={processing}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
儲存
|
||||
</Button>
|
||||
<Can permission="inventory_adjust.edit">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
onClick={handleSave}
|
||||
disabled={processing}
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
儲存
|
||||
</Button>
|
||||
|
||||
<AlertDialog open={isPostDialogOpen} onOpenChange={setIsPostDialogOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
className="button-filled-primary"
|
||||
disabled={processing || data.items.length === 0}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
確認過帳
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要過帳嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
過帳後庫存將立即根據調整數量進行增減,且無法再修改此盤調單。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handlePost} className="button-filled-primary">確認過帳</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Can>
|
||||
<AlertDialog open={isPostDialogOpen} onOpenChange={setIsPostDialogOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
className="button-filled-primary"
|
||||
disabled={processing || data.items.length === 0}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
確認過帳
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要過帳嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
過帳後庫存將立即根據調整數量進行增減,且無法再修改此盤調單。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handlePost} className="button-filled-primary">確認過帳</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Can>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,7 +336,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pb-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-bold text-grey-500 uppercase tracking-wider font-semibold">調整原因</Label>
|
||||
{isDraft ? (
|
||||
{!isReadOnly ? (
|
||||
<Input
|
||||
value={data.reason}
|
||||
onChange={e => setData('reason', e.target.value)}
|
||||
@@ -345,7 +349,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs font-bold text-grey-500 uppercase tracking-wider font-semibold">備註說明</Label>
|
||||
{isDraft ? (
|
||||
{!isReadOnly ? (
|
||||
<Input
|
||||
value={data.remarks}
|
||||
onChange={e => setData('remarks', e.target.value)}
|
||||
@@ -367,7 +371,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
請輸入各項商品的實盤與帳面之差異數量。正數為增加,負數為減少。
|
||||
</p>
|
||||
</div>
|
||||
{isDraft && !doc.count_doc_id && (
|
||||
{!isReadOnly && !doc.count_doc_id && (
|
||||
<Dialog open={isProductDialogOpen} onOpenChange={setIsProductDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="button-outlined-primary">
|
||||
@@ -505,13 +509,13 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
<TableHead className="w-32 text-right font-medium text-grey-600">調整前庫存</TableHead>
|
||||
<TableHead className="w-40 text-right font-medium text-grey-600">調整數量 (+/-)</TableHead>
|
||||
<TableHead className="font-medium text-grey-600">備註</TableHead>
|
||||
{isDraft && <TableHead className="w-[50px]"></TableHead>}
|
||||
{!isReadOnly && <TableHead className="w-[50px]"></TableHead>}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={isDraft ? 8 : 7} className="h-32 text-center text-grey-400">
|
||||
<TableCell colSpan={!isReadOnly ? 8 : 7} className="h-32 text-center text-grey-400">
|
||||
尚未加入任何項目
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -534,7 +538,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
{item.qty_before}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{isDraft ? (
|
||||
{!isReadOnly ? (
|
||||
<div className="flex justify-end pr-2">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -550,7 +554,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isDraft ? (
|
||||
{!isReadOnly ? (
|
||||
<Input
|
||||
className="h-9 text-sm"
|
||||
value={item.notes || ''}
|
||||
@@ -561,7 +565,7 @@ export default function Show({ doc }: { auth: any, doc: AdjDoc }) {
|
||||
<span className="text-grey-600 text-sm">{item.notes || '-'}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
{isDraft && !doc.count_doc_id && (
|
||||
{!isReadOnly && !doc.count_doc_id && (
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, Link, useForm, router } from '@inertiajs/react';
|
||||
import { Head, Link, useForm, router, usePage } from '@inertiajs/react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { debounce } from "lodash";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import {
|
||||
@@ -54,6 +55,8 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
||||
remarks: '',
|
||||
});
|
||||
|
||||
const { can } = usePermission();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState(filters.search || "");
|
||||
const [warehouseFilter, setWarehouseFilter] = useState(filters.warehouse_id || "all");
|
||||
const [perPage, setPerPage] = useState(filters.per_page || "10");
|
||||
@@ -207,7 +210,7 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Can permission="inventory.view">
|
||||
<Can permission="inventory_count.create">
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex-1 md:flex-none button-filled-primary">
|
||||
@@ -302,22 +305,46 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
||||
<TableCell className="text-sm">{doc.created_by}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Can permission="inventory.view">
|
||||
<Link href={route('inventory.count.show', [doc.id])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
title={['completed', 'adjusted'].includes(doc.status) ? '查閱' : '盤點'}
|
||||
>
|
||||
{['completed', 'adjusted'].includes(doc.status) ? (
|
||||
<Eye className="w-4 h-4 ml-0.5" />
|
||||
) : (
|
||||
<Pencil className="w-4 h-4 ml-0.5" />
|
||||
)}
|
||||
</Button>
|
||||
</Link>
|
||||
{!['completed', 'adjusted'].includes(doc.status) && (
|
||||
{/* Action Button Logic: Prefer Edit if allowed and status is active, otherwise fallback to View if allowed */}
|
||||
{(() => {
|
||||
const isEditable = !['completed', 'adjusted'].includes(doc.status);
|
||||
const canEdit = can('inventory_count.edit');
|
||||
const canView = can('inventory_count.view');
|
||||
|
||||
if (isEditable && canEdit) {
|
||||
return (
|
||||
<Link href={route('inventory.count.show', [doc.id])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
title="盤點"
|
||||
>
|
||||
<Pencil className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if (canView) {
|
||||
return (
|
||||
<Link href={route('inventory.count.show', [doc.id])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-primary"
|
||||
title="查閱"
|
||||
>
|
||||
<Eye className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})()}
|
||||
{!['completed', 'adjusted'].includes(doc.status) && (
|
||||
<Can permission="inventory_count.delete">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -327,8 +354,8 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
||||
>
|
||||
<Trash2 className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
)}
|
||||
</Can>
|
||||
</Can>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -351,7 +378,7 @@ export default function Index({ auth, docs, warehouses, filters }: any) {
|
||||
{ label: "50", value: "50" },
|
||||
{ label: "100", value: "100" }
|
||||
]}
|
||||
className="w-[100px] h-8"
|
||||
className="w-[90px] h-8"
|
||||
showSearch={false}
|
||||
/>
|
||||
<span>筆</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
||||
import { Head, useForm, Link, router } from '@inertiajs/react'; // Added Link
|
||||
import { Head, useForm, Link, router } from '@inertiajs/react';
|
||||
import { usePermission } from '@/hooks/usePermission'; // Added Link
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -61,7 +62,10 @@ export default function Show({ doc }: any) {
|
||||
});
|
||||
}
|
||||
|
||||
const { can } = usePermission();
|
||||
const isCompleted = ['completed', 'adjusted'].includes(doc.status);
|
||||
const canEdit = can('inventory_count.edit');
|
||||
const isReadOnly = isCompleted || !canEdit;
|
||||
|
||||
// Calculate progress
|
||||
const totalItems = doc.items.length;
|
||||
@@ -155,7 +159,7 @@ export default function Show({ doc }: any) {
|
||||
|
||||
{!isCompleted && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Can permission="inventory.view">
|
||||
<Can permission="inventory_count.delete">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" disabled={processing} className="button-outlined-error">
|
||||
@@ -176,7 +180,9 @@ export default function Show({ doc }: any) {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Can>
|
||||
|
||||
<Can permission="inventory_count.edit">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -261,7 +267,7 @@ export default function Show({ doc }: any) {
|
||||
<TableCell className="text-sm font-mono">{item.batch_number || '-'}</TableCell>
|
||||
<TableCell className="text-right font-medium">{item.system_qty.toFixed(0)}</TableCell>
|
||||
<TableCell className="text-right px-1 py-3">
|
||||
{isCompleted ? (
|
||||
{isReadOnly ? (
|
||||
<span className="font-semibold mr-2">{item.counted_qty}</span>
|
||||
) : (
|
||||
<Input
|
||||
@@ -290,7 +296,7 @@ export default function Show({ doc }: any) {
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-500">{item.unit || item.unit_name}</TableCell>
|
||||
<TableCell className="px-1">
|
||||
{isCompleted ? (
|
||||
{isReadOnly ? (
|
||||
<span className="text-sm text-gray-600">{item.notes}</span>
|
||||
) : (
|
||||
<Input
|
||||
|
||||
@@ -226,7 +226,7 @@ export default function Index({ warehouses, orders, filters }: any) {
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Can permission="inventory.view">
|
||||
<Can permission="inventory_transfer.create">
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex-1 md:flex-none button-filled-primary">
|
||||
@@ -320,7 +320,7 @@ export default function Index({ warehouses, orders, filters }: any) {
|
||||
<TableCell className="text-sm">{order.created_by}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||
<Can permission="inventory.view">
|
||||
<Can permission="inventory_transfer.view">
|
||||
<Link href={route('inventory.transfer.show', [order.id])}>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -336,15 +336,17 @@ export default function Index({ warehouses, orders, filters }: any) {
|
||||
</Button>
|
||||
</Link>
|
||||
{order.status === 'draft' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-error"
|
||||
title="刪除"
|
||||
onClick={() => confirmDelete(order.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
<Can permission="inventory_transfer.delete">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="button-outlined-error"
|
||||
title="刪除"
|
||||
onClick={() => confirmDelete(order.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 ml-0.5" />
|
||||
</Button>
|
||||
</Can>
|
||||
)}
|
||||
</Can>
|
||||
</div>
|
||||
@@ -369,7 +371,7 @@ export default function Index({ warehouses, orders, filters }: any) {
|
||||
{ label: "50", value: "50" },
|
||||
{ label: "100", value: "100" }
|
||||
]}
|
||||
className="w-[100px] h-8"
|
||||
className="w-[90px] h-8"
|
||||
showSearch={false}
|
||||
/>
|
||||
<span>筆</span>
|
||||
|
||||
@@ -288,7 +288,7 @@ export default function ProductManagement({ products, categories, units, filters
|
||||
{ label: "50", value: "50" },
|
||||
{ label: "100", value: "100" }
|
||||
]}
|
||||
className="w-[100px] h-8"
|
||||
className="w-[90px] h-8"
|
||||
showSearch={false}
|
||||
/>
|
||||
<span>筆</span>
|
||||
|
||||
@@ -38,12 +38,14 @@ interface PageProps {
|
||||
};
|
||||
filters: {
|
||||
search?: string;
|
||||
per_page?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function WarehouseIndex({ warehouses, totals, filters }: PageProps) {
|
||||
// 篩選狀態
|
||||
const [searchTerm, setSearchTerm] = useState(filters.search || "");
|
||||
const [perPage, setPerPage] = useState(filters.per_page || '10');
|
||||
|
||||
// 對話框狀態
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
@@ -58,13 +60,21 @@ export default function WarehouseIndex({ warehouses, totals, filters }: PageProp
|
||||
// 搜尋處理
|
||||
const handleSearch = (term: string) => {
|
||||
setSearchTerm(term);
|
||||
router.get(route('warehouses.index'), { search: term }, {
|
||||
router.get(route('warehouses.index'), { search: term, per_page: perPage }, {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePerPageChange = (value: string) => {
|
||||
setPerPage(value);
|
||||
router.get(route('warehouses.index'),
|
||||
{ ...filters, per_page: value, page: 1 },
|
||||
{ preserveState: false, replace: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
|
||||
// 導航處理
|
||||
const handleViewInventory = (warehouseId: string | number) => {
|
||||
router.get(`/warehouses/${warehouseId}/inventory`);
|
||||
@@ -193,7 +203,7 @@ export default function WarehouseIndex({ warehouses, totals, filters }: PageProp
|
||||
|
||||
{/* 操作按鈕 */}
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Can permission="inventory.transfer">
|
||||
<Can permission="inventory_count.create">
|
||||
<Button
|
||||
onClick={handleAddTransferOrder}
|
||||
className="flex-1 md:flex-initial button-outlined-primary"
|
||||
@@ -238,7 +248,26 @@ export default function WarehouseIndex({ warehouses, totals, filters }: PageProp
|
||||
)}
|
||||
|
||||
{/* 分頁 */}
|
||||
<div className="mt-6">
|
||||
<div className="mt-6 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<span>每頁顯示</span>
|
||||
<SearchableSelect
|
||||
value={perPage}
|
||||
onValueChange={handlePerPageChange}
|
||||
options={[
|
||||
{ label: "10", value: "10" },
|
||||
{ label: "20", value: "20" },
|
||||
{ label: "50", value: "50" },
|
||||
{ label: "100", value: "100" }
|
||||
]}
|
||||
className="w-[90px] h-8"
|
||||
showSearch={false}
|
||||
/>
|
||||
<span>筆</span>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">共 {warehouses.total} 筆紀錄</span>
|
||||
</div>
|
||||
<Pagination links={warehouses.links} />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user