kontenhumas-fe/app/[locale]/(admin)/admin/tenants/page.tsx

464 lines
17 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { PlusIcon, EditIcon, DeleteIcon } from "@/components/icons";
import {
Tenant,
TenantCreateRequest,
getTenantList,
createTenant,
deleteTenant,
} from "@/service/tenant";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import Swal from "sweetalert2";
import { FormField } from "@/components/form/common/FormField";
import { getCookiesDecrypt } from "@/lib/utils";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function TenantsManagementPage() {
const router = useRouter();
const [tenants, setTenants] = useState<Tenant[]>([]);
const [parentTenants, setParentTenants] = useState<Tenant[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [formData, setFormData] = useState<TenantCreateRequest>({
name: "",
description: "",
clientType: "standalone",
parentClientId: undefined,
maxUsers: undefined,
maxStorage: undefined,
address: "",
phoneNumber: "",
website: "",
});
useEffect(() => {
// Check if user has roleId = 1
const roleId = getCookiesDecrypt("urie");
if (Number(roleId) !== 1) {
Swal.fire({
title: "Access Denied",
text: "You don't have permission to access this page",
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
}).then(() => {
router.push("/admin/dashboard");
});
return;
}
loadData();
}, [router]);
const loadData = async () => {
setIsLoading(true);
try {
const [tenantsRes, parentRes] = await Promise.all([
getTenantList({ limit: 100 }),
getTenantList({ clientType: "parent_client", limit: 100 }),
]);
if (!tenantsRes?.error) {
setTenants(tenantsRes?.data?.data || []);
}
if (!parentRes?.error) {
setParentTenants(parentRes?.data?.data || []);
}
} catch (error) {
console.error("Error loading tenants:", error);
} finally {
setIsLoading(false);
}
};
const handleOpenDialog = () => {
setFormData({
name: "",
description: "",
clientType: "standalone",
parentClientId: undefined,
maxUsers: undefined,
maxStorage: undefined,
address: "",
phoneNumber: "",
website: "",
});
setIsDialogOpen(true);
};
const handleSave = async () => {
try {
const createData: TenantCreateRequest = {
name: formData.name,
description: formData.description || undefined,
clientType: formData.clientType,
parentClientId: formData.parentClientId || undefined,
maxUsers: formData.maxUsers,
maxStorage: formData.maxStorage,
address: formData.address || undefined,
phoneNumber: formData.phoneNumber || undefined,
website: formData.website || undefined,
};
const res = await createTenant(createData);
if (res?.error) {
Swal.fire({
title: "Error",
text: res?.message || "Failed to create tenant",
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
} else {
Swal.fire({
title: "Success",
text: "Tenant created successfully",
icon: "success",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
await loadData();
setIsDialogOpen(false);
}
} catch (error) {
console.error("Error saving tenant:", error);
Swal.fire({
title: "Error",
text: "An unexpected error occurred",
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
}
};
const handleDelete = async (tenant: Tenant) => {
const result = await Swal.fire({
title: "Delete Tenant?",
text: `Are you sure you want to delete "${tenant.name}"? This action cannot be undone.`,
icon: "warning",
showCancelButton: true,
confirmButtonText: "Yes, delete it",
cancelButtonText: "Cancel",
customClass: {
popup: 'swal-z-index-9999'
}
});
if (result.isConfirmed) {
try {
const res = await deleteTenant(tenant.id);
if (res?.error) {
Swal.fire({
title: "Error",
text: res?.message || "Failed to delete tenant",
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
} else {
Swal.fire({
title: "Deleted!",
text: "Tenant has been deleted.",
icon: "success",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
await loadData();
}
} catch (error) {
console.error("Error deleting tenant:", error);
Swal.fire({
title: "Error",
text: "An unexpected error occurred",
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
}
}
};
const roleId = getCookiesDecrypt("urie");
if (Number(roleId) !== 1) {
return null; // Will redirect in useEffect
}
return (
<>
<SiteBreadcrumb />
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Tenant Management</h1>
<p className="text-gray-600 mt-2">
Manage system tenants and their configurations
</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
<PlusIcon className="h-4 w-4" />
Create Tenant
</Button>
</DialogTrigger>
{/* @ts-ignore - DialogContent accepts children */}
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Tenant</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<FormField
label="Tenant Name"
name="name"
type="text"
placeholder="e.g., Company ABC, Organization XYZ"
value={formData.name}
onChange={(value) => setFormData({ ...formData, name: value })}
required
/>
<FormField
label="Description"
name="description"
type="textarea"
placeholder="Brief description of the tenant"
value={formData.description}
onChange={(value) => setFormData({ ...formData, description: value })}
/>
<FormField
label="Client Type"
name="clientType"
type="select"
placeholder="Select client type"
value={formData.clientType}
onChange={(value) => setFormData({ ...formData, clientType: value as any })}
options={[
{ value: "standalone", label: "Standalone" },
{ value: "parent_client", label: "Parent Client" },
{ value: "sub_client", label: "Sub Client" },
]}
required
/>
{formData.clientType === "sub_client" && (
<FormField
label="Parent Tenant"
name="parentClientId"
type="select"
placeholder="Select parent tenant"
value={formData.parentClientId || "none"}
onChange={(value) => setFormData({ ...formData, parentClientId: value === "none" ? undefined : value })}
options={[
{ value: "none", label: "Select a parent tenant" },
...parentTenants.map((tenant) => ({
value: tenant.id,
label: tenant.name,
})),
]}
required
/>
)}
<div className="grid grid-cols-2 gap-4">
<FormField
label="Max Users"
name="maxUsers"
type="number"
placeholder="e.g., 100"
value={formData.maxUsers?.toString() || ""}
onChange={(value) => setFormData({ ...formData, maxUsers: value ? Number(value) : undefined })}
helpText="Maximum number of users allowed"
/>
<FormField
label="Max Storage (MB)"
name="maxStorage"
type="number"
placeholder="e.g., 10000"
value={formData.maxStorage?.toString() || ""}
onChange={(value) => setFormData({ ...formData, maxStorage: value ? Number(value) : undefined })}
helpText="Maximum storage in MB"
/>
</div>
<FormField
label="Address"
name="address"
type="textarea"
placeholder="Tenant address"
value={formData.address}
onChange={(value) => setFormData({ ...formData, address: value })}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
label="Phone Number"
name="phoneNumber"
type="tel"
placeholder="e.g., +62 123 456 7890"
value={formData.phoneNumber}
onChange={(value) => setFormData({ ...formData, phoneNumber: value })}
/>
<FormField
label="Website"
name="website"
type="url"
placeholder="e.g., https://example.com"
value={formData.website}
onChange={(value) => setFormData({ ...formData, website: value })}
/>
</div>
<div className="flex items-center justify-end gap-2 pt-4 border-t">
<Button
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button>
<Button onClick={handleSave}>
Create Tenant
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{isLoading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading tenants...</p>
</div>
) : tenants.length > 0 ? (
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Type</TableHead>
<TableHead>Parent</TableHead>
<TableHead>Address</TableHead>
<TableHead>Phone</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{tenants.map((tenant) => {
const parentTenant = tenants.find((t) => t.id === tenant.parentClientId);
return (
<TableRow key={tenant.id}>
<TableCell className="font-medium">{tenant.name}</TableCell>
<TableCell>
<span className={`px-2 py-1 text-xs rounded-full ${
tenant.clientType === "parent_client"
? "bg-blue-100 text-blue-800"
: tenant.clientType === "sub_client"
? "bg-purple-100 text-purple-800"
: "bg-gray-100 text-gray-800"
}`}>
{tenant.clientType.replace("_", " ")}
</span>
</TableCell>
<TableCell>{parentTenant?.name || "-"}</TableCell>
<TableCell className="max-w-xs truncate">{tenant.address || "-"}</TableCell>
<TableCell>{tenant.phoneNumber || "-"}</TableCell>
<TableCell>
{tenant.isActive ? (
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
Active
</span>
) : (
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
Inactive
</span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/admin/tenants/${tenant.id}/edit`)}
>
<EditIcon className="h-4 w-4 mr-2" />
Edit
</Button>
<Button
variant="outline"
size="sm"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => handleDelete(tenant)}
>
<DeleteIcon className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="flex items-center justify-center py-12">
<div className="text-center">
<div className="h-12 w-12 text-gray-400 mx-auto mb-4 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="w-12 h-12"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z"
/>
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">No Tenants Found</h3>
<p className="text-gray-500 mb-4">
Create your first tenant to get started
</p>
<Button onClick={() => handleOpenDialog()}>
<PlusIcon className="h-4 w-4 mr-2" />
Create Tenant
</Button>
</div>
</CardContent>
</Card>
)}
</div>
</>
);
}