fix: tenancy middleware order and ui consistency for user profile
This commit is contained in:
55
app/Http/Controllers/Landlord/ProfileController.php
Normal file
55
app/Http/Controllers/Landlord/ProfileController.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Landlord;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 顯示使用者設定頁面
|
||||||
|
*/
|
||||||
|
public function edit(Request $request)
|
||||||
|
{
|
||||||
|
return Inertia::render('Landlord/Profile/Edit', [
|
||||||
|
'user' => $request->user(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新使用者基本資料
|
||||||
|
*/
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'username' => ['required', 'string', 'max:255', 'unique:users,username,' . $request->user()->id],
|
||||||
|
'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users,email,' . $request->user()->id],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update($validated);
|
||||||
|
|
||||||
|
return back()->with('success', '個人資料已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新密碼
|
||||||
|
*/
|
||||||
|
public function updatePassword(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', 'confirmed', Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', '密碼已更新');
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Http/Controllers/ProfileController.php
Normal file
54
app/Http/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 顯示使用者設定頁面
|
||||||
|
*/
|
||||||
|
public function edit(Request $request)
|
||||||
|
{
|
||||||
|
return Inertia::render('Profile/Edit', [
|
||||||
|
'user' => $request->user(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新使用者基本資料
|
||||||
|
*/
|
||||||
|
public function update(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'username' => ['required', 'string', 'max:255', 'unique:users,username,' . $request->user()->id],
|
||||||
|
'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users,email,' . $request->user()->id],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update($validated);
|
||||||
|
|
||||||
|
return back()->with('success', '個人資料已更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新密碼
|
||||||
|
*/
|
||||||
|
public function updatePassword(Request $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'current_password' => ['required', 'current_password'],
|
||||||
|
'password' => ['required', 'confirmed', Password::defaults()],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$request->user()->update([
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return back()->with('success', '密碼已更新');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ class HandleInertiaRequests extends Middleware
|
|||||||
'username' => $user->username ?? null,
|
'username' => $user->username ?? null,
|
||||||
// 權限資料
|
// 權限資料
|
||||||
'roles' => $user->getRoleNames(),
|
'roles' => $user->getRoleNames(),
|
||||||
|
'role_labels' => $user->roles->pluck('display_name'),
|
||||||
'permissions' => $user->getAllPermissions()->pluck('name')->toArray(),
|
'permissions' => $user->getAllPermissions()->pluck('name')->toArray(),
|
||||||
] : null,
|
] : null,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->web(append: [
|
// Tenancy 必須最先執行,確保資料庫連線在 Session 讀取之前建立
|
||||||
|
$middleware->web(prepend: [
|
||||||
\App\Http\Middleware\UniversalTenancy::class,
|
\App\Http\Middleware\UniversalTenancy::class,
|
||||||
|
]);
|
||||||
|
$middleware->web(append: [
|
||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ export default function AuthenticatedLayout({
|
|||||||
{user.name}
|
{user.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-slate-500">
|
<span className="text-xs text-slate-500">
|
||||||
{user.username || 'Administrator'}
|
{user.role_labels?.[0] || user.roles?.[0] || '一般用戶'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<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">
|
||||||
@@ -359,7 +359,17 @@ export default function AuthenticatedLayout({
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-56 z-[100]" sideOffset={8}>
|
<DropdownMenuContent align="end" className="w-56 z-[100]" sideOffset={8}>
|
||||||
<DropdownMenuLabel>我的帳號</DropdownMenuLabel>
|
<DropdownMenuLabel>{user.name} ({user.username})</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={route('profile.edit')}
|
||||||
|
className="w-full flex items-center cursor-pointer text-slate-600 focus:bg-slate-100 focus:text-slate-900 group"
|
||||||
|
>
|
||||||
|
<Settings className="mr-2 h-4 w-4 text-slate-500 group-focus:text-slate-900" />
|
||||||
|
<span>使用者設定</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {
|
|||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogOut,
|
LogOut,
|
||||||
User,
|
User,
|
||||||
|
Menu,
|
||||||
|
X,
|
||||||
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +19,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/Components/ui/dropdown-menu";
|
} from "@/Components/ui/dropdown-menu";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Menu, X } from "lucide-react";
|
|
||||||
|
|
||||||
interface LandlordLayoutProps {
|
interface LandlordLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -114,24 +116,36 @@ export default function LandlordLayout({ children, title }: LandlordLayoutProps)
|
|||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger className="flex items-center gap-2 outline-none group">
|
<DropdownMenuTrigger className="flex items-center gap-2 outline-none group">
|
||||||
<div className="flex flex-col items-end mr-1">
|
<div className="flex flex-col items-end mr-1">
|
||||||
<span className="text-sm font-medium text-slate-700">
|
<span className="text-sm font-medium text-slate-700 group-hover:text-slate-900 transition-colors">
|
||||||
{user.name}
|
{user.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-slate-500">系統管理員</span>
|
<span className="text-xs text-slate-500">
|
||||||
|
{user.role_labels?.[0] || user.roles?.[0] || '系統管理員'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-9 w-9 bg-slate-100 rounded-full flex items-center justify-center">
|
<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 text-slate-600" />
|
<User className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-56 z-[100]">
|
<DropdownMenuContent align="end" className="w-56 z-[100]" sideOffset={8}>
|
||||||
<DropdownMenuLabel>我的帳號</DropdownMenuLabel>
|
<DropdownMenuLabel>{user.name} ({user.username})</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link
|
||||||
|
href={route('landlord.profile.edit')}
|
||||||
|
className="w-full flex items-center cursor-pointer text-slate-600 focus:bg-slate-100 focus:text-slate-900 group"
|
||||||
|
>
|
||||||
|
<Settings className="mr-2 h-4 w-4 text-slate-500 group-focus:text-slate-900" />
|
||||||
|
<span>使用者設定</span>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
href={route('logout')}
|
href={route('logout')}
|
||||||
method="post"
|
method="post"
|
||||||
as="button"
|
as="button"
|
||||||
className="w-full flex items-center cursor-pointer text-red-600"
|
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" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
<span>登出系統</span>
|
<span>登出系統</span>
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ export default function Dashboard({ totalTenants, activeTenants, recentTenants }
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LandlordLayout title="儀表板">
|
<LandlordLayout
|
||||||
|
title="儀表板"
|
||||||
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
|||||||
195
resources/js/Pages/Landlord/Profile/Edit.tsx
Normal file
195
resources/js/Pages/Landlord/Profile/Edit.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import LandlordLayout from "@/Layouts/LandlordLayout";
|
||||||
|
import { Head, useForm } from "@inertiajs/react";
|
||||||
|
import { User, Lock, Mail } from "lucide-react";
|
||||||
|
import { FormEvent } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Edit({ user }: Props) {
|
||||||
|
// 個人資料表單
|
||||||
|
const { data: profileData, setData: setProfileData, patch: patchProfile, processing: profileProcessing, errors: profileErrors } = useForm({
|
||||||
|
name: user.name,
|
||||||
|
username: user.username || "",
|
||||||
|
email: user.email || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 密碼表單
|
||||||
|
const { data: passwordData, setData: setPasswordData, put: putPassword, processing: passwordProcessing, errors: passwordErrors, reset: resetPassword } = useForm({
|
||||||
|
current_password: "",
|
||||||
|
password: "",
|
||||||
|
password_confirmation: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleProfileSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
patchProfile(route('landlord.profile.update'), {
|
||||||
|
onSuccess: () => toast.success('個人資料已更新'),
|
||||||
|
onError: () => toast.error('更新失敗,請檢查輸入內容'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
putPassword(route('landlord.profile.password'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('密碼已更新');
|
||||||
|
resetPassword();
|
||||||
|
},
|
||||||
|
onError: () => toast.error('密碼更新失敗'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LandlordLayout
|
||||||
|
title="使用者設定"
|
||||||
|
>
|
||||||
|
<Head title="使用者設定" />
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
|
{/* 頁面標題 */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
||||||
|
<User className="h-6 w-6 text-primary-main" />
|
||||||
|
使用者設定
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-500 mt-1">管理您的個人資料與帳號安全</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 個人資料區塊 */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||||||
|
<User className="h-5 w-5 text-slate-600" />
|
||||||
|
個人資料
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleProfileSubmit} className="p-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
帳號 (登入用) <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={profileData.username}
|
||||||
|
onChange={(e) => setProfileData("username", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||||
|
placeholder="請輸入登入帳號"
|
||||||
|
/>
|
||||||
|
{profileErrors.username && <p className="mt-1 text-sm text-red-500">{profileErrors.username}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
使用者名稱 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={profileData.name}
|
||||||
|
onChange={(e) => setProfileData("name", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||||
|
/>
|
||||||
|
{profileErrors.name && <p className="mt-1 text-sm text-red-500">{profileErrors.name}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
Email (選填)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={profileData.email}
|
||||||
|
onChange={(e) => setProfileData("email", e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||||
|
placeholder="example@mail.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{profileErrors.email && <p className="mt-1 text-sm text-red-500">{profileErrors.email}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 pt-4 border-t border-slate-200">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={profileProcessing}
|
||||||
|
className="bg-primary-main hover:bg-primary-dark text-white px-6 py-2 rounded-lg disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{profileProcessing ? "儲存中..." : "儲存變更"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 密碼變更區塊 */}
|
||||||
|
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||||||
|
<Lock className="h-5 w-5 text-slate-600" />
|
||||||
|
變更密碼
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handlePasswordSubmit} className="p-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
目前密碼 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.current_password}
|
||||||
|
onChange={(e) => setPasswordData("current_password", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||||
|
/>
|
||||||
|
{passwordErrors.current_password && <p className="mt-1 text-sm text-red-500">{passwordErrors.current_password}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
新密碼 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.password}
|
||||||
|
onChange={(e) => setPasswordData("password", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||||
|
/>
|
||||||
|
{passwordErrors.password && <p className="mt-1 text-sm text-red-500">{passwordErrors.password}</p>}
|
||||||
|
<p className="mt-1 text-sm text-slate-500">密碼至少需要 8 個字元</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
確認新密碼 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.password_confirmation}
|
||||||
|
onChange={(e) => setPasswordData("password_confirmation", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 pt-4 border-t border-slate-200">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={passwordProcessing}
|
||||||
|
className="bg-primary-main hover:bg-primary-dark text-white px-6 py-2 rounded-lg disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{passwordProcessing ? "更新中..." : "更新密碼"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LandlordLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,7 +16,9 @@ export default function TenantCreate() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LandlordLayout title="新增客戶">
|
<LandlordLayout
|
||||||
|
title="新增客戶"
|
||||||
|
>
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold text-slate-900">新增客戶</h1>
|
<h1 className="text-2xl font-bold text-slate-900">新增客戶</h1>
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ export default function TenantEdit({ tenant }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LandlordLayout title="編輯客戶">
|
<LandlordLayout
|
||||||
|
title="編輯客戶"
|
||||||
|
>
|
||||||
<div className="max-w-2xl">
|
<div className="max-w-2xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-bold text-slate-900">編輯客戶</h1>
|
<h1 className="text-2xl font-bold text-slate-900">編輯客戶</h1>
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ export default function TenantIndex({ tenants }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LandlordLayout title="客戶管理">
|
<LandlordLayout
|
||||||
|
title="客戶管理"
|
||||||
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ export default function TenantShow({ tenant }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LandlordLayout title="客戶詳情">
|
<LandlordLayout
|
||||||
|
title="客戶詳情"
|
||||||
|
>
|
||||||
<div className="max-w-3xl space-y-6">
|
<div className="max-w-3xl space-y-6">
|
||||||
{/* Back Link */}
|
{/* Back Link */}
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
205
resources/js/Pages/Profile/Edit.tsx
Normal file
205
resources/js/Pages/Profile/Edit.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout";
|
||||||
|
import { Head, useForm } from "@inertiajs/react";
|
||||||
|
import { User, Lock, Mail } from "lucide-react";
|
||||||
|
import { FormEvent } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/Components/ui/button";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string | null;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Edit({ user }: Props) {
|
||||||
|
// 個人資料表單
|
||||||
|
const { data: profileData, setData: setProfileData, patch: patchProfile, processing: profileProcessing, errors: profileErrors } = useForm({
|
||||||
|
name: user.name,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 密碼表單
|
||||||
|
const { data: passwordData, setData: setPasswordData, put: putPassword, processing: passwordProcessing, errors: passwordErrors, reset: resetPassword } = useForm({
|
||||||
|
current_password: "",
|
||||||
|
password: "",
|
||||||
|
password_confirmation: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleProfileSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
patchProfile(route('profile.update'), {
|
||||||
|
onSuccess: () => toast.success('個人資料已更新'),
|
||||||
|
onError: () => toast.error('更新失敗,請檢查輸入內容'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
putPassword(route('profile.password'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('密碼已更新');
|
||||||
|
resetPassword();
|
||||||
|
},
|
||||||
|
onError: () => toast.error('密碼更新失敗'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticatedLayout
|
||||||
|
breadcrumbs={[
|
||||||
|
{ label: '使用者設定', href: route('profile.edit'), isPage: true },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Head title="使用者設定" />
|
||||||
|
|
||||||
|
<div className="container mx-auto p-6 max-w-7xl space-y-6">
|
||||||
|
{/* 頁面標題 */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
||||||
|
<User className="h-6 w-6 text-primary-main" />
|
||||||
|
使用者設定
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-500 mt-1">管理您的個人資料與帳號安全</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6">
|
||||||
|
{/* 個人資料區塊 */}
|
||||||
|
<section className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50/50">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800 flex items-center gap-2">
|
||||||
|
<User className="h-5 w-5 text-slate-400" />
|
||||||
|
個人資料
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleProfileSubmit} className="p-6 space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
登入帳號 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={profileData.username}
|
||||||
|
onChange={(e) => setProfileData("username", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all"
|
||||||
|
placeholder="請輸入登入帳號"
|
||||||
|
/>
|
||||||
|
{profileErrors.username && <p className="mt-1 text-sm text-red-500">{profileErrors.username}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
使用者姓名 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={profileData.name}
|
||||||
|
onChange={(e) => setProfileData("name", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all"
|
||||||
|
placeholder="請輸入姓名"
|
||||||
|
/>
|
||||||
|
{profileErrors.name && <p className="mt-1 text-sm text-red-500">{profileErrors.name}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
電子郵件 (選填)
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={profileData.email}
|
||||||
|
onChange={(e) => setProfileData("email", e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all"
|
||||||
|
placeholder="example@mail.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{profileErrors.email && <p className="mt-1 text-sm text-red-500">{profileErrors.email}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={profileProcessing}
|
||||||
|
className="button-filled-primary"
|
||||||
|
>
|
||||||
|
{profileProcessing ? "儲存中..." : "儲存變更"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* 密碼變更區塊 */}
|
||||||
|
<section className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-slate-200 bg-slate-50/50">
|
||||||
|
<h2 className="text-lg font-semibold text-slate-800 flex items-center gap-2">
|
||||||
|
<Lock className="h-5 w-5 text-slate-400" />
|
||||||
|
安全性與密碼
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handlePasswordSubmit} className="p-6 space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
目前密碼 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.current_password}
|
||||||
|
onChange={(e) => setPasswordData("current_password", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all"
|
||||||
|
/>
|
||||||
|
{passwordErrors.current_password && <p className="mt-1 text-sm text-red-500">{passwordErrors.current_password}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
新密碼 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.password}
|
||||||
|
onChange={(e) => setPasswordData("password", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all"
|
||||||
|
/>
|
||||||
|
{passwordErrors.password && <p className="mt-1 text-sm text-red-500">{passwordErrors.password}</p>}
|
||||||
|
<p className="mt-1 text-xs text-slate-500">建議使用 8 個字元以上包含數字與符號的密碼</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||||
|
確認新密碼 <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.password_confirmation}
|
||||||
|
onChange={(e) => setPasswordData("password_confirmation", e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={passwordProcessing}
|
||||||
|
className="button-filled-primary"
|
||||||
|
>
|
||||||
|
{passwordProcessing ? "更新中..." : "更新密碼"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
resources/js/types/global.d.ts
vendored
1
resources/js/types/global.d.ts
vendored
@@ -7,6 +7,7 @@ export interface AuthUser {
|
|||||||
email: string;
|
email: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
|
role_labels: string[];
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\Landlord\DashboardController;
|
use App\Http\Controllers\Landlord\DashboardController;
|
||||||
use App\Http\Controllers\Landlord\TenantController;
|
use App\Http\Controllers\Landlord\TenantController;
|
||||||
|
use App\Http\Controllers\Landlord\ProfileController;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@@ -21,6 +22,11 @@ Route::prefix('landlord')->name('landlord.')->middleware(['web', 'auth', \App\Ht
|
|||||||
// 租戶管理 CRUD
|
// 租戶管理 CRUD
|
||||||
Route::resource('tenants', TenantController::class);
|
Route::resource('tenants', TenantController::class);
|
||||||
|
|
||||||
|
// 使用者設定
|
||||||
|
Route::get('profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
|
Route::patch('profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
|
Route::put('profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
|
||||||
|
|
||||||
// 租戶域名管理
|
// 租戶域名管理
|
||||||
Route::post('tenants/{tenant}/domains', [TenantController::class, 'addDomain'])->name('tenants.domains.store');
|
Route::post('tenants/{tenant}/domains', [TenantController::class, 'addDomain'])->name('tenants.domains.store');
|
||||||
Route::delete('tenants/{tenant}/domains/{domain}', [TenantController::class, 'removeDomain'])->name('tenants.domains.destroy');
|
Route::delete('tenants/{tenant}/domains/{domain}', [TenantController::class, 'removeDomain'])->name('tenants.domains.destroy');
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use App\Http\Controllers\TransferOrderController;
|
|||||||
use App\Http\Controllers\UnitController;
|
use App\Http\Controllers\UnitController;
|
||||||
use App\Http\Controllers\Admin\RoleController;
|
use App\Http\Controllers\Admin\RoleController;
|
||||||
use App\Http\Controllers\Admin\UserController;
|
use App\Http\Controllers\Admin\UserController;
|
||||||
|
use App\Http\Controllers\ProfileController;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||||
|
|
||||||
// 登入/登出路由
|
// 登入/登出路由
|
||||||
@@ -27,6 +28,11 @@ Route::middleware('auth')->group(function () {
|
|||||||
// 儀表板 - 所有登入使用者皆可存取
|
// 儀表板 - 所有登入使用者皆可存取
|
||||||
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
|
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
|
||||||
|
|
||||||
|
// 使用者帳號設定
|
||||||
|
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
|
||||||
|
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
|
Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password');
|
||||||
|
|
||||||
// 類別管理 (用於商品對話框) - 需要商品權限
|
// 類別管理 (用於商品對話框) - 需要商品權限
|
||||||
Route::middleware('permission:products.view')->group(function () {
|
Route::middleware('permission:products.view')->group(function () {
|
||||||
Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index');
|
Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index');
|
||||||
|
|||||||
Reference in New Issue
Block a user