From fc20c6d8138349705c268ca970fcb13de1611f67 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Wed, 21 Jan 2026 16:30:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=95=86=E5=93=81):=20=E8=AA=BF=E6=95=B4?= =?UTF-8?q?=E5=95=86=E5=93=81=E4=BB=A3=E8=99=9F=E9=A1=AF=E7=A4=BA=E8=88=87?= =?UTF-8?q?=E6=9C=83=E8=A8=88=E5=A0=B1=E8=A1=A8=E6=A8=A3=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountingReportController.php | 39 ++++++++++++---- app/Http/Controllers/ProductController.php | 16 +++---- .../ActivityLog/ActivityDetailDialog.tsx | 2 +- .../Components/Product/BarcodeViewDialog.tsx | 2 +- .../js/Components/Product/ProductDialog.tsx | 17 +++++++ .../js/Components/Product/ProductTable.tsx | 2 +- resources/js/Pages/Accounting/Report.tsx | 46 +++++++++++++++++-- resources/js/Pages/Product/Index.tsx | 2 +- 8 files changed, 101 insertions(+), 25 deletions(-) diff --git a/app/Http/Controllers/AccountingReportController.php b/app/Http/Controllers/AccountingReportController.php index 3ae426c..c65dfe6 100644 --- a/app/Http/Controllers/AccountingReportController.php +++ b/app/Http/Controllers/AccountingReportController.php @@ -90,37 +90,58 @@ class AccountingReportController extends Controller { $dateStart = $request->input('date_start', Carbon::now()->toDateString()); $dateEnd = $request->input('date_end', Carbon::now()->toDateString()); + $selectedIdsParam = $request->input('selected_ids'); - $purchaseOrders = PurchaseOrder::with(['vendor']) - ->whereIn('status', ['received', 'completed']) - ->whereBetween('created_at', [$dateStart . ' 00:00:00', $dateEnd . ' 23:59:59']) - ->get(); + $purchaseOrdersQuery = PurchaseOrder::with(['vendor']) + ->whereIn('status', ['received', 'completed']); + + $utilityFeesQuery = UtilityFee::query(); - $utilityFees = UtilityFee::whereBetween('transaction_date', [$dateStart, $dateEnd])->get(); + if ($selectedIdsParam) { + $ids = explode(',', $selectedIdsParam); + $poIds = []; + $ufIds = []; + foreach ($ids as $id) { + if (str_starts_with($id, 'PO-')) { + $poIds[] = substr($id, 3); + } elseif (str_starts_with($id, 'UF-')) { + $ufIds[] = substr($id, 3); + } + } + $purchaseOrders = $purchaseOrdersQuery->whereIn('id', $poIds)->get(); + $utilityFees = $utilityFeesQuery->whereIn('id', $ufIds)->get(); + } else { + $purchaseOrders = $purchaseOrdersQuery + ->whereBetween('created_at', [$dateStart . ' 00:00:00', $dateEnd . ' 23:59:59']) + ->get(); + $utilityFees = $utilityFeesQuery + ->whereBetween('transaction_date', [$dateStart, $dateEnd]) + ->get(); + } $allRecords = collect(); foreach ($purchaseOrders as $po) { $allRecords->push([ - $po->created_at->toDateString(), + Carbon::parse($po->created_at)->toDateString(), '採購單', '進貨支出', $po->vendor->name ?? '', $po->code, $po->invoice_number, - $po->grand_total, + (float)$po->grand_total, ]); } foreach ($utilityFees as $fee) { $allRecords->push([ - $fee->transaction_date, + Carbon::parse($fee->transaction_date)->toDateString(), '公共事業費', $fee->category, $fee->description, '-', $fee->invoice_number, - $fee->amount, + (float)$fee->amount, ]); } diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 9e3e527..6207b4e 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -75,6 +75,7 @@ class ProductController extends Controller public function store(Request $request) { $validated = $request->validate([ + 'code' => 'required|string|max:2|unique:products,code', 'name' => 'required|string|max:255', 'category_id' => 'required|exists:categories,id', 'brand' => 'nullable|string|max:255', @@ -85,6 +86,9 @@ class ProductController extends Controller 'conversion_rate' => 'required_with:large_unit_id|nullable|numeric|min:0.0001', 'purchase_unit_id' => 'nullable|exists:units,id', ], [ + 'code.required' => '商品代號為必填', + 'code.max' => '商品代號最多 2 碼', + 'code.unique' => '商品代號已存在', 'name.required' => '商品名稱為必填', 'category_id.required' => '請選擇分類', 'category_id.exists' => '所選分類不存在', @@ -94,14 +98,6 @@ class ProductController extends Controller 'conversion_rate.numeric' => '換算率必須為數字', 'conversion_rate.min' => '換算率最小為 0.0001', ]); - - // Auto-generate code - $prefix = 'P'; - $lastProduct = Product::withTrashed()->latest('id')->first(); - $nextId = $lastProduct ? $lastProduct->id + 1 : 1; - $code = $prefix . str_pad($nextId, 5, '0', STR_PAD_LEFT); - - $validated['code'] = $code; $product = Product::create($validated); @@ -114,6 +110,7 @@ class ProductController extends Controller public function update(Request $request, Product $product) { $validated = $request->validate([ + 'code' => 'required|string|max:2|unique:products,code,' . $product->id, 'name' => 'required|string|max:255', 'category_id' => 'required|exists:categories,id', 'brand' => 'nullable|string|max:255', @@ -123,6 +120,9 @@ class ProductController extends Controller 'conversion_rate' => 'required_with:large_unit_id|nullable|numeric|min:0.0001', 'purchase_unit_id' => 'nullable|exists:units,id', ], [ + 'code.required' => '商品代號為必填', + 'code.max' => '商品代號最多 2 碼', + 'code.unique' => '商品代號已存在', 'name.required' => '商品名稱為必填', 'category_id.required' => '請選擇分類', 'category_id.exists' => '所選分類不存在', diff --git a/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx b/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx index 9d88c89..7b3ad51 100644 --- a/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx +++ b/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx @@ -44,7 +44,7 @@ interface Props { // Field translation map const fieldLabels: Record = { name: '名稱', - code: '代碼', + code: '商品代號', username: '登入帳號', description: '描述', price: '價格', diff --git a/resources/js/Components/Product/BarcodeViewDialog.tsx b/resources/js/Components/Product/BarcodeViewDialog.tsx index 13f362c..d46e27c 100644 --- a/resources/js/Components/Product/BarcodeViewDialog.tsx +++ b/resources/js/Components/Product/BarcodeViewDialog.tsx @@ -78,7 +78,7 @@ export default function BarcodeViewDialog({
${productName}
-
商品編號: ${productCode}
+
商品代號: ${productCode}
商品條碼
diff --git a/resources/js/Components/Product/ProductDialog.tsx b/resources/js/Components/Product/ProductDialog.tsx index cf2e59c..d7d95bd 100644 --- a/resources/js/Components/Product/ProductDialog.tsx +++ b/resources/js/Components/Product/ProductDialog.tsx @@ -35,6 +35,7 @@ export default function ProductDialog({ units, }: ProductDialogProps) { const { data, setData, post, put, processing, errors, reset, clearErrors } = useForm({ + code: "", name: "", category_id: "", brand: "", @@ -50,6 +51,7 @@ export default function ProductDialog({ clearErrors(); if (product) { setData({ + code: product.code, name: product.name, category_id: product.category_id.toString(), brand: product.brand || "", @@ -142,6 +144,21 @@ export default function ProductDialog({ {errors.name &&

{errors.name}

} +
+ + setData("code", e.target.value)} + placeholder="例:A1 (最多2碼)" + maxLength={2} + className={errors.code ? "border-red-500" : ""} + /> + {errors.code &&

{errors.code}

} +
+
# diff --git a/resources/js/Pages/Accounting/Report.tsx b/resources/js/Pages/Accounting/Report.tsx index 3ae30fe..d933fbe 100644 --- a/resources/js/Pages/Accounting/Report.tsx +++ b/resources/js/Pages/Accounting/Report.tsx @@ -28,6 +28,7 @@ import { Badge } from "@/Components/ui/badge"; import Pagination from "@/Components/shared/Pagination"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { Can } from "@/Components/Permission/Can"; +import { Checkbox } from "@/Components/ui/checkbox"; interface Record { id: string; @@ -71,6 +72,7 @@ export default function AccountingReport({ records, summary, filters }: PageProp const initialRangeType = (filters.date_start === today && filters.date_end === today) ? "today" : "custom"; const [dateRangeType, setDateRangeType] = useState(initialRangeType); const [perPage, setPerPage] = useState(filters.per_page?.toString() || "10"); + const [selectedIds, setSelectedIds] = useState([]); const handleDateRangeChange = (type: string) => { setDateRangeType(type); @@ -111,14 +113,35 @@ export default function AccountingReport({ records, summary, filters }: PageProp setDateStart(""); setDateEnd(""); setPerPage("10"); + setSelectedIds([]); router.get(route("accounting.report"), {}, { preserveState: false }); }; + const toggleSelectAll = () => { + if (selectedIds.length === records.data.length) { + setSelectedIds([]); + } else { + setSelectedIds(records.data.map(r => r.id)); + } + }; + + const toggleSelect = (id: string) => { + setSelectedIds(prev => + prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] + ); + }; + const handleExport = () => { - window.location.href = route("accounting.export", { + const query: any = { date_start: dateStart, date_end: dateEnd, - }); + }; + + if (selectedIds.length > 0) { + query.selected_ids = selectedIds.join(','); + } + + window.location.href = route("accounting.export", query); }; return ( @@ -142,7 +165,8 @@ export default function AccountingReport({ records, summary, filters }: PageProp variant="outline" className="button-outlined-primary gap-2" > - 匯出 CSV 報表 + + {selectedIds.length > 0 ? `匯出已選 (${selectedIds.length})` : '匯出 CSV 報表'}
@@ -265,6 +289,13 @@ export default function AccountingReport({ records, summary, filters }: PageProp + + 0 && selectedIds.length === records.data.length} + onCheckedChange={toggleSelectAll} + aria-label="Select all" + /> + 日期 來源 類別 @@ -275,7 +306,7 @@ export default function AccountingReport({ records, summary, filters }: PageProp {records.data.length === 0 ? ( - +

此日期區間內無支出紀錄

@@ -285,6 +316,13 @@ export default function AccountingReport({ records, summary, filters }: PageProp ) : ( records.data.map((record) => ( + + toggleSelect(record.id)} + aria-label={`Select record ${record.id}`} + /> + {formatDateWithDayOfWeek(record.date)} diff --git a/resources/js/Pages/Product/Index.tsx b/resources/js/Pages/Product/Index.tsx index df2e319..044c16d 100644 --- a/resources/js/Pages/Product/Index.tsx +++ b/resources/js/Pages/Product/Index.tsx @@ -191,7 +191,7 @@ export default function ProductManagement({ products, categories, units, filters
handleSearchChange(e.target.value)} className="pl-10 pr-10"