From 7367577f6a4053a524105082c3921587922f58b8 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Mon, 19 Jan 2026 17:07:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B5=B1=E4=B8=80=E6=8E=A1=E8=B3=BC?= =?UTF-8?q?=E5=96=AE=E8=88=87=E6=93=8D=E4=BD=9C=E7=B4=80=E9=8C=84=20UI?= =?UTF-8?q?=E3=80=81=E5=A2=9E=E5=BC=B7=E5=90=84=E6=A8=A1=E7=B5=84=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E7=B4=80=E9=8C=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 統一採購單篩選列與表單樣式 (移除舊元件、標準化 Input) - 增強操作紀錄功能 (加入篩選、快照、詳細異動比對) - 統一刪除確認視窗與按鈕樣式 - 修復庫存編輯頁面樣式 - 實作採購單品項異動紀錄 - 實作角色分配異動紀錄 - 擴充供應商與倉庫模組紀錄 --- .../Admin/ActivityLogController.php | 81 +++++- app/Http/Controllers/Admin/UserController.php | 2 +- .../Controllers/PurchaseOrderController.php | 11 +- .../ActivityLog/ActivityDetailDialog.tsx | 4 +- .../js/Components/ActivityLog/LogTable.tsx | 2 +- .../Components/PurchaseOrder/DateFilter.tsx | 176 ------------- .../PurchaseOrder/PurchaseOrderFilters.tsx | 135 ---------- .../PurchaseOrder/PurchaseOrderItemsTable.tsx | 6 +- .../js/Pages/Admin/ActivityLog/Index.tsx | 176 ++++++++++++- resources/js/Pages/Admin/Role/Index.tsx | 50 +++- resources/js/Pages/Admin/User/Index.tsx | 61 ++++- resources/js/Pages/Product/Index.tsx | 8 +- resources/js/Pages/PurchaseOrder/Create.tsx | 10 +- resources/js/Pages/PurchaseOrder/Index.tsx | 245 ++++++++++++------ resources/js/Pages/Vendor/Index.tsx | 8 +- .../js/Pages/Warehouse/EditInventory.tsx | 10 +- 16 files changed, 541 insertions(+), 444 deletions(-) delete mode 100644 resources/js/Components/PurchaseOrder/DateFilter.tsx delete mode 100644 resources/js/Components/PurchaseOrder/PurchaseOrderFilters.tsx diff --git a/app/Http/Controllers/Admin/ActivityLogController.php b/app/Http/Controllers/Admin/ActivityLogController.php index d34fbfe..8338771 100644 --- a/app/Http/Controllers/Admin/ActivityLogController.php +++ b/app/Http/Controllers/Admin/ActivityLogController.php @@ -9,14 +9,64 @@ use Spatie\Activitylog\Models\Activity; class ActivityLogController extends Controller { + private function getSubjectMap() + { + return [ + 'App\Models\User' => '使用者', + 'App\Models\Role' => '角色', + 'App\Models\Product' => '商品', + 'App\Models\Vendor' => '廠商', + 'App\Models\Category' => '商品分類', + 'App\Models\Unit' => '單位', + 'App\Models\PurchaseOrder' => '採購單', + 'App\Models\Warehouse' => '倉庫', + 'App\Models\Inventory' => '庫存', + ]; + } + public function index(Request $request) { $perPage = $request->input('per_page', 10); $sortBy = $request->input('sort_by', 'created_at'); $sortOrder = $request->input('sort_order', 'desc'); + $search = $request->input('search'); + $dateStart = $request->input('date_start'); + $dateEnd = $request->input('date_end'); + $event = $request->input('event'); + $subjectType = $request->input('subject_type'); + $causerId = $request->input('causer_id'); + $query = Activity::with('causer'); + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('description', 'like', "%{$search}%") + ->orWhere('log_name', 'like', "%{$search}%") + ->orWhere('properties', 'like', "%{$search}%"); + }); + } + + if ($dateStart) { + $query->whereDate('created_at', '>=', $dateStart); + } + + if ($dateEnd) { + $query->whereDate('created_at', '<=', $dateEnd); + } + + if ($event) { + $query->where('event', $event); + } + + if ($subjectType) { + $query->where('subject_type', $subjectType); + } + + if ($causerId) { + $query->where('causer_id', $causerId); + } + if ($sortBy === 'created_at') { $query->orderBy($sortBy, $sortOrder); } else { @@ -25,17 +75,7 @@ class ActivityLogController extends Controller $activities = $query->paginate($perPage) ->through(function ($activity) { - $subjectMap = [ - 'App\Models\User' => '使用者', - 'App\Models\Role' => '角色', - 'App\Models\Product' => '商品', - 'App\Models\Vendor' => '廠商', - 'App\Models\Category' => '商品分類', - 'App\Models\Unit' => '單位', - 'App\Models\PurchaseOrder' => '採購單', - 'App\Models\Warehouse' => '倉庫', - 'App\Models\Inventory' => '庫存', - ]; + $subjectMap = $this->getSubjectMap(); $eventMap = [ 'created' => '新增', @@ -54,13 +94,32 @@ class ActivityLogController extends Controller ]; }); + // Prepare subject types for frontend filter + $subjectTypes = collect($this->getSubjectMap())->map(function ($label, $value) { + return ['label' => $label, 'value' => $value]; + })->values(); + + // Get users for causer filter + $users = \App\Models\User::select('id', 'name')->orderBy('name')->get() + ->map(function ($user) { + return ['label' => $user->name, 'value' => (string) $user->id]; + }); + return Inertia::render('Admin/ActivityLog/Index', [ 'activities' => $activities, 'filters' => [ 'per_page' => $request->input('per_page', '10'), 'sort_by' => $request->input('sort_by'), 'sort_order' => $request->input('sort_order'), + 'search' => $request->input('search'), + 'date_start' => $request->input('date_start'), + 'date_end' => $request->input('date_end'), + 'event' => $request->input('event'), + 'subject_type' => $request->input('subject_type'), + 'causer_id' => $request->input('causer_id'), ], + 'subject_types' => $subjectTypes, + 'users' => $users, ]); } } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index be159cd..8a41d80 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -86,7 +86,7 @@ class UserController extends Controller if ($activity) { $roleNames = $user->roles()->pluck('display_name')->join(', '); - $properties = $activity->properties; + $properties = $activity->properties->toArray(); $properties['attributes']['role_id'] = $roleNames; $activity->properties = $properties; $activity->save(); diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 03720ae..6ff462b 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -34,6 +34,15 @@ class PurchaseOrderController extends Controller $query->where('warehouse_id', $request->warehouse_id); } + // Date Range + if ($request->date_start) { + $query->whereDate('created_at', '>=', $request->date_start); + } + + if ($request->date_end) { + $query->whereDate('created_at', '<=', $request->date_end); + } + // Sorting $sortField = $request->sort_field ?? 'id'; $sortDirection = $request->sort_direction ?? 'desc'; @@ -48,7 +57,7 @@ class PurchaseOrderController extends Controller return Inertia::render('PurchaseOrder/Index', [ 'orders' => $orders, - 'filters' => $request->only(['search', 'status', 'warehouse_id', 'sort_field', 'sort_direction', 'per_page']), + 'filters' => $request->only(['search', 'status', 'warehouse_id', 'date_start', 'date_end', 'sort_field', 'sort_direction', 'per_page']), 'warehouses' => Warehouse::all(['id', 'name']), ]); } diff --git a/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx b/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx index b524e5d..44035ed 100644 --- a/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx +++ b/resources/js/Components/ActivityLog/ActivityDetailDialog.tsx @@ -64,6 +64,8 @@ const fieldLabels: Record = { phone: '電話', address: '地址', role_id: '角色', + email_verified_at: '電子郵件驗證時間', + remember_token: '登入權杖', // Snapshot fields category_name: '分類名稱', base_unit_name: '基本單位名稱', @@ -132,7 +134,7 @@ export default function ActivityDetailDialog({ open, onOpenChange, activity }: P // Filter out internal keys often logged but not useful for users const filteredKeys = allKeys .filter(key => - !['created_at', 'updated_at', 'deleted_at', 'id'].includes(key) + !['created_at', 'updated_at', 'deleted_at', 'id', 'remember_token'].includes(key) ) .sort((a, b) => { const indexA = sortOrder.indexOf(a); diff --git a/resources/js/Components/ActivityLog/LogTable.tsx b/resources/js/Components/ActivityLog/LogTable.tsx index c49778c..ed6bcb9 100644 --- a/resources/js/Components/ActivityLog/LogTable.tsx +++ b/resources/js/Components/ActivityLog/LogTable.tsx @@ -156,7 +156,7 @@ export default function LogTable({ 操作人員 描述 動作 - 對象 + 操作對象 操作 diff --git a/resources/js/Components/PurchaseOrder/DateFilter.tsx b/resources/js/Components/PurchaseOrder/DateFilter.tsx deleted file mode 100644 index 0dfd727..0000000 --- a/resources/js/Components/PurchaseOrder/DateFilter.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/** - * 日期篩選器元件 - * 支援快捷日期範圍選項和自定義日期範圍 - */ - -import { Calendar } from "lucide-react"; -import { Label } from "@/Components/ui/label"; -import { Input } from "@/Components/ui/input"; -import { Button } from "@/Components/ui/button"; - -export interface DateRange { - start: string; // YYYY-MM-DD 格式 - end: string; // YYYY-MM-DD 格式 -} - -interface DateFilterProps { - dateRange: DateRange | null; - onDateRangeChange: (range: DateRange | null) => void; -} - -// 格式化日期為 YYYY-MM-DD -function formatDate(date: Date): string { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - return `${year}-${month}-${day}`; -} - -// 獲取從今天往前 N 天的日期範圍 -function getDateRange(days: number): DateRange { - const end = new Date(); - const start = new Date(); - start.setDate(start.getDate() - days); - - return { - start: formatDate(start), - end: formatDate(end), - }; -} - -// 獲取本月的日期範圍 -function getCurrentMonth(): DateRange { - const now = new Date(); - const start = new Date(now.getFullYear(), now.getMonth(), 1); - const end = new Date(now.getFullYear(), now.getMonth() + 1, 0); - - return { - start: formatDate(start), - end: formatDate(end), - }; -} - -// 獲取上月的日期範圍 -function getLastMonth(): DateRange { - const now = new Date(); - const start = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const end = new Date(now.getFullYear(), now.getMonth(), 0); - - return { - start: formatDate(start), - end: formatDate(end), - }; -} - -// 快捷日期選項 -const DATE_SHORTCUTS = [ - { label: "今天", getValue: () => getDateRange(0) }, - { label: "最近7天", getValue: () => getDateRange(7) }, - { label: "最近30天", getValue: () => getDateRange(30) }, - { label: "本月", getValue: () => getCurrentMonth() }, - { label: "上月", getValue: () => getLastMonth() }, -]; - -export function DateFilter({ dateRange, onDateRangeChange }: DateFilterProps) { - const handleStartDateChange = (value: string) => { - if (!value) { - onDateRangeChange(null); - return; - } - - onDateRangeChange({ - start: value, - end: dateRange?.end || value, - }); - }; - - const handleEndDateChange = (value: string) => { - if (!value) { - onDateRangeChange(null); - return; - } - - onDateRangeChange({ - start: dateRange?.start || value, - end: value, - }); - }; - - const handleShortcutClick = (getValue: () => DateRange) => { - onDateRangeChange(getValue()); - }; - - const handleClearClick = () => { - onDateRangeChange(null); - }; - - return ( -
- {/* 標題 */} -
- - -
- - {/* 快捷選項 */} -
- {DATE_SHORTCUTS.map((shortcut) => ( - - ))} -
- - {/* 自定義日期範圍 */} -
-
- {/* 開始日期 */} -
- - handleStartDateChange(e.target.value)} - className="border-gray-200 focus:border-primary" - /> -
- - {/* 結束日期 */} -
- - handleEndDateChange(e.target.value)} - className="border-gray-200 focus:border-primary" - /> -
-
- - {/* 清除按鈕 */} - {dateRange && ( - - )} -
-
- ); -} diff --git a/resources/js/Components/PurchaseOrder/PurchaseOrderFilters.tsx b/resources/js/Components/PurchaseOrder/PurchaseOrderFilters.tsx deleted file mode 100644 index 8a2c033..0000000 --- a/resources/js/Components/PurchaseOrder/PurchaseOrderFilters.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/** - * 採購單篩選器元件 - */ - -import { useState } from "react"; -import { Search, X, Filter, ChevronDown, ChevronUp } from "lucide-react"; -import { Button } from "@/Components/ui/button"; -import { Input } from "@/Components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/Components/ui/select"; -import { DateFilter, type DateRange } from "./DateFilter"; - -interface PurchaseOrderFiltersProps { - searchQuery: string; - statusFilter: string; - requesterFilter: string; - warehouses: { id: string | number; name: string }[]; - dateRange: DateRange | null; - onSearchChange: (value: string) => void; - onStatusChange: (value: string) => void; - onRequesterChange: (value: string) => void; - onDateRangeChange: (range: DateRange | null) => void; - onClearFilters: () => void; - hasActiveFilters: boolean; -} - -export function PurchaseOrderFilters({ - searchQuery, - statusFilter, - requesterFilter, - warehouses, - dateRange, - onSearchChange, - onStatusChange, - onRequesterChange, - onDateRangeChange, - onClearFilters, - hasActiveFilters, -}: PurchaseOrderFiltersProps) { - const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); - - return ( -
- {/* 主要篩選列 */} -
- {/* 搜尋框 */} -
- - onSearchChange(e.target.value)} - className="h-10 pl-10 border-gray-200 focus:border-primary" - /> -
- - {/* 快速篩選區 */} -
- {/* 狀態篩選 */} -
- - -
- - {/* 倉庫篩選 */} - - - {/* 進階篩選按鈕 */} - - - {/* 清除篩選按鈕 */} - {hasActiveFilters && ( - - )} -
-
- - {/* 進階篩選區 */} - {showAdvancedFilters && ( -
- -
- )} -
- ); -} diff --git a/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx b/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx index a938579..fe4c9ea 100644 --- a/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx +++ b/resources/js/Components/PurchaseOrder/PurchaseOrderItemsTable.tsx @@ -105,7 +105,7 @@ export function PurchaseOrderItemsTable({ onItemChange?.(index, "quantity", Math.floor(Number(e.target.value))) } disabled={isDisabled} - className="h-10 text-left border-gray-200 w-24" + className="text-left w-24" /> )} @@ -161,11 +161,11 @@ export function PurchaseOrderItemsTable({ onItemChange?.(index, "subtotal", Number(e.target.value)) } disabled={isDisabled} - className={`h-10 text-left w-32 ${ + className={`text-left w-32 ${ // 如果有數量但沒有金額,顯示錯誤樣式 item.quantity > 0 && (!item.subtotal || item.subtotal <= 0) ? "border-red-400 bg-red-50 focus-visible:ring-red-500" - : "border-gray-200" + : "" }`} /> {/* 錯誤提示 */} diff --git a/resources/js/Pages/Admin/ActivityLog/Index.tsx b/resources/js/Pages/Admin/ActivityLog/Index.tsx index 45ea0a6..fd548cb 100644 --- a/resources/js/Pages/Admin/ActivityLog/Index.tsx +++ b/resources/js/Pages/Admin/ActivityLog/Index.tsx @@ -4,9 +4,13 @@ import { Head, router } from '@inertiajs/react'; import { PageProps } from '@/types/global'; import Pagination from '@/Components/shared/Pagination'; import { SearchableSelect } from "@/Components/ui/searchable-select"; -import { FileText } from 'lucide-react'; +import { FileText, Search, RotateCcw, Calendar } from 'lucide-react'; import LogTable, { Activity } from '@/Components/ActivityLog/LogTable'; import ActivityDetailDialog from '@/Components/ActivityLog/ActivityDetailDialog'; +import { Button } from '@/Components/ui/button'; +import { Input } from '@/Components/ui/input'; +import { Label } from '@/Components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select"; interface PaginationLinks { url: string | null; @@ -27,14 +31,62 @@ interface Props extends PageProps { per_page?: string; sort_by?: string; sort_order?: 'asc' | 'desc'; + search?: string; + date_start?: string; + date_end?: string; + event?: string; + subject_type?: string; + causer_id?: string; }; + subject_types: { label: string; value: string }[]; + users: { label: string; value: string }[]; } -export default function ActivityLogIndex({ activities, filters }: Props) { +export default function ActivityLogIndex({ activities, filters, subject_types, users }: Props) { const [perPage, setPerPage] = useState(filters.per_page || "10"); const [selectedActivity, setSelectedActivity] = useState(null); const [detailOpen, setDetailOpen] = useState(false); + // Filter States + const [search, setSearch] = useState(filters.search || ''); + const [dateStart, setDateStart] = useState(filters.date_start || ''); + const [dateEnd, setDateEnd] = useState(filters.date_end || ''); + const [event, setEvent] = useState(filters.event || 'all'); + const [subjectType, setSubjectType] = useState(filters.subject_type || 'all'); + const [causer, setCauser] = useState(filters.causer_id || 'all'); + + const handleFilter = () => { + router.get( + route('activity-logs.index'), + { + ...filters, + search: search, + date_start: dateStart, + date_end: dateEnd, + event: event === 'all' ? undefined : event, + subject_type: subjectType === 'all' ? undefined : subjectType, + causer_id: causer === 'all' ? undefined : causer, + page: 1 // Reset to first page on filter + }, + { preserveState: true, replace: true } + ); + }; + + const handleReset = () => { + setSearch(''); + setDateStart(''); + setDateEnd(''); + setEvent('all'); + setSubjectType('all'); + setCauser('all'); + + router.get( + route('activity-logs.index'), + { per_page: perPage, sort_by: filters.sort_by, sort_order: filters.sort_order }, + { preserveState: true, replace: true } + ); + }; + const handleViewDetail = (activity: Activity) => { setSelectedActivity(activity); setDetailOpen(true); @@ -91,6 +143,118 @@ export default function ActivityLogIndex({ activities, filters }: Props) { + {/* 篩選區塊 */} +
+
+ {/* 關鍵字搜尋 */} +
+ +
+ + setSearch(e.target.value)} + className="pl-9" + onKeyDown={(e) => e.key === 'Enter' && handleFilter()} + /> +
+
+ + {/* 操作人員 */} +
+ + +
+ + {/* 事件類型 */} +
+ + +
+ + {/* 操作對象 */} +
+ + +
+ + {/* 日期範圍 - 開始 */} +
+ +
+ + setDateStart(e.target.value)} + className="pl-9 block w-full" + /> +
+
+ + {/* 日期範圍 - 結束 */} +
+ +
+ + setDateEnd(e.target.value)} + className="pl-9 block w-full text-left" + /> +
+
+
+ +
+ + +
+
+ -
+
每頁顯示
- +
+ +
diff --git a/resources/js/Pages/Admin/Role/Index.tsx b/resources/js/Pages/Admin/Role/Index.tsx index df0e084..399aa29 100644 --- a/resources/js/Pages/Admin/Role/Index.tsx +++ b/resources/js/Pages/Admin/Role/Index.tsx @@ -13,7 +13,6 @@ import { TableRow, } from "@/Components/ui/table"; import { format } from 'date-fns'; -import { toast } from 'sonner'; import { Can } from '@/Components/Permission/Can'; import { useState } from 'react'; import { @@ -23,6 +22,16 @@ import { DialogHeader, DialogTitle, } from "@/Components/ui/dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/Components/ui/alert-dialog"; interface User { id: number; @@ -50,11 +59,23 @@ interface Props { export default function RoleIndex({ roles, filters = {} }: Props) { const [selectedRole, setSelectedRole] = useState(null); + const [deleteId, setDeleteId] = useState(null); + const [deleteName, setDeleteName] = useState(''); + const [modelOpen, setModelOpen] = useState(false); - const handleDelete = (id: number, name: string) => { - if (confirm(`確定要刪除角色「${name}」嗎?此操作無法復原。`)) { - router.delete(route('roles.destroy', id), { - onSuccess: () => toast.success('角色已刪除'), + const confirmDelete = (id: number, name: string) => { + setDeleteId(id); + setDeleteName(name); + setModelOpen(true); + }; + + const handleDelete = () => { + if (deleteId) { + router.delete(route('roles.destroy', deleteId), { + onSuccess: () => { + setModelOpen(false); + }, + onFinish: () => setModelOpen(false), }); } }; @@ -212,7 +233,7 @@ export default function RoleIndex({ roles, filters = {} }: Props) { className="button-outlined-error" title="刪除" disabled={role.users_count > 0} - onClick={() => handleDelete(role.id, role.display_name)} + onClick={() => confirmDelete(role.id, role.display_name)} > @@ -274,6 +295,23 @@ export default function RoleIndex({ roles, filters = {} }: Props) { + + + + + 確定要刪除此角色嗎? + + 您即將刪除角色「{deleteName}」。此操作無法復原,請確認是否繼續。 + + + + 取消 + + 確認刪除 + + + + ); } diff --git a/resources/js/Pages/Admin/User/Index.tsx b/resources/js/Pages/Admin/User/Index.tsx index 52a3ccb..dac2aff 100644 --- a/resources/js/Pages/Admin/User/Index.tsx +++ b/resources/js/Pages/Admin/User/Index.tsx @@ -12,11 +12,20 @@ import { TableRow, } from "@/Components/ui/table"; import { format } from 'date-fns'; -import { toast } from 'sonner'; import { Can } from '@/Components/Permission/Can'; import { cn } from "@/lib/utils"; import Pagination from "@/Components/shared/Pagination"; import { SearchableSelect } from "@/Components/ui/searchable-select"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/Components/ui/alert-dialog"; interface Role { id: number; @@ -54,12 +63,23 @@ interface Props { export default function UserIndex({ users, filters }: Props) { const [perPage, setPerPage] = useState(filters.per_page || "10"); + const [deleteId, setDeleteId] = useState(null); + const [deleteName, setDeleteName] = useState(''); + const [modelOpen, setModelOpen] = useState(false); - const handleDelete = (id: number, name: string) => { - if (confirm(`確定要刪除使用者「${name}」嗎?此操作無法復原。`)) { - router.delete(route('users.destroy', id), { - onSuccess: () => toast.success('使用者已刪除'), - onError: () => toast.error('刪除失敗,請檢查權限'), + const confirmDelete = (id: number, name: string) => { + setDeleteId(id); + setDeleteName(name); + setModelOpen(true); + }; + + const handleDelete = () => { + if (deleteId) { + router.delete(route('users.destroy', deleteId), { + onSuccess: () => { + setModelOpen(false); + }, + onFinish: () => setModelOpen(false), }); } }; @@ -229,7 +249,7 @@ export default function UserIndex({ users, filters }: Props) { size="sm" className="button-outlined-error" title="刪除" - onClick={() => handleDelete(user.id, user.name)} + onClick={() => confirmDelete(user.id, user.name)} > @@ -243,7 +263,7 @@ export default function UserIndex({ users, filters }: Props) { {/* 分頁元件 - 統一樣式 */} -
+
每頁顯示
- +
+ +
- + + + + + 確定要刪除此使用者嗎? + + 您即將刪除使用者「{deleteName}」。此操作無法復原,請確認是否繼續。 + + + + 取消 + + 確認刪除 + + + + + ); } diff --git a/resources/js/Pages/Product/Index.tsx b/resources/js/Pages/Product/Index.tsx index fc03432..df2e319 100644 --- a/resources/js/Pages/Product/Index.tsx +++ b/resources/js/Pages/Product/Index.tsx @@ -260,7 +260,7 @@ export default function ProductManagement({ products, categories, units, filters /> {/* 分頁元件 */} -
+
每頁顯示
- +
+ +
setExpectedDate(e.target.value)} min={getTodayDate()} - className="h-12 border-gray-200" + className="block w-full" />
@@ -267,7 +267,7 @@ export default function CreatePurchaseOrder({ value={notes || ""} onChange={(e) => setNotes(e.target.value)} placeholder="備註這筆採購單的特殊需求..." - className="min-h-[100px] border-gray-200" + className="min-h-[100px]" /> @@ -293,7 +293,7 @@ export default function CreatePurchaseOrder({ onChange={(e) => setInvoiceNumber(e.target.value)} placeholder="AB-12345678" maxLength={11} - className="h-12 border-gray-200" + className="block w-full" />

格式:2 碼英文 + 分隔線 + 8 碼數字

@@ -306,7 +306,7 @@ export default function CreatePurchaseOrder({ type="date" value={invoiceDate} onChange={(e) => setInvoiceDate(e.target.value)} - className="h-12 border-gray-200" + className="block w-full" /> @@ -321,7 +321,7 @@ export default function CreatePurchaseOrder({ placeholder="0" min="0" step="0.01" - className="h-12 border-gray-200" + className="block w-full" /> {invoiceAmount && totalAmount > 0 && parseFloat(invoiceAmount) !== totalAmount && (

diff --git a/resources/js/Pages/PurchaseOrder/Index.tsx b/resources/js/Pages/PurchaseOrder/Index.tsx index 6ea57a5..b801b88 100644 --- a/resources/js/Pages/PurchaseOrder/Index.tsx +++ b/resources/js/Pages/PurchaseOrder/Index.tsx @@ -2,20 +2,26 @@ * 採購單管理主頁面 */ -import { useState, useCallback } from "react"; -import { Plus, ShoppingCart } from 'lucide-react'; +import { useState, useEffect } from "react"; +import { Plus, ShoppingCart, Search, RotateCcw, Calendar } from 'lucide-react'; import { Button } from "@/Components/ui/button"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Head, router } from "@inertiajs/react"; import PurchaseOrderTable from "@/Components/PurchaseOrder/PurchaseOrderTable"; -import { PurchaseOrderFilters } from "@/Components/PurchaseOrder/PurchaseOrderFilters"; -import { type DateRange } from "@/Components/PurchaseOrder/DateFilter"; import type { PurchaseOrder } from "@/types/purchase-order"; -import { debounce } from "lodash"; import Pagination from "@/Components/shared/Pagination"; import { getBreadcrumbs } from "@/utils/breadcrumb"; import { Can } from "@/Components/Permission/Can"; import { SearchableSelect } from "@/Components/ui/searchable-select"; +import { Input } from "@/Components/ui/input"; +import { Label } from "@/Components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/Components/ui/select"; interface Props { orders: { @@ -29,6 +35,8 @@ interface Props { search?: string; status?: string; warehouse_id?: string; + date_start?: string; + date_end?: string; sort_field?: string; sort_direction?: string; per_page?: string; @@ -37,74 +45,70 @@ interface Props { } export default function PurchaseOrderIndex({ orders, filters, warehouses }: Props) { - const [searchQuery, setSearchQuery] = useState(filters.search || ""); - const [statusFilter, setStatusFilter] = useState(filters.status || "all"); - const [requesterFilter, setRequesterFilter] = useState(filters.warehouse_id || "all"); + // 篩選狀態 + const [search, setSearch] = useState(filters.search || ""); + const [status, setStatus] = useState(filters.status || "all"); + const [warehouseId, setWarehouseId] = useState(filters.warehouse_id || "all"); + const [dateStart, setDateStart] = useState(filters.date_start || ""); + const [dateEnd, setDateEnd] = useState(filters.date_end || ""); const [perPage, setPerPage] = useState(filters.per_page || "10"); - const [dateRange, setDateRange] = useState(null); - const handleFilterChange = (newFilters: any) => { - router.get("/purchase-orders", { - ...filters, - ...newFilters, - page: 1, - }, { - preserveState: true, - replace: true, - }); + // 同步 URL 參數到 State (雖有初始值,但若由外部連結進入可確保同步) + useEffect(() => { + setSearch(filters.search || ""); + setStatus(filters.status || "all"); + setWarehouseId(filters.warehouse_id || "all"); + setDateStart(filters.date_start || ""); + setDateEnd(filters.date_end || ""); + setPerPage(filters.per_page || "10"); + }, [filters]); + + const handleFilter = () => { + router.get( + route('purchase-orders.index'), + { + search, + status: status === 'all' ? undefined : status, + warehouse_id: warehouseId === 'all' ? undefined : warehouseId, + date_start: dateStart, + date_end: dateEnd, + per_page: perPage, + sort_field: filters.sort_field, + sort_direction: filters.sort_direction, + }, + { preserveState: true, replace: true } + ); }; - const handleSearch = useCallback( - debounce((value: string) => { - handleFilterChange({ search: value }); - }, 500), - [filters] - ); + const handleReset = () => { + setSearch(""); + setStatus("all"); + setWarehouseId("all"); + setDateStart(""); + setDateEnd(""); - const onSearchChange = (value: string) => { - setSearchQuery(value); - handleSearch(value); - }; - - const onStatusChange = (value: string) => { - setStatusFilter(value); - handleFilterChange({ status: value }); - }; - - const onWarehouseChange = (value: string) => { - setRequesterFilter(value); - handleFilterChange({ warehouse_id: value }); - }; - - const handleClearFilters = () => { - setSearchQuery(""); - setStatusFilter("all"); - setRequesterFilter("all"); - setDateRange(null); - router.get("/purchase-orders"); - }; - - const hasActiveFilters = searchQuery !== "" || statusFilter !== "all" || requesterFilter !== "all" || dateRange !== null; - - const handleNavigateToCreateOrder = () => { - router.get("/purchase-orders/create"); + router.get(route('purchase-orders.index')); }; const handlePerPageChange = (value: string) => { setPerPage(value); - router.get("/purchase-orders", { - ...filters, - per_page: value, - page: 1, - }, { - preserveState: false, - replace: true, - }); + router.get( + route("purchase-orders.index"), + { + ...filters, + per_page: value, + }, + { preserveState: false, replace: true, preserveScroll: true } + ); + }; + + const handleNavigateToCreateOrder = () => { + router.get(route('purchase-orders.create')); }; return ( - +

@@ -129,20 +133,105 @@ export default function PurchaseOrderIndex({ orders, filters, warehouses }: Prop
-
- + {/* 篩選區塊 */} +
+
+ {/* 關鍵字搜尋 */} +
+ +
+ + setSearch(e.target.value)} + className="pl-9" + onKeyDown={(e) => e.key === 'Enter' && handleFilter()} + /> +
+
+ + {/* 狀態篩選 */} +
+ + +
+ + {/* 倉庫篩選 */} +
+ + ({ label: w.name, value: String(w.id) })) + ]} + placeholder="選擇倉庫" + className="w-full" + /> +
+ + {/* 日期範圍 - 開始 */} +
+ +
+ + setDateStart(e.target.value)} + className="pl-9 block w-full" + /> +
+
+ + {/* 日期範圍 - 結束 */} +
+ +
+ + setDateEnd(e.target.value)} + className="pl-9 block w-full text-left" + /> +
+
+
+ +
+ + +
{/* 分頁元件 - 統一樣式 */} -
+
每頁顯示
- +
+ +
diff --git a/resources/js/Pages/Vendor/Index.tsx b/resources/js/Pages/Vendor/Index.tsx index 95e8899..57d77b2 100644 --- a/resources/js/Pages/Vendor/Index.tsx +++ b/resources/js/Pages/Vendor/Index.tsx @@ -202,7 +202,7 @@ export default function VendorManagement({ vendors, filters }: PageProps) { /> {/* 分頁元件 - 統一樣式 */} -
+
每頁顯示
- +
+ +
diff --git a/resources/js/Pages/Warehouse/EditInventory.tsx b/resources/js/Pages/Warehouse/EditInventory.tsx index adf8f9c..9722b5f 100644 --- a/resources/js/Pages/Warehouse/EditInventory.tsx +++ b/resources/js/Pages/Warehouse/EditInventory.tsx @@ -146,7 +146,7 @@ export default function EditInventory({ warehouse, inventory, transactions = [] value={data.batchNumber} onChange={(e) => setData("batchNumber", e.target.value)} placeholder="例:FL20251101" - className="button-outlined-primary" + className="border-gray-300" // 目前後端可能尚未支援儲存,但依需求顯示 />
@@ -172,7 +172,7 @@ export default function EditInventory({ warehouse, inventory, transactions = [] setData("quantity", parseFloat(e.target.value) || 0) } placeholder="0" - className={`button-outlined-primary ${errors.quantity ? "border-red-500" : ""}`} + className={`border-gray-300 ${errors.quantity ? "border-red-500" : ""}`} /> {errors.quantity &&

{errors.quantity}

}

@@ -194,7 +194,7 @@ export default function EditInventory({ warehouse, inventory, transactions = [] type="date" value={data.expiryDate} onChange={(e) => setData("expiryDate", e.target.value)} - className="button-outlined-primary" + className="border-gray-300" />

@@ -207,7 +207,7 @@ export default function EditInventory({ warehouse, inventory, transactions = [] onChange={(e) => setData("lastInboundDate", e.target.value) } - className="button-outlined-primary" + className="border-gray-300" /> @@ -220,7 +220,7 @@ export default function EditInventory({ warehouse, inventory, transactions = [] onChange={(e) => setData("lastOutboundDate", e.target.value) } - className="button-outlined-primary" + className="border-gray-300" />