優化公共事業費:修正日期顯示、改善發票號碼輸入UX與調整介面欄位順序
This commit is contained in:
@@ -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',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user