From 9a50bbf8875996576f7920df5182e0a00b3c634b Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 20 Jan 2026 17:45:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(accounting):=20=E5=84=AA=E5=8C=96=E6=9C=83?= =?UTF-8?q?=E8=A8=88=E5=A0=B1=E8=A1=A8=E8=88=87=E5=85=AC=E5=85=B1=E4=BA=8B?= =?UTF-8?q?=E6=A5=AD=E8=B2=BB=20UI=EF=BC=8C=E4=B8=A6=E7=B5=B1=E4=B8=80?= =?UTF-8?q?=E5=85=A8=E5=9F=9F=E6=97=A5=E6=9C=9F=E8=99=95=E7=90=86=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccountingReportController.php | 29 +- app/Models/PurchaseOrder.php | 9 +- .../js/Components/Product/ProductTable.tsx | 2 +- .../UtilityFee/UtilityFeeDialog.tsx | 2 +- .../js/Components/Vendor/VendorTable.tsx | 2 +- resources/js/Layouts/AuthenticatedLayout.tsx | 2 +- resources/js/Pages/Accounting/Report.tsx | 309 ++++++++++++------ resources/js/Pages/UtilityFee/Index.tsx | 4 +- routes/web.php | 12 +- 9 files changed, 251 insertions(+), 120 deletions(-) diff --git a/app/Http/Controllers/AccountingReportController.php b/app/Http/Controllers/AccountingReportController.php index 929c867..3ae426c 100644 --- a/app/Http/Controllers/AccountingReportController.php +++ b/app/Http/Controllers/AccountingReportController.php @@ -7,13 +7,14 @@ use App\Models\UtilityFee; use Illuminate\Http\Request; use Inertia\Inertia; use Illuminate\Support\Carbon; +use Illuminate\Pagination\LengthAwarePaginator; class AccountingReportController extends Controller { public function index(Request $request) { - $dateStart = $request->input('date_start', Carbon::now()->startOfMonth()->toDateString()); - $dateEnd = $request->input('date_end', Carbon::now()->endOfMonth()->toDateString()); + $dateStart = $request->input('date_start', Carbon::now()->toDateString()); + $dateEnd = $request->input('date_end', Carbon::now()->toDateString()); // 1. Get Purchase Orders (Completed or Received that are ready for accounting) $purchaseOrders = PurchaseOrder::with(['vendor']) @@ -23,7 +24,7 @@ class AccountingReportController extends Controller ->map(function ($po) { return [ 'id' => 'PO-' . $po->id, - 'date' => $po->created_at->toDateString(), + 'date' => Carbon::parse($po->created_at)->timezone(config('app.timezone'))->toDateString(), 'source' => '採購單', 'category' => '進貨支出', 'item' => $po->vendor->name ?? '未知廠商', @@ -39,7 +40,7 @@ class AccountingReportController extends Controller ->map(function ($fee) { return [ 'id' => 'UF-' . $fee->id, - 'date' => $fee->transaction_date, + 'date' => $fee->transaction_date->format('Y-m-d'), 'source' => '公共事業費', 'category' => $fee->category, 'item' => $fee->description ?: $fee->category, @@ -54,6 +55,19 @@ class AccountingReportController extends Controller ->sortByDesc('date') ->values(); + // 3. Manual Pagination + $perPage = $request->input('per_page', 10); + $page = $request->input('page', 1); + $offset = ($page - 1) * $perPage; + + $paginatedRecords = new LengthAwarePaginator( + $allRecords->slice($offset, $perPage)->values(), + $allRecords->count(), + $perPage, + $page, + ['path' => $request->url(), 'query' => $request->query()] + ); + $summary = [ 'total_amount' => $allRecords->sum('amount'), 'purchase_total' => $purchaseOrders->sum('amount'), @@ -62,19 +76,20 @@ class AccountingReportController extends Controller ]; return Inertia::render('Accounting/Report', [ - 'records' => $allRecords, + 'records' => $paginatedRecords, 'summary' => $summary, 'filters' => [ 'date_start' => $dateStart, 'date_end' => $dateEnd, + 'per_page' => (int)$perPage, ], ]); } public function export(Request $request) { - $dateStart = $request->input('date_start', Carbon::now()->startOfMonth()->toDateString()); - $dateEnd = $request->input('date_end', Carbon::now()->endOfMonth()->toDateString()); + $dateStart = $request->input('date_start', Carbon::now()->toDateString()); + $dateEnd = $request->input('date_end', Carbon::now()->toDateString()); $purchaseOrders = PurchaseOrder::with(['vendor']) ->whereIn('status', ['received', 'completed']) diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 1fa8ff1..794647e 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -30,8 +30,8 @@ class PurchaseOrder extends Model ]; protected $casts = [ - 'expected_delivery_date' => 'date', - 'invoice_date' => 'date', + 'expected_delivery_date' => 'date:Y-m-d', + 'invoice_date' => 'date:Y-m-d', 'total_amount' => 'decimal:2', 'tax_amount' => 'decimal:2', 'grand_total' => 'decimal:2', @@ -76,7 +76,7 @@ class PurchaseOrder extends Model public function getExpectedDateAttribute(): ?string { - return $this->expected_delivery_date ? $this->expected_delivery_date->format('Y-m-d') : null; + return $this->attributes['expected_delivery_date'] ?? null; } public function getTotalAmountAttribute(): float @@ -111,8 +111,7 @@ class PurchaseOrder extends Model public function getInvoiceDateAttribute(): ?string { - $date = $this->attributes['invoice_date'] ?? null; - return $date ? \Illuminate\Support\Carbon::parse($date)->format('Y-m-d') : null; + return $this->attributes['invoice_date'] ?? null; } public function getInvoiceAmountAttribute(): ?float diff --git a/resources/js/Components/Product/ProductTable.tsx b/resources/js/Components/Product/ProductTable.tsx index 6f77620..a6c0c83 100644 --- a/resources/js/Components/Product/ProductTable.tsx +++ b/resources/js/Components/Product/ProductTable.tsx @@ -71,7 +71,7 @@ export default function ProductTable({ <>
- + # diff --git a/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx b/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx index 124fc1b..feafee4 100644 --- a/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx +++ b/resources/js/Components/UtilityFee/UtilityFeeDialog.tsx @@ -68,7 +68,7 @@ export default function UtilityFeeDialog({ clearErrors(); if (fee) { setData({ - transaction_date: fee.transaction_date.split("T")[0].split(" ")[0], + transaction_date: fee.transaction_date, category: fee.category, amount: fee.amount.toString(), invoice_number: fee.invoice_number || "", diff --git a/resources/js/Components/Vendor/VendorTable.tsx b/resources/js/Components/Vendor/VendorTable.tsx index 693db7d..18f7a66 100644 --- a/resources/js/Components/Vendor/VendorTable.tsx +++ b/resources/js/Components/Vendor/VendorTable.tsx @@ -57,7 +57,7 @@ export default function VendorTable({ return (
- + # diff --git a/resources/js/Layouts/AuthenticatedLayout.tsx b/resources/js/Layouts/AuthenticatedLayout.tsx index 91c80bb..65736d3 100644 --- a/resources/js/Layouts/AuthenticatedLayout.tsx +++ b/resources/js/Layouts/AuthenticatedLayout.tsx @@ -154,7 +154,7 @@ export default function AuthenticatedLayout({ id: "accounting-report", label: "會計報表", icon: , - route: "/accounting/report", + route: "/accounting-report", permission: "accounting.view", }, ], diff --git a/resources/js/Pages/Accounting/Report.tsx b/resources/js/Pages/Accounting/Report.tsx index 3bcfe10..e86abc3 100644 --- a/resources/js/Pages/Accounting/Report.tsx +++ b/resources/js/Pages/Accounting/Report.tsx @@ -1,16 +1,17 @@ import { useState } from "react"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; +import { Label } from "@/Components/ui/label"; import { BarChart3, Download, Calendar, Filter, - ArrowUpRight, TrendingDown, - FileSpreadsheet, Package, - Pocket + Pocket, + RotateCcw, + FileText } from 'lucide-react'; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router } from "@inertiajs/react"; @@ -22,12 +23,10 @@ import { TableHeader, TableRow, } from "@/Components/ui/table"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "@/Components/ui/card"; +import { getDateRange, formatDateWithDayOfWeek } from "@/utils/format"; +import { Badge } from "@/Components/ui/badge"; +import Pagination from "@/Components/shared/Pagination"; +import { SearchableSelect } from "@/Components/ui/searchable-select"; interface Record { id: string; @@ -41,7 +40,14 @@ interface Record { } interface PageProps { - records: Record[]; + records: { + data: Record[]; + links: any[]; + total: number; + from: number; + to: number; + current_page: number; + }; summary: { total_amount: number; purchase_total: number; @@ -51,6 +57,7 @@ interface PageProps { filters: { date_start: string; date_end: string; + per_page?: number; }; } @@ -58,17 +65,54 @@ export default function AccountingReport({ records, summary, filters }: PageProp const [dateStart, setDateStart] = useState(filters.date_start); const [dateEnd, setDateEnd] = useState(filters.date_end); + // Determine initial range type + const today = new Date().toISOString().split('T')[0]; + 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 handleDateRangeChange = (type: string) => { + setDateRangeType(type); + if (type === "custom") return; + + const { start, end } = getDateRange(type); + setDateStart(start); + setDateEnd(end); + }; + + const handleFilter = () => { router.get( route("accounting.report"), { date_start: dateStart, date_end: dateEnd, + per_page: perPage, }, { preserveState: true } ); }; + const handlePerPageChange = (value: string) => { + setPerPage(value); + router.get( + route("accounting.report"), + { + date_start: dateStart, + date_end: dateEnd, + per_page: value, + }, + { preserveState: true } + ); + }; + + const handleClearFilters = () => { + setDateStart(""); + setDateEnd(""); + setPerPage("10"); + router.get(route("accounting.report"), {}, { preserveState: false }); + }; + const handleExport = () => { window.location.href = route("accounting.export", { date_start: dateStart, @@ -77,15 +121,16 @@ export default function AccountingReport({ records, summary, filters }: PageProp }; return ( - +
+ {/* Header */}

- 會計支出報表 + 會計報表

彙整採購支出與各項公用事業費用

@@ -99,111 +144,161 @@ export default function AccountingReport({ records, summary, filters }: PageProp
- {/* Filters */} -
-
-
- -
- - setDateStart(e.target.value)} - className="pl-10 h-10 w-48" - /> + {/* Filters with Quick Date Range */} +
+
+
+
+ +
+ {[ + { label: "今日", value: "today" }, + { label: "昨日", value: "yesterday" }, + { label: "本週", value: "this_week" }, + { label: "本月", value: "this_month" }, + { label: "上月", value: "last_month" }, + ].map((opt) => ( + + ))} +
+
+ +
+
+
+ +
+ + { + setDateStart(e.target.value); + setDateRangeType('custom'); + }} + className="pl-9 block w-full h-9 bg-white" + /> +
+
+
+ +
+ + { + setDateEnd(e.target.value); + setDateRangeType('custom'); + }} + className="pl-9 block w-full h-9 bg-white" + /> +
+
+
-
- -
- - setDateEnd(e.target.value)} - className="pl-10 h-10 w-48" - /> -
+ {/* Action Buttons */} +
+ +
- -
- {/* Summary Cards */} -
- - - 總計支出 - - - -
$ {Number(summary.total_amount).toLocaleString()}
-

共有 {summary.record_count} 筆紀錄

-
-
+ {/* Compact Summary - Full Width Grid (Horizontal Style) */} +
+
+ +
+ 總計支出 + $ {Number(summary.total_amount).toLocaleString()} +
+
- - - 採購支出 - - - -
$ {Number(summary.purchase_total).toLocaleString()}
-

採購單彙整

-
-
+
+ +
+ 採購支出 + $ {Number(summary.purchase_total).toLocaleString()} +
+
- - - 公共事業費 - - - -
$ {Number(summary.utility_total).toLocaleString()}
-

水、電、瓦斯、電信等費項

-
-
+
+ +
+ 公共事業費 + $ {Number(summary.utility_total).toLocaleString()} +
+
{/* Results Table */} -
+
- 日期 - 來源 - 類別 - 項目詳細 - 憑證 / 單號 - 金額 + 日期 + 來源 + 類別 + 項目詳細 + 金額 - {records.length === 0 ? ( + {records.data.length === 0 ? ( - - 此日期區間內無支出紀錄 + +
+ +

此日期區間內無支出紀錄

+
) : ( - records.map((record) => ( - - {record.date} - - - {record.source} - + records.data.map((record) => ( + + + {formatDateWithDayOfWeek(record.date)} + + + + {record.source} + + + + + {record.category} + - {record.category}
{record.item} @@ -212,9 +307,6 @@ export default function AccountingReport({ records, summary, filters }: PageProp )}
- - {record.reference} - $ {Number(record.amount).toLocaleString()} @@ -224,6 +316,29 @@ export default function AccountingReport({ records, summary, filters }: PageProp
+ + {/* Pagination Footer */} +
+
+ 每頁顯示 + + +
+
+ +
+
); diff --git a/resources/js/Pages/UtilityFee/Index.tsx b/resources/js/Pages/UtilityFee/Index.tsx index fe31f9b..eb9155e 100644 --- a/resources/js/Pages/UtilityFee/Index.tsx +++ b/resources/js/Pages/UtilityFee/Index.tsx @@ -212,7 +212,7 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: }; return ( - +
@@ -385,7 +385,7 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }: {/* Table */}
- + # diff --git a/routes/web.php b/routes/web.php index b6aa958..268016e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -144,6 +144,12 @@ Route::middleware('auth')->group(function () { ->middleware('permission:inventory.view') ->name('api.warehouses.inventories'); + // 系統管理 + Route::middleware('permission:accounting.view')->prefix('accounting-report')->group(function () { + Route::get('/', [AccountingReportController::class, 'index'])->name('accounting.report'); + Route::get('/export', [AccountingReportController::class, 'export'])->name('accounting.export'); + }); + // 系統管理 Route::prefix('admin')->group(function () { Route::middleware('permission:roles.view')->group(function () { @@ -171,11 +177,7 @@ Route::middleware('auth')->group(function () { Route::middleware('permission:system.view_logs')->group(function () { Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('activity-logs.index'); }); - // 會計報表 - Route::middleware('permission:accounting.view')->group(function () { - Route::get('/accounting/report', [AccountingReportController::class, 'index'])->name('accounting.report'); - Route::get('/accounting/report/export', [AccountingReportController::class, 'export'])->name('accounting.export'); - }); + }); }); // End of auth middleware group