feat: 實作 Multi-tenancy 多租戶架構 (stancl/tenancy)
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 1m3s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

- 安裝並設定 stancl/tenancy 套件
- 分離 Central / Tenant migrations
- 建立 Tenant Model 與資料遷移指令
- 建立房東後台 CRUD (Landlord Dashboard)
- 新增租戶管理頁面 (列表、新增、編輯、詳情)
- 新增域名管理功能
- 更新部署手冊
This commit is contained in:
2026-01-15 13:15:18 +08:00
parent 3e3d8ffb6c
commit 4f745c1021
51 changed files with 1954 additions and 1 deletions

View File

@@ -0,0 +1,104 @@
import LandlordLayout from "@/Layouts/LandlordLayout";
import { useForm, Link } from "@inertiajs/react";
import { FormEvent } from "react";
export default function TenantCreate() {
const { data, setData, post, processing, errors } = useForm({
id: "",
name: "",
email: "",
domain: "",
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
post(route("landlord.tenants.store"));
};
return (
<LandlordLayout title="新增租戶">
<div className="max-w-2xl">
<div className="mb-6">
<h1 className="text-2xl font-bold text-slate-900"></h1>
<p className="text-slate-500 mt-1"></p>
</div>
<form onSubmit={handleSubmit} className="bg-white rounded-xl border border-slate-200 p-6 space-y-6">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
ID <span className="text-red-500">*</span>
</label>
<input
type="text"
value={data.id}
onChange={(e) => setData("id", e.target.value.toLowerCase())}
placeholder="例如koori, alcohol"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
/>
<p className="mt-1 text-sm text-slate-500">使</p>
{errors.id && <p className="mt-1 text-sm text-red-500">{errors.id}</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={data.name}
onChange={(e) => setData("name", e.target.value)}
placeholder="例如:小小冰室"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
/>
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
</label>
<input
type="email"
value={data.email}
onChange={(e) => setData("email", e.target.value)}
placeholder="admin@example.com"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
/>
{errors.email && <p className="mt-1 text-sm text-red-500">{errors.email}</p>}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
</label>
<input
type="text"
value={data.domain}
onChange={(e) => setData("domain", e.target.value)}
placeholder="例如koori.erp.koori.tw"
className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main"
/>
<p className="mt-1 text-sm text-slate-500"></p>
{errors.domain && <p className="mt-1 text-sm text-red-500">{errors.domain}</p>}
</div>
<div className="flex items-center gap-4 pt-4 border-t">
<button
type="submit"
disabled={processing}
className="bg-primary-main hover:bg-primary-dark text-white px-6 py-2 rounded-lg disabled:opacity-50 transition-colors"
>
{processing ? "處理中..." : "建立租戶"}
</button>
<Link
href="/landlord/tenants"
className="text-slate-600 hover:text-slate-900"
>
</Link>
</div>
</form>
</div>
</LandlordLayout>
);
}