diff --git a/src/app/(modules)/tenants/components/TenantColumns.tsx b/src/app/(modules)/tenants/components/TenantColumns.tsx new file mode 100644 index 0000000..66394e6 --- /dev/null +++ b/src/app/(modules)/tenants/components/TenantColumns.tsx @@ -0,0 +1,111 @@ +'use client'; + +import { useMemo } from 'react'; +import type { ColumnDef } from '@tanstack/react-table'; +import { Edit, Trash2 } from 'lucide-react'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { PERMISSIONS } from '@/constants/permissions'; +import { usePermissions } from '@/hooks/usePermissions'; +import type { Tenant } from '@/types'; + +function formatDate(value?: string | null) { + if (!value) return '-'; + return new Intl.DateTimeFormat('en-IN', { + day: '2-digit', + month: 'short', + year: 'numeric', + }).format(new Date(value)); +} + +interface UseTenantColumnsParams { + onEdit: (tenant: Tenant) => void; + onDelete: (tenant: Tenant) => void; + pendingDeleteId?: number; +} + +export function useTenantColumns({ + onEdit, + onDelete, + pendingDeleteId, +}: UseTenantColumnsParams): ColumnDef[] { + const { hasPermission } = usePermissions(); + const canEdit = hasPermission(PERMISSIONS.TENANT.UPDATE); + const canDelete = hasPermission(PERMISSIONS.TENANT.DELETE); + + return useMemo(() => { + const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Tenant', + cell: ({ row }) => ( +
+

{row.original.name}

+

{row.original.slug}

+
+ ), + }, + { + accessorKey: 'domain', + header: 'Domain', + cell: ({ row }) => row.original.domain || '-', + }, + { + accessorKey: 'admin_email', + header: 'Admin Email', + cell: ({ row }) => row.original.admin_email || '-', + }, + { + accessorKey: 'plan_id', + header: 'Plan', + cell: ({ row }) => row.original.plan_id || '-', + }, + { + accessorKey: 'created_at', + header: 'Created', + cell: ({ row }) => formatDate(row.original.created_at), + }, + { + accessorKey: 'is_active', + header: 'Status', + cell: ({ row }) => ( + + {row.original.is_active ? 'Active' : 'Inactive'} + + ), + }, + ]; + + if (!canEdit && !canDelete) return columns; + + columns.push({ + id: 'actions', + header: () =>
Actions
, + cell: ({ row }) => { + const tenant = row.original; + return ( +
+ {canEdit ? ( + + ) : null} + {canDelete ? ( + + ) : null} +
+ ); + }, + }); + + return columns; + }, [canDelete, canEdit, onDelete, onEdit, pendingDeleteId]); +} diff --git a/src/app/(modules)/tenants/components/TenantFilters.tsx b/src/app/(modules)/tenants/components/TenantFilters.tsx new file mode 100644 index 0000000..018662e --- /dev/null +++ b/src/app/(modules)/tenants/components/TenantFilters.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import type { TenantStatusFilter } from '../hooks/useTenantFilters'; + +interface TenantFiltersProps { + searchTerm: string; + statusFilter: TenantStatusFilter; + onSearchChange: (value: string) => void; + onStatusChange: (value: TenantStatusFilter) => void; +} + +export function TenantFilters({ + searchTerm, + statusFilter, + onSearchChange, + onStatusChange, +}: TenantFiltersProps) { + return ( +
+ onSearchChange(event.target.value)} + placeholder="Search tenants" + className="md:max-w-sm" + /> + +
+ ); +} diff --git a/src/app/(modules)/tenants/components/TenantSheet.tsx b/src/app/(modules)/tenants/components/TenantSheet.tsx new file mode 100644 index 0000000..129cb78 --- /dev/null +++ b/src/app/(modules)/tenants/components/TenantSheet.tsx @@ -0,0 +1,233 @@ +'use client'; + +import type { ComponentProps } from 'react'; +import type { UseFormRegister } from 'react-hook-form'; +import { Loader2 } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import type { Client, Plan } from '@/types'; + +import type { TenantFormValues } from '../hooks/useTenantForm'; + +interface TenantSheetProps { + open: boolean; + onOpenChange: (open: boolean) => void; + tenantId?: number; + isEditMode: boolean; + register: UseFormRegister; + onSubmit: ComponentProps<'form'>['onSubmit']; + clientId: string; + planId: string; + onClientChange: (clientId: string) => void; + onPlanChange: (planId: string) => void; + onNameChange: (name: string) => void; + clients: Client[]; + plans: Plan[]; + isLookupsLoading: boolean; + isSaving: boolean; + adminEmail?: string; +} + +export function TenantSheet({ + open, + onOpenChange, + tenantId, + isEditMode, + register, + onSubmit, + clientId, + planId, + onClientChange, + onPlanChange, + onNameChange, + clients, + plans, + isLookupsLoading, + isSaving, + adminEmail, +}: TenantSheetProps) { + return ( + + + + {tenantId ? 'Edit Tenant' : 'Create Tenant'} + + Bind a client and subscription plan, then invite the tenant administrator. + + +
+
+

Basic Information

+

Tenant identity and subscription binding.

+
+ +
+
+ + onNameChange(event.target.value), + })} + required + /> +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +