登入驗證以及使用者按鈕
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 55s

This commit is contained in:
2026-01-07 14:44:01 +08:00
parent 19c60a6126
commit ef1fc47cff
10 changed files with 380 additions and 69 deletions

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
/**
* Display the login view.
*/
public function show()
{
return Inertia::render('Auth/Login');
}
/**
* Handle an incoming authentication request.
*/
public function store(Request $request)
{
$request->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('/');
}
}

View File

@@ -37,7 +37,9 @@ class HandleInertiaRequests extends Middleware
{ {
return [ return [
...parent::share($request), ...parent::share($request),
// 'auth' => [
'user' => $request->user(),
],
]; ];
} }
} }

View File

@@ -20,6 +20,7 @@ class User extends Authenticatable
protected $fillable = [ protected $fillable = [
'name', 'name',
'email', 'email',
'username',
'password', 'password',
]; ];

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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();
});
}
};

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,11 @@
import { ImgHTMLAttributes } from 'react';
export default function ApplicationLogo(props: ImgHTMLAttributes<HTMLImageElement>) {
return (
<img
{...props}
src="/logo.png"
alt="小小冰室 Logo"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { HTMLAttributes } from 'react';
import { cn } from '@/lib/utils';
export default function InputError({ message, className = '', ...props }: HTMLAttributes<HTMLParagraphElement> & { message?: string }) {
return message ? (
<p {...props} className={cn('text-sm text-red-600', className)}>
{message}
</p>
) : null;
}

View File

@@ -1,5 +1,4 @@
import { import {
ChevronDown,
ChevronRight, ChevronRight,
Package, Package,
ShoppingCart, ShoppingCart,
@@ -11,13 +10,24 @@ import {
Warehouse, Warehouse,
Truck, Truck,
Contact2, Contact2,
FileText FileText,
LogOut,
User,
ChevronDown
} from "lucide-react"; } from "lucide-react";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Link, usePage } from "@inertiajs/react"; import { Link, usePage } from "@inertiajs/react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav"; import BreadcrumbNav, { BreadcrumbItemType } from "@/Components/shared/BreadcrumbNav";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
interface MenuItem { interface MenuItem {
id: string; id: string;
@@ -34,7 +44,9 @@ export default function AuthenticatedLayout({
children: React.ReactNode, children: React.ReactNode,
breadcrumbs?: BreadcrumbItemType[] breadcrumbs?: BreadcrumbItemType[]
}) { }) {
const { url } = usePage(); const { url, props } = usePage();
// @ts-ignore
const user = props.auth?.user || { name: 'Guest', username: 'guest' };
const [isCollapsed, setIsCollapsed] = useState(() => { const [isCollapsed, setIsCollapsed] = useState(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
return localStorage.getItem("sidebar-collapsed") === "true"; return localStorage.getItem("sidebar-collapsed") === "true";
@@ -243,6 +255,38 @@ export default function AuthenticatedLayout({
<span className="font-bold text-slate-900"> ERP</span> <span className="font-bold text-slate-900"> ERP</span>
</Link> </Link>
</div> </div>
{/* User Menu */}
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="flex items-center gap-2 outline-none group">
<div className="hidden md:flex flex-col items-end mr-1">
<span className="text-sm font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
{user.name}
</span>
<span className="text-xs text-slate-500">
{user.username || 'Administrator'}
</span>
</div>
<div className="h-9 w-9 bg-slate-100 rounded-full flex items-center justify-center text-slate-600 group-hover:bg-primary-lightest group-hover:text-primary-main transition-all">
<User className="h-5 w-5" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56 z-[100]" sideOffset={8}>
<DropdownMenuLabel></DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
href={route('logout')}
method="post"
as="button"
className="w-full flex items-center cursor-pointer text-red-600 focus:text-red-600 focus:bg-red-50"
>
<LogOut className="mr-2 h-4 w-4" />
<span></span>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header> </header>
{/* Sidebar Desktop */} {/* Sidebar Desktop */}
@@ -281,15 +325,17 @@ export default function AuthenticatedLayout({
{isCollapsed ? <PanelLeftOpen className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />} {isCollapsed ? <PanelLeftOpen className="h-5 w-5" /> : <PanelLeftClose className="h-5 w-5" />}
</button> </button>
</div> </div>
</aside> </aside >
{/* Mobile Sidebar Overlay */} {/* Mobile Sidebar Overlay */}
{isMobileOpen && ( {
<div isMobileOpen && (
className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[70] lg:hidden" <div
onClick={() => setIsMobileOpen(false)} className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm z-[70] lg:hidden"
/> onClick={() => setIsMobileOpen(false)}
)} />
)
}
{/* Mobile Sidebar Drawer */} {/* Mobile Sidebar Drawer */}
<aside className={cn( <aside className={cn(
@@ -329,6 +375,6 @@ export default function AuthenticatedLayout({
</div> </div>
<Toaster richColors closeButton position="top-center" /> <Toaster richColors closeButton position="top-center" />
</main> </main>
</div> </div >
); );
} }

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50 relative overflow-hidden">
<Head title="登入" />
{/* 動態背景裝飾 */}
<div className="absolute top-0 -left-4 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
<div className="absolute top-0 -right-4 w-72 h-72 bg-yellow-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
<div className="absolute -bottom-8 left-20 w-72 h-72 bg-pink-300 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
<div className="w-full max-w-md p-8 relative z-10">
<div className="flex flex-col items-center mb-6">
<ApplicationLogo className="w-40 h-40 object-contain" />
</div>
<div className="glass-panel p-8 rounded-2xl shadow-xl bg-white/80 backdrop-blur-md border border-white/50">
<form onSubmit={submit} className="space-y-6">
<div>
<Label htmlFor="username"></Label>
<Input
id="username"
type="text"
className="mt-1 block w-full bg-white/50"
value={data.username}
onChange={(e) => setData("username", e.target.value)}
required
autoFocus
/>
<InputError message={errors.username} className="mt-2" />
</div>
<div>
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
className="mt-1 block w-full bg-white/50"
value={data.password}
onChange={(e) => setData("password", e.target.value)}
required
/>
<InputError message={errors.password} className="mt-2" />
</div>
<div className="flex items-center justify-between">
<label className="flex items-center cursor-pointer group">
<div className="relative">
<input
type="checkbox"
className="sr-only"
checked={data.rememberUsername}
onChange={(e) => setData("rememberUsername", e.target.checked)}
/>
<div className={cn(
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
data.rememberUsername ? "bg-[#01ab83]" : "bg-gray-300"
)}></div>
<div className={cn(
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
data.rememberUsername ? "translate-x-4" : "translate-x-0"
)}></div>
</div>
<span className="ml-2 text-sm text-gray-600 group-hover:text-gray-900 transition-colors"></span>
</label>
<label className="flex items-center cursor-pointer group">
<span className="mr-2 text-sm text-gray-600 group-hover:text-gray-900 transition-colors"></span>
<div className="relative">
<input
type="checkbox"
className="sr-only"
checked={data.remember}
onChange={(e) => setData("remember", e.target.checked)}
/>
<div className={cn(
"w-9 h-5 rounded-full shadow-inner transition-colors duration-300 ease-in-out",
data.remember ? "bg-[#01ab83]" : "bg-gray-300"
)}></div>
<div className={cn(
"absolute left-0.5 top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform duration-300 ease-in-out",
data.remember ? "translate-x-4" : "translate-x-0"
)}></div>
</div>
</label>
</div>
<Button
className="w-full h-11 text-base bg-[#01ab83] hover:bg-[#018a6a] transition-all shadow-lg hover:shadow-xl"
disabled={processing}
>
{processing ? "登入中..." : "登入系統"}
</Button>
</form>
</div>
<p className="text-center text-gray-400 text-sm mt-8">
&copy; 2026 . All rights reserved.
</p>
</div>
</div>
);
}

View File

@@ -5,66 +5,77 @@ use Inertia\Inertia;
use App\Http\Controllers\CategoryController; use App\Http\Controllers\CategoryController;
use App\Http\Controllers\VendorController; use App\Http\Controllers\VendorController;
use App\Http\Controllers\VendorProductController; use App\Http\Controllers\VendorProductController;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
use App\Http\Controllers\ProductController; use App\Http\Controllers\ProductController;
use App\Http\Controllers\Auth\LoginController;
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\PurchaseOrderController; 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('/login', [LoginController::class, 'show'])->name('login');
Route::get('/purchase-orders/create', [PurchaseOrderController::class, 'create'])->name('purchase-orders.create'); Route::post('/login', [LoginController::class, 'store']);
Route::post('/purchase-orders', [PurchaseOrderController::class, 'store'])->name('purchase-orders.store'); Route::post('/logout', [LoginController::class, 'destroy'])->name('logout');
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::middleware('auth')->group(function () {
Route::get('/vendors', [VendorController::class, 'index'])->name('vendors.index'); Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
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::get('/categories', [CategoryController::class, 'index'])->name('categories.index');
Route::put('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'update'])->name('vendors.products.update'); Route::post('/categories', [CategoryController::class, 'store'])->name('categories.store');
Route::delete('/vendors/{vendor}/products/{product}', [VendorProductController::class, 'destroy'])->name('vendors.products.destroy'); 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