feat: 實作 Multi-tenancy 多租戶架構 (stancl/tenancy)
- 安裝並設定 stancl/tenancy 套件 - 分離 Central / Tenant migrations - 建立 Tenant Model 與資料遷移指令 - 建立房東後台 CRUD (Landlord Dashboard) - 新增租戶管理頁面 (列表、新增、編輯、詳情) - 新增域名管理功能 - 更新部署手冊
This commit is contained in:
188
resources/js/Pages/Landlord/Tenant/Index.tsx
Normal file
188
resources/js/Pages/Landlord/Tenant/Index.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import LandlordLayout from "@/Layouts/LandlordLayout";
|
||||
import { Link, router } from "@inertiajs/react";
|
||||
import { Plus, Edit, Trash2, Globe } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/Components/ui/alert-dialog";
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
domains: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
tenants: Tenant[];
|
||||
}
|
||||
|
||||
export default function TenantIndex({ tenants }: Props) {
|
||||
const [deleteTarget, setDeleteTarget] = useState<Tenant | null>(null);
|
||||
|
||||
const handleDelete = () => {
|
||||
if (deleteTarget) {
|
||||
router.delete(route("landlord.tenants.destroy", deleteTarget.id));
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LandlordLayout title="租戶管理">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">租戶管理</h1>
|
||||
<p className="text-slate-500 mt-1">管理系統中的所有租戶</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/landlord/tenants/create"
|
||||
className="bg-primary-main hover:bg-primary-dark text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
新增租戶
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
ID
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
名稱
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
域名
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
狀態
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">
|
||||
建立時間
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{tenants.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-12 text-center text-slate-500">
|
||||
尚無租戶資料,請點擊「新增租戶」建立第一個租戶
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
tenants.map((tenant) => (
|
||||
<tr key={tenant.id} className="hover:bg-slate-50">
|
||||
<td className="px-6 py-4 font-mono text-sm text-slate-600">
|
||||
{tenant.id}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">{tenant.name}</p>
|
||||
{tenant.email && (
|
||||
<p className="text-sm text-slate-500">{tenant.email}</p>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{tenant.domains.length > 0 ? (
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{tenant.domains.map((domain) => (
|
||||
<span
|
||||
key={domain}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-slate-100 rounded text-xs"
|
||||
>
|
||||
<Globe className="w-3 h-3" />
|
||||
{domain}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-400 text-sm">無</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium ${tenant.is_active
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-slate-100 text-slate-600"
|
||||
}`}
|
||||
>
|
||||
{tenant.is_active ? "啟用" : "停用"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-500">
|
||||
{tenant.created_at}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Link
|
||||
href={`/landlord/tenants/${tenant.id}`}
|
||||
className="p-2 text-slate-400 hover:text-primary-main hover:bg-slate-100 rounded-lg transition-colors"
|
||||
title="查看詳情"
|
||||
>
|
||||
<Globe className="w-4 h-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href={`/landlord/tenants/${tenant.id}/edit`}
|
||||
className="p-2 text-slate-400 hover:text-blue-600 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
title="編輯"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setDeleteTarget(tenant)}
|
||||
className="p-2 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
title="刪除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>確定要刪除這個租戶嗎?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
刪除租戶將會同時刪除其資料庫和所有資料,此操作無法復原。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
確定刪除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</LandlordLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user