feat(accounting): 實作公共事業費管理與會計支出報表功能
This commit is contained in:
230
resources/js/Pages/Accounting/Report.tsx
Normal file
230
resources/js/Pages/Accounting/Report.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import {
|
||||
BarChart3,
|
||||
Download,
|
||||
Calendar,
|
||||
Filter,
|
||||
ArrowUpRight,
|
||||
TrendingDown,
|
||||
FileSpreadsheet,
|
||||
Package,
|
||||
Pocket
|
||||
} from 'lucide-react';
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router } from "@inertiajs/react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/Components/ui/table";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/Components/ui/card";
|
||||
|
||||
interface Record {
|
||||
id: string;
|
||||
date: string;
|
||||
source: string;
|
||||
category: string;
|
||||
item: string;
|
||||
reference: string;
|
||||
invoice_number?: string;
|
||||
amount: number | string;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
records: Record[];
|
||||
summary: {
|
||||
total_amount: number;
|
||||
purchase_total: number;
|
||||
utility_total: number;
|
||||
record_count: number;
|
||||
};
|
||||
filters: {
|
||||
date_start: string;
|
||||
date_end: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AccountingReport({ records, summary, filters }: PageProps) {
|
||||
const [dateStart, setDateStart] = useState(filters.date_start);
|
||||
const [dateEnd, setDateEnd] = useState(filters.date_end);
|
||||
|
||||
const handleFilter = () => {
|
||||
router.get(
|
||||
route("accounting.report"),
|
||||
{
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
},
|
||||
{ preserveState: true }
|
||||
);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
window.location.href = route("accounting.export", {
|
||||
date_start: dateStart,
|
||||
date_end: dateEnd,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthenticatedLayout breadcrumbs={[{ label: "報表管理", href: "#" }, { label: "會計報表", href: route("accounting.report") }]}>
|
||||
<Head title="會計報表" />
|
||||
|
||||
<div className="container mx-auto p-6 max-w-7xl">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-grey-0 flex items-center gap-2">
|
||||
<BarChart3 className="h-6 w-6 text-primary-main" />
|
||||
會計支出報表
|
||||
</h1>
|
||||
<p className="text-gray-500 mt-1">彙整採購支出與各項公用事業費用</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
variant="outline"
|
||||
className="button-outlined-primary gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" /> 匯出 CSV 報表
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">開始日期</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={dateStart}
|
||||
onChange={(e) => setDateStart(e.target.value)}
|
||||
className="pl-10 h-10 w-48"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">結束日期</label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={dateEnd}
|
||||
onChange={(e) => setDateEnd(e.target.value)}
|
||||
className="pl-10 h-10 w-48"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleFilter}
|
||||
className="button-filled-primary h-10 px-6 gap-2"
|
||||
>
|
||||
<Filter className="h-4 w-4" /> 篩選
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<Card className="border-l-4 border-l-red-500 shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-500">總計支出</CardTitle>
|
||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-gray-900">$ {Number(summary.total_amount).toLocaleString()}</div>
|
||||
<p className="text-xs text-gray-400 mt-1">共有 {summary.record_count} 筆紀錄</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-l-4 border-l-orange-500 shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-500">採購支出</CardTitle>
|
||||
<Package className="h-4 w-4 text-orange-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-gray-900">$ {Number(summary.purchase_total).toLocaleString()}</div>
|
||||
<p className="text-xs text-gray-400 mt-1">採購單彙整</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-l-4 border-l-blue-500 shadow-sm">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-gray-500">公共事業費</CardTitle>
|
||||
<Pocket className="h-4 w-4 text-blue-500" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-gray-900">$ {Number(summary.utility_total).toLocaleString()}</div>
|
||||
<p className="text-xs text-gray-400 mt-1">水、電、瓦斯、電信等費項</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Results Table */}
|
||||
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader className="bg-gray-50">
|
||||
<TableRow>
|
||||
<TableHead className="w-[120px] py-4 px-4">日期</TableHead>
|
||||
<TableHead className="w-[120px]">來源</TableHead>
|
||||
<TableHead className="w-[120px]">類別</TableHead>
|
||||
<TableHead className="px-4">項目詳細</TableHead>
|
||||
<TableHead className="w-[180px]">憑證 / 單號</TableHead>
|
||||
<TableHead className="w-[150px] text-right px-4">金額</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{records.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-48 text-center text-gray-500">
|
||||
此日期區間內無支出紀錄
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
records.map((record) => (
|
||||
<TableRow key={record.id} className="hover:bg-gray-50/50">
|
||||
<TableCell className="font-medium">{record.date}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${record.source === '採購單' ? 'bg-orange-100 text-orange-700' : 'bg-blue-100 text-blue-700'
|
||||
}`}>
|
||||
{record.source}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-gray-600">{record.category}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-gray-900">{record.item}</span>
|
||||
{record.invoice_number && (
|
||||
<span className="text-xs text-gray-400">發票:{record.invoice_number}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm text-gray-500">
|
||||
{record.reference}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-bold text-gray-900 px-4">
|
||||
$ {Number(record.amount).toLocaleString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user