優化公共事業費:修正日期顯示、改善發票號碼輸入UX與調整介面欄位順序
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 44s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-20 13:02:05 +08:00
parent 7bf892db19
commit b2a63bd1ed
4 changed files with 98 additions and 66 deletions

View File

@@ -20,7 +20,7 @@ class UtilityFee extends Model
]; ];
protected $casts = [ protected $casts = [
'transaction_date' => 'date', 'transaction_date' => 'date:Y-m-d',
'amount' => 'decimal:2', 'amount' => 'decimal:2',
]; ];

View File

@@ -16,6 +16,7 @@ import { useForm } from "@inertiajs/react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Calendar } from "lucide-react"; import { Calendar } from "lucide-react";
import { getCurrentDate } from "@/utils/format"; import { getCurrentDate } from "@/utils/format";
import { validateInvoiceNumber } from "@/utils/validation";
export interface UtilityFee { export interface UtilityFee {
id: number; id: number;
@@ -84,9 +85,9 @@ export default function UtilityFeeDialog({
e.preventDefault(); e.preventDefault();
if (fee) { if (fee) {
// Validate invoice number format if present const validation = validateInvoiceNumber(data.invoice_number);
if (data.invoice_number && !/^[A-Z]{2}-\d{8}$/.test(data.invoice_number)) { if (!validation.isValid) {
toast.error("發票號碼格式錯誤應為AB-12345678"); toast.error(validation.error);
return; return;
} }
@@ -101,9 +102,9 @@ export default function UtilityFeeDialog({
} }
}); });
} else { } else {
// Validate invoice number format if present const validation = validateInvoiceNumber(data.invoice_number);
if (data.invoice_number && !/^[A-Z]{2}-\d{8}$/.test(data.invoice_number)) { if (!validation.isValid) {
toast.error("發票號碼格式錯誤應為AB-12345678"); toast.error(validation.error);
return; return;
} }
@@ -189,8 +190,9 @@ export default function UtilityFeeDialog({
<Input <Input
id="invoice_number" id="invoice_number"
value={data.invoice_number} value={data.invoice_number}
onChange={(e) => setData("invoice_number", e.target.value)} onChange={(e) => setData("invoice_number", e.target.value.toUpperCase())}
placeholder="例AB-12345678" placeholder="例AB-12345678"
maxLength={11}
/> />
<p className="text-xs text-gray-500">AB-12345678</p> <p className="text-xs text-gray-500">AB-12345678</p>
{errors.invoice_number && <p className="text-sm text-red-500">{errors.invoice_number}</p>} {errors.invoice_number && <p className="text-sm text-red-500">{errors.invoice_number}</p>}

View File

@@ -15,6 +15,7 @@ import {
ArrowUp, ArrowUp,
ArrowDown ArrowDown
} from 'lucide-react'; } from 'lucide-react';
import { Label } from "@/Components/ui/label";
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
import { Head, router } from "@inertiajs/react"; import { Head, router } from "@inertiajs/react";
import Pagination from "@/Components/shared/Pagination"; import Pagination from "@/Components/shared/Pagination";
@@ -200,57 +201,69 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
<div className="bg-white rounded-lg shadow-sm border p-4 mb-6"> <div className="bg-white rounded-lg shadow-sm border p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Search */} {/* Search */}
<div className="relative"> <div className="space-y-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> <Label className="text-xs text-gray-500"></Label>
<Input <div className="relative">
placeholder="搜尋發票、備註..." <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
value={searchTerm} <Input
onChange={(e) => setSearchTerm(e.target.value)} placeholder="搜尋發票、備註..."
onKeyDown={(e) => e.key === "Enter" && handleSearch()} value={searchTerm}
className="pl-10" onChange={(e) => setSearchTerm(e.target.value)}
/> onKeyDown={(e) => e.key === "Enter" && handleSearch()}
{searchTerm && ( className="pl-10"
<button />
onClick={() => setSearchTerm("")} {searchTerm && (
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600" <button
> onClick={() => setSearchTerm("")}
<X className="h-4 w-4" /> className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
</button> >
)} <X className="h-4 w-4" />
</button>
)}
</div>
</div> </div>
{/* Category Filter */}
<SearchableSelect
value={categoryFilter}
onValueChange={setCategoryFilter}
options={[
{ label: "所有類別", value: "all" },
...availableCategories.map(c => ({ label: c, value: c }))
]}
placeholder="篩選類別"
/>
{/* Date Range Start */} {/* Date Range Start */}
<div className="relative"> <div className="space-y-1">
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 pointer-events-none" /> <Label className="text-xs text-gray-500"></Label>
<Input <div className="relative">
type="date" <Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
value={dateStart} <Input
onChange={(e) => setDateStart(e.target.value)} type="date"
className="pl-9 bg-white block w-full" value={dateStart}
placeholder="開始日期" onChange={(e) => setDateStart(e.target.value)}
/> className="pl-9 bg-white block w-full"
placeholder="開始日期"
/>
</div>
</div> </div>
{/* Date Range End */} {/* Date Range End */}
<div className="relative"> <div className="space-y-1">
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 pointer-events-none" /> <Label className="text-xs text-gray-500"></Label>
<Input <div className="relative">
type="date" <Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
value={dateEnd} <Input
onChange={(e) => setDateEnd(e.target.value)} type="date"
className="pl-9 bg-white block w-full" value={dateEnd}
placeholder="結束日期" onChange={(e) => setDateEnd(e.target.value)}
className="pl-9 bg-white block w-full"
placeholder="結束日期"
/>
</div>
</div>
{/* Category Filter */}
<div className="space-y-1">
<Label className="text-xs text-gray-500"></Label>
<SearchableSelect
value={categoryFilter}
onValueChange={setCategoryFilter}
options={[
{ label: "所有類別", value: "all" },
...availableCategories.map(c => ({ label: c, value: c }))
]}
placeholder="篩選類別"
/> />
</div> </div>
</div> </div>
@@ -259,13 +272,13 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
<Button <Button
variant="outline" variant="outline"
onClick={handleClearFilters} onClick={handleClearFilters}
className="button-outlined-primary h-9" className="button-outlined-primary h-9 mt-auto"
> >
</Button> </Button>
<Button <Button
onClick={handleSearch} onClick={handleSearch}
className="button-filled-primary h-9 gap-2" className="button-filled-primary h-9 gap-2 mt-auto"
> >
<Filter className="h-4 w-4" /> <Filter className="h-4 w-4" />
</Button> </Button>
@@ -294,6 +307,14 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
<SortIcon field="category" /> <SortIcon field="category" />
</button> </button>
</TableHead> </TableHead>
<TableHead>
<button
onClick={() => handleSort('invoice_number')}
className="flex items-center hover:text-gray-900"
>
<SortIcon field="invoice_number" />
</button>
</TableHead>
<TableHead className="text-right"> <TableHead className="text-right">
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
@@ -304,14 +325,6 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
</button> </button>
</div> </div>
</TableHead> </TableHead>
<TableHead>
<button
onClick={() => handleSort('invoice_number')}
className="flex items-center hover:text-gray-900"
>
<SortIcon field="invoice_number" />
</button>
</TableHead>
<TableHead> / </TableHead> <TableHead> / </TableHead>
<TableHead className="text-center w-[120px]"></TableHead> <TableHead className="text-center w-[120px]"></TableHead>
</TableRow> </TableRow>
@@ -340,12 +353,12 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
{fee.category} {fee.category}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="text-right font-bold text-gray-900">
$ {Number(fee.amount).toLocaleString()}
</TableCell>
<TableCell className="font-mono text-sm text-gray-600"> <TableCell className="font-mono text-sm text-gray-600">
{formatInvoiceNumber(fee.invoice_number)} {formatInvoiceNumber(fee.invoice_number)}
</TableCell> </TableCell>
<TableCell className="text-right font-bold text-gray-900">
$ {Number(fee.amount).toLocaleString()}
</TableCell>
<TableCell className="max-w-xs truncate text-gray-600" title={fee.description}> <TableCell className="max-w-xs truncate text-gray-600" title={fee.description}>
{fee.description || '-'} {fee.description || '-'}
</TableCell> </TableCell>

View File

@@ -70,3 +70,20 @@ export const validateWarehouse = (formData: {
return { isValid: true }; return { isValid: true };
}; };
/**
* 驗證發票號碼格式 (AA-12345678)
*/
export const validateInvoiceNumber = (invoiceNumber?: string): { isValid: boolean; error?: string } => {
if (!invoiceNumber) return { isValid: true };
const regex = /^[A-Z]{2}-\d{8}$/;
if (!regex.test(invoiceNumber)) {
return {
isValid: false,
error: "發票號碼格式錯誤應為AB-12345678",
};
}
return { isValid: true };
};