優化公共事業費:修正日期顯示、改善發票號碼輸入UX與調整介面欄位順序
This commit is contained in:
@@ -20,7 +20,7 @@ class UtilityFee extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'transaction_date' => 'date',
|
||||
'transaction_date' => 'date:Y-m-d',
|
||||
'amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useForm } from "@inertiajs/react";
|
||||
import { toast } from "sonner";
|
||||
import { Calendar } from "lucide-react";
|
||||
import { getCurrentDate } from "@/utils/format";
|
||||
import { validateInvoiceNumber } from "@/utils/validation";
|
||||
|
||||
export interface UtilityFee {
|
||||
id: number;
|
||||
@@ -84,9 +85,9 @@ export default function UtilityFeeDialog({
|
||||
e.preventDefault();
|
||||
|
||||
if (fee) {
|
||||
// Validate invoice number format if present
|
||||
if (data.invoice_number && !/^[A-Z]{2}-\d{8}$/.test(data.invoice_number)) {
|
||||
toast.error("發票號碼格式錯誤,應為:AB-12345678");
|
||||
const validation = validateInvoiceNumber(data.invoice_number);
|
||||
if (!validation.isValid) {
|
||||
toast.error(validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,9 +102,9 @@ export default function UtilityFeeDialog({
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Validate invoice number format if present
|
||||
if (data.invoice_number && !/^[A-Z]{2}-\d{8}$/.test(data.invoice_number)) {
|
||||
toast.error("發票號碼格式錯誤,應為:AB-12345678");
|
||||
const validation = validateInvoiceNumber(data.invoice_number);
|
||||
if (!validation.isValid) {
|
||||
toast.error(validation.error);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -189,8 +190,9 @@ export default function UtilityFeeDialog({
|
||||
<Input
|
||||
id="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"
|
||||
maxLength={11}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">格式範例:AB-12345678</p>
|
||||
{errors.invoice_number && <p className="text-sm text-red-500">{errors.invoice_number}</p>}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ArrowUp,
|
||||
ArrowDown
|
||||
} from 'lucide-react';
|
||||
import { Label } from "@/Components/ui/label";
|
||||
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||
import { Head, router } from "@inertiajs/react";
|
||||
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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜尋發票、備註..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
className="pl-10"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm("")}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-500">關鍵字搜尋</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
|
||||
<Input
|
||||
placeholder="搜尋發票、備註..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
className="pl-10"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm("")}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<SearchableSelect
|
||||
value={categoryFilter}
|
||||
onValueChange={setCategoryFilter}
|
||||
options={[
|
||||
{ label: "所有類別", value: "all" },
|
||||
...availableCategories.map(c => ({ label: c, value: c }))
|
||||
]}
|
||||
placeholder="篩選類別"
|
||||
/>
|
||||
|
||||
{/* Date Range Start */}
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={dateStart}
|
||||
onChange={(e) => setDateStart(e.target.value)}
|
||||
className="pl-9 bg-white block w-full"
|
||||
placeholder="開始日期"
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-500">開始日期</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={dateStart}
|
||||
onChange={(e) => setDateStart(e.target.value)}
|
||||
className="pl-9 bg-white block w-full"
|
||||
placeholder="開始日期"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Range End */}
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={dateEnd}
|
||||
onChange={(e) => setDateEnd(e.target.value)}
|
||||
className="pl-9 bg-white block w-full"
|
||||
placeholder="結束日期"
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs text-gray-500">結束日期</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
|
||||
<Input
|
||||
type="date"
|
||||
value={dateEnd}
|
||||
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>
|
||||
@@ -259,13 +272,13 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClearFilters}
|
||||
className="button-outlined-primary h-9"
|
||||
className="button-outlined-primary h-9 mt-auto"
|
||||
>
|
||||
清除所有
|
||||
</Button>
|
||||
<Button
|
||||
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" /> 執行篩選
|
||||
</Button>
|
||||
@@ -294,6 +307,14 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
|
||||
費用類別 <SortIcon field="category" />
|
||||
</button>
|
||||
</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">
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
@@ -304,14 +325,6 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
|
||||
</button>
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<button
|
||||
onClick={() => handleSort('invoice_number')}
|
||||
className="flex items-center hover:text-gray-900"
|
||||
>
|
||||
發票號碼 <SortIcon field="invoice_number" />
|
||||
</button>
|
||||
</TableHead>
|
||||
<TableHead>說明 / 備註</TableHead>
|
||||
<TableHead className="text-center w-[120px]">操作</TableHead>
|
||||
</TableRow>
|
||||
@@ -340,12 +353,12 @@ export default function UtilityFeeIndex({ fees, availableCategories, filters }:
|
||||
{fee.category}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-bold text-gray-900">
|
||||
$ {Number(fee.amount).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm text-gray-600">
|
||||
{formatInvoiceNumber(fee.invoice_number)}
|
||||
</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}>
|
||||
{fee.description || '-'}
|
||||
</TableCell>
|
||||
|
||||
@@ -70,3 +70,20 @@ export const validateWarehouse = (formData: {
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user