diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..47359ce --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,60 @@ +validate([ + 'username' => ['required', 'string'], + 'password' => ['required', 'string'], + ], [ + 'username.required' => '請輸入帳號', + 'password.required' => '請輸入密碼', + ]); + + $credentials = $request->only('username', 'password'); + + if (Auth::attempt($credentials, $request->boolean('remember'))) { + $request->session()->regenerate(); + + return redirect()->intended(route('dashboard')); + } + + throw ValidationException::withMessages([ + 'username' => '帳號或密碼錯誤。', + ]); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request) + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index c19ce18..546321f 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -37,7 +37,9 @@ class HandleInertiaRequests extends Middleware { return [ ...parent::share($request), - // + 'auth' => [ + 'user' => $request->user(), + ], ]; } } diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..b48779b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -20,6 +20,7 @@ class User extends Authenticatable protected $fillable = [ 'name', 'email', + 'username', 'password', ]; diff --git a/database/migrations/2026_01_07_132554_add_username_to_users_table.php b/database/migrations/2026_01_07_132554_add_username_to_users_table.php new file mode 100644 index 0000000..2b9a7ab --- /dev/null +++ b/database/migrations/2026_01_07_132554_add_username_to_users_table.php @@ -0,0 +1,30 @@ +string('username')->unique()->after('name'); + $table->string('email')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('username'); + $table->string('email')->nullable(false)->change(); + }); + } +}; diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..8d08d2d Binary files /dev/null and b/public/logo.png differ diff --git a/resources/js/Components/ApplicationLogo.tsx b/resources/js/Components/ApplicationLogo.tsx new file mode 100644 index 0000000..686fe6d --- /dev/null +++ b/resources/js/Components/ApplicationLogo.tsx @@ -0,0 +1,11 @@ +import { ImgHTMLAttributes } from 'react'; + +export default function ApplicationLogo(props: ImgHTMLAttributes) { + return ( + 小小冰室 Logo + ); +} diff --git a/resources/js/Components/InputError.tsx b/resources/js/Components/InputError.tsx new file mode 100644 index 0000000..593d1ba --- /dev/null +++ b/resources/js/Components/InputError.tsx @@ -0,0 +1,10 @@ +import { HTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +export default function InputError({ message, className = '', ...props }: HTMLAttributes & { message?: string }) { + return message ? ( +

+ {message} +

+ ) : null; +} diff --git a/resources/js/Layouts/AuthenticatedLayout.tsx b/resources/js/Layouts/AuthenticatedLayout.tsx index f9e2c66..a273263 100644 --- a/resources/js/Layouts/AuthenticatedLayout.tsx +++ b/resources/js/Layouts/AuthenticatedLayout.tsx @@ -1,5 +1,4 @@ import { - ChevronDown, ChevronRight, Package, ShoppingCart, @@ -11,13 +10,24 @@ import { Warehouse, Truck, Contact2, - FileText + FileText, + LogOut, + User, + ChevronDown } from "lucide-react"; import { Toaster } from "sonner"; import { useState, useEffect } from "react"; import { Link, usePage } from "@inertiajs/react"; import { cn } from "@/lib/utils"; import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/Components/ui/dropdown-menu"; interface MenuItem { id: string; @@ -34,7 +44,9 @@ export default function AuthenticatedLayout({ children: React.ReactNode, breadcrumbs?: BreadcrumbItemType[] }) { - const { url } = usePage(); + const { url, props } = usePage(); + // @ts-ignore + const user = props.auth?.user || { name: 'Guest', username: 'guest' }; const [isCollapsed, setIsCollapsed] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("sidebar-collapsed") === "true"; @@ -243,6 +255,38 @@ export default function AuthenticatedLayout({ 小小冰室 ERP + + {/* User Menu */} + + +
+ + {user.name} + + + {user.username || 'Administrator'} + +
+
+ +
+
+ + 我的帳號 + + + + + 登出系統 + + + +
{/* Sidebar Desktop */} @@ -281,15 +325,17 @@ export default function AuthenticatedLayout({ {isCollapsed ? : } - + {/* Mobile Sidebar Overlay */} - {isMobileOpen && ( -
setIsMobileOpen(false)} - /> - )} + { + isMobileOpen && ( +
setIsMobileOpen(false)} + /> + ) + } {/* Mobile Sidebar Drawer */}
+
); } diff --git a/resources/js/Pages/Auth/Login.tsx b/resources/js/Pages/Auth/Login.tsx new file mode 100644 index 0000000..d92b91c --- /dev/null +++ b/resources/js/Pages/Auth/Login.tsx @@ -0,0 +1,140 @@ +import { Head, useForm } from "@inertiajs/react"; +import { FormEventHandler, useEffect } from "react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/Components/ui/button"; +import { Input } from "@/Components/ui/input"; +import { Label } from "@/Components/ui/label"; +import InputError from "../../Components/InputError"; +import ApplicationLogo from "../../Components/ApplicationLogo"; + +export default function Login() { + const { data, setData, post, processing, errors, reset } = useForm({ + username: localStorage.getItem("saved_username") || "", + password: "", + remember: false, + rememberUsername: localStorage.getItem("remember_username") === "true", + }); + + useEffect(() => { + return () => { + reset("password"); + }; + }, []); + + const submit: FormEventHandler = (e) => { + e.preventDefault(); + + // 處理記住帳號邏輯 + if (data.rememberUsername) { + localStorage.setItem("saved_username", data.username); + localStorage.setItem("remember_username", "true"); + } else { + localStorage.removeItem("saved_username"); + localStorage.setItem("remember_username", "false"); + } + + post(route("login"), { + onFinish: () => reset("password"), + }); + }; + + return ( +
+ + + {/* 動態背景裝飾 */} +
+
+
+ +
+
+ +
+
+
+
+ + setData("username", e.target.value)} + required + autoFocus + /> + +
+ +
+ + setData("password", e.target.value)} + required + /> + +
+ +
+ + + +
+ + +
+
+ +

+ © 2026 小小冰室. All rights reserved. +

+
+
+ ); +} diff --git a/routes/web.php b/routes/web.php index e826813..c9c2b69 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,66 +5,77 @@ use Inertia\Inertia; use App\Http\Controllers\CategoryController; use App\Http\Controllers\VendorController; use App\Http\Controllers\VendorProductController; - use App\Http\Controllers\DashboardController; - -Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); - use App\Http\Controllers\ProductController; - -Route::get('/products', [ProductController::class, 'index'])->name('products.index'); -Route::post('/products', [ProductController::class, 'store'])->name('products.store'); -Route::put('/products/{product}', [ProductController::class, 'update'])->name('products.update'); -Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy'); - -Route::post('/categories', [CategoryController::class, 'store'])->name('categories.store'); -Route::put('/categories/{category}', [CategoryController::class, 'update'])->name('categories.update'); -Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->name('categories.destroy'); - - -// 倉庫管理 -Route::resource('warehouses', \App\Http\Controllers\WarehouseController::class); - -// 庫存管理 -Route::get('warehouses/{warehouse}/inventory', [\App\Http\Controllers\InventoryController::class, 'index'])->name('warehouses.inventory.index'); - -// 安全庫存管理 -Route::prefix('warehouses/{warehouse}/safety-stock-settings')->name('warehouses.safety-stock.')->group(function () { - Route::get('/', [\App\Http\Controllers\SafetyStockController::class, 'index'])->name('index'); - Route::post('/', [\App\Http\Controllers\SafetyStockController::class, 'store'])->name('store'); - Route::put('/{inventory}', [\App\Http\Controllers\SafetyStockController::class, 'update'])->name('update'); - Route::delete('/{inventory}', [\App\Http\Controllers\SafetyStockController::class, 'destroy'])->name('destroy'); -}); - -Route::get('/warehouses/{warehouse}/add-inventory', [\App\Http\Controllers\InventoryController::class, 'create'])->name('warehouses.add-inventory'); -Route::post('/warehouses/{warehouse}/inventory', [\App\Http\Controllers\InventoryController::class, 'store'])->name('warehouses.inventory.store'); -Route::get('/warehouses/{warehouse}/inventory/{inventory}/edit', [\App\Http\Controllers\InventoryController::class, 'edit'])->name('warehouses.inventory.edit'); -Route::put('/warehouses/{warehouse}/inventory/{inventory}', [\App\Http\Controllers\InventoryController::class, 'update'])->name('warehouses.inventory.update'); -Route::delete('/warehouses/{warehouse}/inventory/{inventory}', [\App\Http\Controllers\InventoryController::class, 'destroy'])->name('warehouses.inventory.destroy'); -Route::get('/warehouses/{warehouse}/inventory/{inventory}/history', [\App\Http\Controllers\InventoryController::class, 'history'])->name('warehouses.inventory.history'); - -// 撥補單 (Transfer Order) -Route::post('/transfer-orders', [\App\Http\Controllers\TransferOrderController::class, 'store'])->name('transfer-orders.store'); -Route::get('/api/warehouses/{warehouse}/inventories', [\App\Http\Controllers\TransferOrderController::class, 'getWarehouseInventories'])->name('api.warehouses.inventories'); - +use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\PurchaseOrderController; +use App\Http\Controllers\WarehouseController; +use App\Http\Controllers\InventoryController; +use App\Http\Controllers\SafetyStockController; +use App\Http\Controllers\TransferOrderController; -Route::get('/purchase-orders', [PurchaseOrderController::class, 'index'])->name('purchase-orders.index'); -Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create'); -Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store'); -Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show'); -Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->name('purchase-orders.edit'); -Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->name('purchase-orders.update'); -Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->name('purchase-orders.destroy'); +Route::get('/login', [LoginController::class, 'show'])->name('login'); +Route::post('/login', [LoginController::class, 'store']); +Route::post('/logout', [LoginController::class, 'destroy'])->name('logout'); -// 廠商管理 -Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index'); -Route::get('/vendors/{vendor}', [VendorController::class, 'show'])->name('vendors.show'); -Route::post('/vendors', [VendorController::class, 'store'])->name('vendors.store'); -Route::put('/vendors/{vendor}', [VendorController::class, 'update'])->name('vendors.update'); -Route::delete('/vendors/{vendor}', [VendorController::class, 'destroy'])->name('vendors.destroy'); +Route::middleware('auth')->group(function () { + Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); -// 供貨商品相關路由 -Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->name('vendors.products.store'); -Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->name('vendors.products.update'); -Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->name('vendors.products.destroy'); + // 類別管理 (用於商品對話框) + Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index'); + Route::post('/categories', [CategoryController::class, 'store'])->name('categories.store'); + Route::put('/categories/{category}', [CategoryController::class, 'update'])->name('categories.update'); + Route::delete('/categories/{category}', [CategoryController::class, 'destroy'])->name('categories.destroy'); + + // 商品管理 + Route::get('/products', [ProductController::class, 'index'])->name('products.index'); + Route::post('/products', [ProductController::class, 'store'])->name('products.store'); + Route::put('/products/{product}', [ProductController::class, 'update'])->name('products.update'); + + // 廠商管理 + Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index'); + Route::get('/vendors/{vendor}', [VendorController::class, 'show'])->name('vendors.show'); + Route::post('/vendors', [VendorController::class, 'store'])->name('vendors.store'); + Route::put('/vendors/{vendor}', [VendorController::class, 'update'])->name('vendors.update'); + Route::delete('/vendors/{vendor}', [VendorController::class, 'destroy'])->name('vendors.destroy'); + + // 供貨商品相關路由 + Route::post('/vendors/{vendor}/products', [VendorProductController::class, 'store'])->name('vendors.products.store'); + Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->name('vendors.products.update'); + Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->name('vendors.products.destroy'); + + // 倉庫管理 + Route::get('/warehouses', [WarehouseController::class, 'index'])->name('warehouses.index'); + Route::post('/warehouses', [WarehouseController::class, 'store'])->name('warehouses.store'); + Route::put('/warehouses/{warehouse}', [WarehouseController::class, 'update'])->name('warehouses.update'); + Route::delete('/warehouses/{warehouse}', [WarehouseController::class, 'destroy'])->name('warehouses.destroy'); + + // 倉庫庫存管理 + Route::get('/warehouses/{warehouse}/inventory', [InventoryController::class, 'index'])->name('warehouses.inventory.index'); + Route::get('/warehouses/{warehouse}/inventory/create', [InventoryController::class, 'create'])->name('warehouses.inventory.create'); + Route::post('/warehouses/{warehouse}/inventory', [InventoryController::class, 'store'])->name('warehouses.inventory.store'); + Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/edit', [InventoryController::class, 'edit'])->name('warehouses.inventory.edit'); + Route::put('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'update'])->name('warehouses.inventory.update'); + Route::delete('/warehouses/{warehouse}/inventory/{inventoryId}', [InventoryController::class, 'destroy'])->name('warehouses.inventory.destroy'); + Route::get('/warehouses/{warehouse}/inventory/{inventoryId}/history', [InventoryController::class, 'history'])->name('warehouses.inventory.history'); + + // 安全庫存設定 + Route::get('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'index'])->name('warehouses.safety-stock.index'); + Route::post('/warehouses/{warehouse}/safety-stock', [SafetyStockController::class, 'store'])->name('warehouses.safety-stock.store'); + Route::put('/warehouses/{warehouse}/safety-stock/{inventory}', [SafetyStockController::class, 'update'])->name('warehouses.safety-stock.update'); + Route::delete('/warehouses/{warehouse}/safety-stock/{inventory}', [SafetyStockController::class, 'destroy'])->name('warehouses.safety-stock.destroy'); + + // 採購單管理 + Route::get('/purchase-orders', [PurchaseOrderController::class, 'index'])->name('purchase-orders.index'); + Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create'); + Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store'); + Route::get('/purchase-orders/{id}', [PurchaseOrderController::class, 'show'])->name('purchase-orders.show'); + Route::get('/purchase-orders/{id}/edit', [PurchaseOrderController::class, 'edit'])->name('purchase-orders.edit'); + Route::put('/purchase-orders/{id}', [PurchaseOrderController::class, 'update'])->name('purchase-orders.update'); + Route::delete('/purchase-orders/{id}', [PurchaseOrderController::class, 'destroy'])->name('purchase-orders.destroy'); + + // 撥補單 (在庫存調撥時使用) + Route::post('/transfer-orders', [TransferOrderController::class, 'store'])->name('transfer-orders.store'); + Route::get('/api/warehouses/{warehouse}/inventories', [TransferOrderController::class, 'getWarehouseInventories'])->name('api.warehouses.inventories'); + +}); // End of auth middleware group