fix: tenancy middleware order and ui consistency for user profile
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 44s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-16 11:56:44 +08:00
parent 5b15ca2cd6
commit 43d7cada34
16 changed files with 576 additions and 16 deletions

View File

@@ -39,7 +39,9 @@ export default function Dashboard({ totalTenants, activeTenants, recentTenants }
];
return (
<LandlordLayout title="儀表板">
<LandlordLayout
title="儀表板"
>
<div className="space-y-6">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">

View 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>
);
}

View File

@@ -16,7 +16,9 @@ export default function TenantCreate() {
};
return (
<LandlordLayout title="新增客戶">
<LandlordLayout
title="新增客戶"
>
<div className="max-w-2xl">
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-900"></h1>

View File

@@ -26,7 +26,9 @@ export default function TenantEdit({ tenant }: Props) {
};
return (
<LandlordLayout title="編輯客戶">
<LandlordLayout
title="編輯客戶"
>
<div className="max-w-2xl">
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-900"></h1>

View File

@@ -37,7 +37,9 @@ export default function TenantIndex({ tenants }: Props) {
};
return (
<LandlordLayout title="客戶管理">
<LandlordLayout
title="客戶管理"
>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">

View File

@@ -45,7 +45,9 @@ export default function TenantShow({ tenant }: Props) {
};
return (
<LandlordLayout title="客戶詳情">
<LandlordLayout
title="客戶詳情"
>
<div className="max-w-3xl space-y-6">
{/* Back Link */}
<Link

View 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>
);
}