feat: 更新庫存報表、銷售匯入及採購單相關功能
This commit is contained in:
@@ -12,7 +12,10 @@ import {
|
||||
ArrowUpFromLine,
|
||||
ArrowDownToLine,
|
||||
ArrowRightLeft,
|
||||
TrendingUp
|
||||
TrendingUp,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown
|
||||
} from 'lucide-react';
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, Link, router } from "@inertiajs/react";
|
||||
@@ -29,6 +32,12 @@ import Pagination from "@/Components/shared/Pagination";
|
||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||
import { Can } from "@/Components/Permission/Can";
|
||||
import { PageProps } from "@/types/global";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/Components/ui/tooltip";
|
||||
|
||||
interface ReportData {
|
||||
product_code: string;
|
||||
@@ -37,6 +46,8 @@ interface ReportData {
|
||||
product_id: number;
|
||||
inbound_qty: number;
|
||||
outbound_qty: number;
|
||||
transfer_in_qty: number;
|
||||
transfer_out_qty: number;
|
||||
adjust_qty: number;
|
||||
net_change: number;
|
||||
}
|
||||
@@ -44,6 +55,8 @@ interface ReportData {
|
||||
interface SummaryData {
|
||||
total_inbound: number;
|
||||
total_outbound: number;
|
||||
total_transfer_in: number;
|
||||
total_transfer_out: number;
|
||||
total_adjust: number;
|
||||
total_net_change: number;
|
||||
}
|
||||
@@ -67,6 +80,8 @@ interface InventoryReportProps extends PageProps {
|
||||
category_id: string;
|
||||
search: string;
|
||||
per_page?: number;
|
||||
sort_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,7 +135,7 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
search: search,
|
||||
per_page: perPage,
|
||||
},
|
||||
{ preserveState: true }
|
||||
{ preserveState: true, preserveScroll: true }
|
||||
);
|
||||
}, [dateStart, dateEnd, warehouseId, categoryId, search, perPage]);
|
||||
|
||||
@@ -136,7 +151,7 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
search: search,
|
||||
per_page: value,
|
||||
},
|
||||
{ preserveState: true }
|
||||
{ preserveState: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -161,10 +176,51 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
warehouse_id: warehouseId === "all" ? "" : warehouseId,
|
||||
category_id: categoryId === "all" ? "" : categoryId,
|
||||
search: search,
|
||||
sort_by: filters.sort_by,
|
||||
sort_order: filters.sort_order,
|
||||
};
|
||||
window.location.href = route("inventory.report.export", query);
|
||||
};
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
let newSortBy: string | undefined = field;
|
||||
let newSortOrder: 'asc' | 'desc' | undefined = 'asc';
|
||||
|
||||
if (filters.sort_by === field) {
|
||||
if (filters.sort_order === 'asc') {
|
||||
newSortOrder = 'desc';
|
||||
} else {
|
||||
newSortBy = undefined;
|
||||
newSortOrder = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
router.get(
|
||||
route("inventory.report.index"),
|
||||
{
|
||||
date_from: dateStart,
|
||||
date_to: dateEnd,
|
||||
warehouse_id: warehouseId === "all" ? "" : warehouseId,
|
||||
category_id: categoryId === "all" ? "" : categoryId,
|
||||
search: search,
|
||||
per_page: perPage,
|
||||
sort_by: newSortBy,
|
||||
sort_order: newSortOrder,
|
||||
},
|
||||
{ preserveState: true, preserveScroll: true }
|
||||
);
|
||||
};
|
||||
|
||||
const SortIcon = ({ field }: { field: string }) => {
|
||||
if (filters.sort_by !== field) {
|
||||
return <ArrowUpDown className="h-4 w-4 text-gray-300 ml-1" />;
|
||||
}
|
||||
if (filters.sort_order === "asc") {
|
||||
return <ArrowUp className="h-4 w-4 text-primary-main ml-1" />;
|
||||
}
|
||||
return <ArrowDown className="h-4 w-4 text-primary-main ml-1" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout breadcrumbs={[{ label: "報表管理", href: "#" }, { label: "庫存報表", href: route("inventory.report.index"), isPage: true }]}>
|
||||
<Head title="庫存報表" />
|
||||
@@ -271,7 +327,7 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
{/* Detailed Filters row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 items-end">
|
||||
{/* Warehouse & Category */}
|
||||
<div className="md:col-span-4 space-y-1">
|
||||
<div className="md:col-span-3 space-y-1">
|
||||
<Label className="text-xs text-grey-2 font-medium">倉庫</Label>
|
||||
<SearchableSelect
|
||||
value={warehouseId}
|
||||
@@ -281,7 +337,7 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
placeholder="選擇倉庫..."
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-4 space-y-1">
|
||||
<div className="md:col-span-3 space-y-1">
|
||||
<Label className="text-xs text-grey-2 font-medium">分類</Label>
|
||||
<SearchableSelect
|
||||
value={categoryId}
|
||||
@@ -293,7 +349,7 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="md:col-span-4 space-y-1">
|
||||
<div className="md:col-span-3 space-y-1">
|
||||
<Label className="text-xs text-grey-2 font-medium">關鍵字</Label>
|
||||
<Input
|
||||
placeholder="搜尋商品..."
|
||||
@@ -303,64 +359,124 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-end border-t border-grey-4 pt-5 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClearFilters}
|
||||
className="flex items-center gap-2 button-outlined-primary h-9 ml-auto"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleFilter}
|
||||
className="button-filled-primary h-9 px-6 gap-2"
|
||||
>
|
||||
<Filter className="h-4 w-4" /> 查詢
|
||||
</Button>
|
||||
{/* Action Buttons Integrated */}
|
||||
<div className="md:col-span-3 flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClearFilters}
|
||||
className="flex-1 items-center gap-2 button-outlined-primary h-9"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleFilter}
|
||||
className="flex-1 button-filled-primary h-9 gap-2"
|
||||
>
|
||||
<Filter className="h-4 w-4" /> 查詢
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="flex items-center gap-3 px-4 py-4 bg-white rounded-xl border-l-4 border-l-emerald-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowDownToLine className="h-6 w-6 text-emerald-500 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-sm text-gray-500 font-medium shrink-0">總進貨量</span>
|
||||
<span className="text-xl font-bold text-gray-900 truncate">{Number(summary?.total_inbound || 0).toLocaleString()}</span>
|
||||
<TooltipProvider>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-3 mb-6">
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-white rounded-xl border-l-4 border-l-emerald-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowDownToLine className="h-4 w-4 text-emerald-500 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-xs text-gray-500 font-medium shrink-0">進貨量</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-lg font-bold text-gray-900 truncate cursor-help">{Number(summary?.total_inbound || 0).toLocaleString()}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{Number(summary?.total_inbound || 0).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-4 bg-white rounded-xl border-l-4 border-l-red-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowUpFromLine className="h-6 w-6 text-red-500 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-sm text-gray-500 font-medium shrink-0">總出貨量</span>
|
||||
<span className="text-xl font-bold text-gray-900 truncate">{Number(summary?.total_outbound || 0).toLocaleString()}</span>
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-white rounded-xl border-l-4 border-l-red-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowUpFromLine className="h-4 w-4 text-red-500 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-xs text-gray-500 font-medium shrink-0">出貨量</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-lg font-bold text-gray-900 truncate cursor-help">{Number(summary?.total_outbound || 0).toLocaleString()}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{Number(summary?.total_outbound || 0).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-4 bg-white rounded-xl border-l-4 border-l-blue-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowRightLeft className="h-6 w-6 text-blue-500 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-sm text-gray-500 font-medium shrink-0">調整量</span>
|
||||
<span className="text-xl font-bold text-gray-900 truncate">{Number(summary?.total_adjust || 0).toLocaleString()}</span>
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-white rounded-xl border-l-4 border-l-cyan-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowDownToLine className="h-4 w-4 text-cyan-500 shrink-0 rotate-180" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-xs text-gray-500 font-medium shrink-0">調撥入</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-lg font-bold text-gray-900 truncate cursor-help">{Number(summary?.total_transfer_in || 0).toLocaleString()}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{Number(summary?.total_transfer_in || 0).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-4 bg-white rounded-xl border-l-4 border-l-gray-700 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<TrendingUp className="h-6 w-6 text-gray-700 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-sm text-gray-500 font-medium shrink-0">淨變動</span>
|
||||
<span className={`text-xl font-bold truncate ${summary?.total_net_change >= 0 ? "text-emerald-600" : "text-red-600"}`}>
|
||||
{summary?.total_net_change > 0 ? "+" : ""}{Number(summary?.total_net_change || 0).toLocaleString()}
|
||||
</span>
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-white rounded-xl border-l-4 border-l-orange-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowUpFromLine className="h-4 w-4 text-orange-500 shrink-0 rotate-180" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-xs text-gray-500 font-medium shrink-0">調撥出</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-lg font-bold text-gray-900 truncate cursor-help">{Number(summary?.total_transfer_out || 0).toLocaleString()}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{Number(summary?.total_transfer_out || 0).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-white rounded-xl border-l-4 border-l-blue-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<ArrowRightLeft className="h-4 w-4 text-blue-500 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-xs text-gray-500 font-medium shrink-0">調整量</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-lg font-bold text-gray-900 truncate cursor-help">{Number(summary?.total_adjust || 0).toLocaleString()}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{Number(summary?.total_adjust || 0).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-3 bg-white rounded-xl border-l-4 border-l-gray-700 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||
<TrendingUp className="h-4 w-4 text-gray-700 shrink-0" />
|
||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||
<span className="text-xs text-gray-500 font-medium shrink-0">淨變動</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className={`text-lg font-bold truncate cursor-help ${summary?.total_net_change >= 0 ? "text-emerald-600" : "text-red-600"}`}>
|
||||
{summary?.total_net_change > 0 ? "+" : ""}{Number(summary?.total_net_change || 0).toLocaleString()}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{summary?.total_net_change > 0 ? "+" : ""}{Number(summary?.total_net_change || 0).toLocaleString()}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
{/* Results Table */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
@@ -368,18 +484,32 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
<TableHeader className="bg-gray-50">
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">商品代碼</TableHead>
|
||||
<TableHead className="">商品名稱</TableHead>
|
||||
<TableHead>商品名稱</TableHead>
|
||||
<TableHead className="w-[120px]">分類</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-emerald-600">進貨量</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-red-600">出貨量</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-blue-600">調整量</TableHead>
|
||||
<TableHead className="text-right w-[100px]">淨變動</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-emerald-600 cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => handleSort('inbound_qty')}>
|
||||
<div className="flex items-center justify-end">進貨量 <SortIcon field="inbound_qty" /></div>
|
||||
</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-red-600 cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => handleSort('outbound_qty')}>
|
||||
<div className="flex items-center justify-end">出貨量 <SortIcon field="outbound_qty" /></div>
|
||||
</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-cyan-600 cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => handleSort('transfer_in_qty')}>
|
||||
<div className="flex items-center justify-end">調撥入 <SortIcon field="transfer_in_qty" /></div>
|
||||
</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-orange-600 cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => handleSort('transfer_out_qty')}>
|
||||
<div className="flex items-center justify-end">調撥出 <SortIcon field="transfer_out_qty" /></div>
|
||||
</TableHead>
|
||||
<TableHead className="text-right w-[100px] text-blue-600 cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => handleSort('adjust_qty')}>
|
||||
<div className="flex items-center justify-end">調整量 <SortIcon field="adjust_qty" /></div>
|
||||
</TableHead>
|
||||
<TableHead className="text-right w-[100px] cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => handleSort('net_change')}>
|
||||
<div className="flex items-center justify-end">淨變動 <SortIcon field="net_change" /></div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{reportData.data.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7}>
|
||||
<TableCell colSpan={9}>
|
||||
<div className="flex flex-col items-center justify-center space-y-2 py-8 text-gray-400">
|
||||
<Package className="h-10 w-10 opacity-20" />
|
||||
<p>無符合條件的報表資料</p>
|
||||
@@ -431,6 +561,12 @@ export default function InventoryReportIndex({ reportData, summary, warehouses,
|
||||
<TableCell className="text-right text-red-600 font-medium">
|
||||
{row.outbound_qty > 0 ? `-${row.outbound_qty}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-cyan-600 font-medium">
|
||||
{row.transfer_in_qty > 0 ? `+${row.transfer_in_qty}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-orange-600 font-medium">
|
||||
{row.transfer_out_qty > 0 ? `-${row.transfer_out_qty}` : "-"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-blue-600 font-medium">
|
||||
{row.adjust_qty !== 0 ? (row.adjust_qty > 0 ? `+${row.adjust_qty}` : row.adjust_qty) : "-"}
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user