231 lines
10 KiB
TypeScript
231 lines
10 KiB
TypeScript
|
|
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>
|
|||
|
|
);
|
|||
|
|
}
|