mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
user management ui
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/shared/common/ui/dialog";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { Input } from "@/shared/common/ui/input";
|
||||
import { Label } from "@/shared/common/ui/label";
|
||||
import { SiteDto, SitePayloadDto } from "@/super-admin/dto/SitesDto";
|
||||
import { toSnakeCase } from "@/super-admin/hooks/useSites";
|
||||
|
||||
interface SiteFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (payload: SitePayloadDto) => void;
|
||||
site?: SiteDto | null; // If provided, we are in Edit mode
|
||||
isSubmitting: boolean;
|
||||
existingKeys: string[]; // for frontend uniqueness check
|
||||
}
|
||||
|
||||
export default function SiteFormModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
site = null,
|
||||
isSubmitting,
|
||||
existingKeys,
|
||||
}: SiteFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const isEdit = !!site;
|
||||
|
||||
const [nameAm, setNameAm] = useState("");
|
||||
const [nameEn, setNameEn] = useState("");
|
||||
const [key, setKey] = useState("");
|
||||
const [domain, setDomain] = useState("");
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (site) {
|
||||
setNameAm(site.name?.am || "");
|
||||
setNameEn(site.name?.en || "");
|
||||
setKey(site.key || "");
|
||||
setDomain(site.domain || "");
|
||||
} else {
|
||||
setNameAm("");
|
||||
setNameEn("");
|
||||
setKey("");
|
||||
setDomain("");
|
||||
}
|
||||
setErrors({});
|
||||
}
|
||||
}, [isOpen, site]);
|
||||
|
||||
const validate = (): boolean => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
if (!nameAm.trim()) {
|
||||
newErrors.nameAm = t("sites.form.nameAmRequired");
|
||||
}
|
||||
if (!nameEn.trim()) {
|
||||
newErrors.nameEn = t("sites.form.nameEnRequired");
|
||||
}
|
||||
|
||||
if (!key.trim()) {
|
||||
newErrors.key = t("sites.form.keyRequired");
|
||||
} else if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
|
||||
newErrors.key = t("sites.form.keyFormat");
|
||||
} else if (!isEdit && existingKeys.includes(key.trim())) {
|
||||
newErrors.key = t("sites.form.keyUnique");
|
||||
}
|
||||
|
||||
if (!domain.trim()) {
|
||||
newErrors.domain = t("sites.form.domainRequired");
|
||||
} else if (/\s/.test(domain)) {
|
||||
newErrors.domain = t("sites.form.domainNoSpaces");
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!validate()) return;
|
||||
|
||||
onSubmit({
|
||||
name: {
|
||||
am: nameAm.trim(),
|
||||
en: nameEn.trim(), // transformed to snake_case in the hook/mutation
|
||||
},
|
||||
key: key.trim(),
|
||||
domain: domain.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-slate-800 dark:text-slate-100">
|
||||
{isEdit ? t("sites.form.titleEdit") : t("sites.form.titleAdd")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5 py-2">
|
||||
{/* Site Name (Amharic) */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="nameAm" className="text-sm font-semibold">
|
||||
{t("sites.form.nameAmLabel")} <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="nameAm"
|
||||
value={nameAm}
|
||||
onChange={(e) => setNameAm(e.target.value)}
|
||||
placeholder={t("sites.form.nameAmPlaceholder")}
|
||||
className={errors.nameAm ? "border-red-500 focus-visible:ring-red-500" : ""}
|
||||
/>
|
||||
{errors.nameAm && (
|
||||
<span className="text-xs text-red-500 font-medium">{errors.nameAm}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Site Name (English) */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="nameEn" className="text-sm font-semibold">
|
||||
{t("sites.form.nameEnLabel")} <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="nameEn"
|
||||
value={nameEn}
|
||||
onChange={(e) => setNameEn(e.target.value)}
|
||||
placeholder={t("sites.form.nameEnPlaceholder")}
|
||||
className={errors.nameEn ? "border-red-500 focus-visible:ring-red-500" : ""}
|
||||
/>
|
||||
{nameEn.trim() && (
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
{t("sites.form.previewLabel")} <span className="font-mono text-primary font-semibold">{toSnakeCase(nameEn)}</span>
|
||||
</p>
|
||||
)}
|
||||
{errors.nameEn && (
|
||||
<span className="text-xs text-red-500 font-medium">{errors.nameEn}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Site Key */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="key" className="text-sm font-semibold">
|
||||
{t("sites.form.keyLabel")} <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="key"
|
||||
value={key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
placeholder={t("sites.form.keyPlaceholder")}
|
||||
disabled={isEdit}
|
||||
className={errors.key ? "border-red-500 focus-visible:ring-red-500" : ""}
|
||||
/>
|
||||
{!isEdit && (
|
||||
<p className="text-xs text-slate-400">
|
||||
{t("sites.form.keyHint")}
|
||||
</p>
|
||||
)}
|
||||
{errors.key && (
|
||||
<span className="text-xs text-red-500 font-medium">{errors.key}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Site Domain */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="domain" className="text-sm font-semibold">
|
||||
{t("sites.form.domainLabel")} <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="domain"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
placeholder={t("sites.form.domainPlaceholder")}
|
||||
className={errors.domain ? "border-red-500 focus-visible:ring-red-500" : ""}
|
||||
/>
|
||||
{errors.domain && (
|
||||
<span className="text-xs text-red-500 font-medium">{errors.domain}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-4 border-t gap-2 sm:gap-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isSubmitting ? t("sites.form.saving") : isEdit ? t("sites.form.saveChanges") : t("sites.form.createSite")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/shared/common/ui/dialog";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { Badge } from "@/shared/common/ui/badge";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SiteDto } from "@/super-admin/dto/SitesDto";
|
||||
|
||||
interface SiteViewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
site?: SiteDto | null;
|
||||
}
|
||||
|
||||
export default function SiteViewModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
site,
|
||||
}: SiteViewModalProps) {
|
||||
const { t } = useTranslation();
|
||||
if (!site) return null;
|
||||
|
||||
const isActive = site.status === "Active" || site.status !== "Archived";
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-slate-800 dark:text-slate-100">
|
||||
{t("sites.view.title")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-2 space-y-4">
|
||||
{/* Detail cards */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2 p-3 bg-slate-50 dark:bg-slate-900 rounded-lg space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
{t("sites.view.nameEn")}
|
||||
</span>
|
||||
<p className="text-base font-semibold text-slate-800 dark:text-slate-100">
|
||||
{site.name?.en || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 p-3 bg-slate-50 dark:bg-slate-900 rounded-lg space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
{t("sites.view.nameAm")}
|
||||
</span>
|
||||
<p className="text-base font-semibold text-slate-800 dark:text-slate-100">
|
||||
{site.name?.am || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-slate-50 dark:bg-slate-900 rounded-lg space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
{t("sites.view.siteKey")}
|
||||
</span>
|
||||
<p className="text-base font-mono text-slate-800 dark:text-slate-100 truncate" title={site.key}>
|
||||
{site.key || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-slate-50 dark:bg-slate-900 rounded-lg space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
{t("sites.view.status")}
|
||||
</span>
|
||||
<div>
|
||||
<Badge
|
||||
className={`${
|
||||
isActive
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-950/40"
|
||||
: "bg-amber-100 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-950/40"
|
||||
} rounded-full px-3 py-0.5 font-medium border-none`}
|
||||
>
|
||||
{site.status || "Active"}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 p-3 bg-slate-50 dark:bg-slate-900 rounded-lg space-y-1">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
{t("sites.view.domain")}
|
||||
</span>
|
||||
<p className="text-base font-semibold text-slate-800 dark:text-slate-100 truncate" title={site.domain}>
|
||||
{site.domain || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="border-t pt-4">
|
||||
<Button onClick={onClose} className="w-full sm:w-auto">
|
||||
{t("sites.view.close")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { SiteDto } from "@/super-admin/dto/SitesDto";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { Badge } from "@/shared/common/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/common/ui/dropdown-menu";
|
||||
import { Archive, Trash2, Edit, Eye, MoreHorizontal, RefreshCw, Palette } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
|
||||
interface ColumnsProps {
|
||||
onView: (site: SiteDto) => void;
|
||||
onEdit: (site: SiteDto) => void;
|
||||
onArchive: (site: SiteDto) => void;
|
||||
onRestore: (site: SiteDto) => void;
|
||||
onDelete: (site: SiteDto) => void;
|
||||
onBranding: (site: SiteDto) => void;
|
||||
}
|
||||
|
||||
export const getSitesColumnDefn = ({
|
||||
onView,
|
||||
onEdit,
|
||||
onArchive,
|
||||
onRestore,
|
||||
onDelete,
|
||||
onBranding,
|
||||
}: ColumnsProps): ColumnDef<SiteDto>[] => [
|
||||
{
|
||||
id: "name",
|
||||
header: () => t("sites.columns.siteName"),
|
||||
cell: ({ row }) => {
|
||||
const site = row.original;
|
||||
return (
|
||||
<div>
|
||||
<div className="font-semibold text-slate-800 dark:text-slate-200">
|
||||
{site.name?.en || "-"}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 dark:text-slate-400">
|
||||
{site.name?.am || "-"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: () => t("sites.columns.key"),
|
||||
cell: ({ row }) => (
|
||||
<span className="font-mono text-slate-700 dark:text-slate-300">
|
||||
{row.original.key}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "domain",
|
||||
header: () => t("sites.columns.domain"),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-slate-700 dark:text-slate-300">
|
||||
{row.original.domain}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: () => t("sites.columns.status"),
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.status || "Active";
|
||||
const isActive = status === "Active";
|
||||
return (
|
||||
<Badge
|
||||
className={`${
|
||||
isActive
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-950/40"
|
||||
: "bg-amber-100 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-950/40"
|
||||
} rounded-full px-3 py-0.5 font-medium border-none`}
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => t("sites.columns.actions"),
|
||||
cell: ({ row }) => {
|
||||
const site = row.original;
|
||||
const isArchived = site.status === "Archived";
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Open Menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[160px]">
|
||||
<DropdownMenuLabel>{t("sites.columns.actions")}</DropdownMenuLabel>
|
||||
<DropdownMenuItem onClick={() => onView(site)}>
|
||||
<Eye className="mr-2 h-4 w-4 text-slate-500" />
|
||||
{t("sites.actions.view")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit(site)}>
|
||||
<Edit className="mr-2 h-4 w-4 text-blue-500" />
|
||||
{t("sites.actions.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onBranding(site)}>
|
||||
<Palette className="mr-2 h-4 w-4 text-purple-500" />
|
||||
{t("sites.actions.branding")}
|
||||
</DropdownMenuItem>
|
||||
{!isArchived ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onArchive(site)}
|
||||
className="text-amber-600 focus:text-amber-600 cursor-pointer"
|
||||
>
|
||||
<Archive className="mr-2 h-4 w-4 text-amber-500" />
|
||||
{t("sites.actions.archive")}
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onRestore(site)}
|
||||
className="text-emerald-600 focus:text-emerald-600 cursor-pointer"
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4 text-emerald-500" />
|
||||
{t("sites.actions.restore")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(site)}
|
||||
className="text-rose-600 focus:text-rose-600 cursor-pointer"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4 text-rose-500" />
|
||||
{t("sites.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,363 @@
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { useSites } from "@/super-admin/hooks/useSites";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import { AdvancedTable } from "@/shared/common/ui/table/AdvancedTable";
|
||||
import { getSitesColumnDefn } from "./SitesColumnDefn";
|
||||
import { Loader, Plus } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/common/ui/card";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/shared/common/ui/alert-dialog";
|
||||
import { SiteDto, SitePayloadDto } from "@/super-admin/dto/SitesDto";
|
||||
import SiteFormModal from "./SiteFormModal";
|
||||
import SiteViewModal from "./SiteViewModal";
|
||||
|
||||
export default function SitesPage() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// Modals state
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [viewOpen, setViewOpen] = useState(false);
|
||||
const [selectedSite, setSelectedSite] = useState<SiteDto | null>(null);
|
||||
|
||||
// Dialogs state for archive/delete/restore
|
||||
const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [restoreDialogOpen, setRestoreDialogOpen] = useState(false);
|
||||
|
||||
// Query & mutation hook
|
||||
const {
|
||||
sitesResponse,
|
||||
isLoading,
|
||||
refetch,
|
||||
createSite,
|
||||
isCreating,
|
||||
updateSite,
|
||||
isUpdating,
|
||||
archiveSite,
|
||||
isArchiving,
|
||||
deleteSite,
|
||||
isDeleting,
|
||||
restoreSite,
|
||||
isRestoring,
|
||||
} = useSites({
|
||||
take: pageSize,
|
||||
skip: pageIndex * pageSize,
|
||||
orderBy: "createdAt:DESC",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setPageIndex(0);
|
||||
}, [searchTerm, pageSize]);
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPageIndex(newPage);
|
||||
};
|
||||
|
||||
const handleAddNewClick = () => {
|
||||
setSelectedSite(null);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleFormSubmit = (payload: SitePayloadDto) => {
|
||||
if (selectedSite) {
|
||||
updateSite(
|
||||
{ id: selectedSite.id, payload },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setFormOpen(false);
|
||||
setSelectedSite(null);
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
createSite(payload, {
|
||||
onSuccess: () => {
|
||||
setFormOpen(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleArchiveConfirm = () => {
|
||||
if (selectedSite) {
|
||||
archiveSite(
|
||||
{ id: selectedSite.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setArchiveDialogOpen(false);
|
||||
setSelectedSite(null);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = () => {
|
||||
if (selectedSite) {
|
||||
deleteSite(
|
||||
{ id: selectedSite.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setDeleteDialogOpen(false);
|
||||
setSelectedSite(null);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreConfirm = () => {
|
||||
if (selectedSite) {
|
||||
restoreSite(
|
||||
{ id: selectedSite.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
setRestoreDialogOpen(false);
|
||||
setSelectedSite(null);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Extract all sites from response
|
||||
const allSites = useMemo(() => sitesResponse?.items || [], [sitesResponse]);
|
||||
// Existing keys list (for create uniqueness validation)
|
||||
const existingKeys = useMemo(() => allSites.map((s) => s.key), [allSites]);
|
||||
|
||||
// Get table column definitions
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
getSitesColumnDefn({
|
||||
onView: (site) => {
|
||||
setSelectedSite(site);
|
||||
setViewOpen(true);
|
||||
},
|
||||
onEdit: (site) => {
|
||||
setSelectedSite(site);
|
||||
setFormOpen(true);
|
||||
},
|
||||
onArchive: (site) => {
|
||||
setSelectedSite(site);
|
||||
setArchiveDialogOpen(true);
|
||||
},
|
||||
onRestore: (site) => {
|
||||
setSelectedSite(site);
|
||||
setRestoreDialogOpen(true);
|
||||
},
|
||||
onDelete: (site) => {
|
||||
setSelectedSite(site);
|
||||
setDeleteDialogOpen(true);
|
||||
},
|
||||
onBranding: (site) => {
|
||||
navigate(`/user-management/web-management/Branding/Branding/${site.id}`);
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<Card className="col-span-2 shadow-none border-none bg-transparent px-0">
|
||||
<CardHeader className="flex flex-row justify-between items-center px-0">
|
||||
<CardTitle className="text-2xl font-bold text-slate-800 dark:text-slate-100">
|
||||
{t("sites.title")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={sitesResponse?.items || []}
|
||||
tableName={t("sites.tableName")}
|
||||
toolBarPosition="right"
|
||||
itemCount={sitesResponse?.count || 0}
|
||||
pageSize={pageSize}
|
||||
onGlobalFilterChange={setSearchTerm}
|
||||
extraToolbar={
|
||||
<Button
|
||||
onClick={handleAddNewClick}
|
||||
className="px-5 py-2 rounded-md text-sm font-medium shadow-md cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t("sites.addNew")}
|
||||
</Button>
|
||||
}
|
||||
pageIndex={pageIndex}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={setPageSize}
|
||||
nextFunction={() => handlePageChange(pageIndex + 1)}
|
||||
prevFunction={() => handlePageChange(Math.max(pageIndex - 1, 0))}
|
||||
refresh={refetch}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Create / Edit Form Modal */}
|
||||
<SiteFormModal
|
||||
isOpen={formOpen}
|
||||
onClose={() => {
|
||||
setFormOpen(false);
|
||||
setSelectedSite(null);
|
||||
}}
|
||||
onSubmit={handleFormSubmit}
|
||||
site={selectedSite}
|
||||
isSubmitting={isCreating || isUpdating}
|
||||
existingKeys={existingKeys}
|
||||
/>
|
||||
|
||||
{/* Read-Only View Modal */}
|
||||
<SiteViewModal
|
||||
isOpen={viewOpen}
|
||||
onClose={() => {
|
||||
setViewOpen(false);
|
||||
setSelectedSite(null);
|
||||
}}
|
||||
site={selectedSite}
|
||||
/>
|
||||
|
||||
{/* Archive Confirmation (Soft Delete) */}
|
||||
<AlertDialog
|
||||
open={archiveDialogOpen}
|
||||
onOpenChange={(open) => !open && setArchiveDialogOpen(false)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-lg font-bold">
|
||||
{t("sites.dialogs.archiveTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("sites.dialogs.archiveDescription", {
|
||||
name: selectedSite?.name?.en,
|
||||
})}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
disabled={isArchiving}
|
||||
onClick={() => {
|
||||
setArchiveDialogOpen(false);
|
||||
setSelectedSite(null);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isArchiving}
|
||||
onClick={handleArchiveConfirm}
|
||||
className="bg-amber-600 hover:bg-amber-700 text-white cursor-pointer"
|
||||
>
|
||||
{isArchiving
|
||||
? t("sites.dialogs.archiving")
|
||||
: t("sites.dialogs.archiveConfirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Permanent Delete Confirmation (Hard Delete) */}
|
||||
<AlertDialog
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={(open) => !open && setDeleteDialogOpen(false)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-lg font-bold text-red-600">
|
||||
{t("sites.dialogs.deleteTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t("sites.dialogs.deleteDescription", {
|
||||
name: selectedSite?.name?.en,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
disabled={isDeleting}
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(false);
|
||||
setSelectedSite(null);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isDeleting}
|
||||
onClick={handleDeleteConfirm}
|
||||
className="bg-red-600 hover:bg-red-700 text-white cursor-pointer"
|
||||
>
|
||||
{isDeleting
|
||||
? t("sites.dialogs.deleting")
|
||||
: t("sites.dialogs.deleteConfirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Restore Confirmation */}
|
||||
<AlertDialog
|
||||
open={restoreDialogOpen}
|
||||
onOpenChange={(open) => !open && setRestoreDialogOpen(false)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-lg font-bold">
|
||||
{t("sites.dialogs.restoreTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("sites.dialogs.restoreDescription", {
|
||||
name: selectedSite?.name?.en,
|
||||
})}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
disabled={isRestoring}
|
||||
onClick={() => {
|
||||
setRestoreDialogOpen(false);
|
||||
setSelectedSite(null);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isRestoring}
|
||||
onClick={handleRestoreConfirm}
|
||||
className="bg-emerald-600 hover:bg-emerald-700 text-white cursor-pointer"
|
||||
>
|
||||
{isRestoring
|
||||
? t("sites.dialogs.restoring")
|
||||
: t("sites.dialogs.restoreConfirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user