mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
552 lines
22 KiB
TypeScript
552 lines
22 KiB
TypeScript
import { useMemo, useState, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { ArchiveRestore, Building2, UserSquare2, Trash2, Search, Filter } from "lucide-react";
|
|
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/shared/common/ui/card";
|
|
import { Button } from "@/shared/common/ui/button";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/common/ui/select";
|
|
import { AdvancedTable } from "@/shared/common/ui/table/AdvancedTable";
|
|
import { useLocalizedName } from "@/shared/common/localizedName";
|
|
import { useUnit } from "@/user-management/hooks/useUnit";
|
|
import {
|
|
useArchivedPositions,
|
|
useArchivedUnits,
|
|
useArchiveActions,
|
|
} from "@/user-management/hooks/useArchived";
|
|
import { UnitDto } from "@/user-management/dto/unit/unitDto";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/shared/common/ui/alert-dialog";
|
|
import { toast } from "sonner";
|
|
import { deleteUnit } from "@/user-management/services/api/unitService";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { useMyAdminOrganizations } from "@/user-management/hooks/useOrgAdminOrganizations";
|
|
import { Badge } from "@/shared/common/ui/badge";
|
|
import { Skeleton } from "@/shared/common/ui/skeleton";
|
|
|
|
type Tab = "units" | "positions";
|
|
|
|
const ArchivedUnitsPositionsPage = () => {
|
|
const { t } = useTranslation();
|
|
const localizedName = useLocalizedName();
|
|
const queryClient = useQueryClient();
|
|
|
|
const { getAccessibleList } = useUnit();
|
|
const { organizations: myAdminOrganizations } = useMyAdminOrganizations();
|
|
|
|
const [activeTab, setActiveTab] = useState<Tab>("units");
|
|
const [selectedOrgId, setSelectedOrgId] = useState<string>("");
|
|
const [selectedUnitId, setSelectedUnitId] = useState<string>("");
|
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
const [unitToDelete, setUnitToDelete] = useState<{ id: string; name: string } | null>(null);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
const { data: unitsResponse, isLoading: isLoadingUnits } = getAccessibleList(
|
|
selectedOrgId || "",
|
|
{ take: 300, skip: 0 },
|
|
!!selectedOrgId
|
|
);
|
|
|
|
const unitList: UnitDto[] = selectedOrgId ? (unitsResponse?.data?.items ?? []) : [];
|
|
|
|
const { data: archivedUnitsData, refetch: refetchArchivedUnits, isLoading: isLoadingArchivedUnits } =
|
|
useArchivedUnits(selectedOrgId || undefined);
|
|
const { data: archivedPositionsData, refetch: refetchArchivedPositions, isLoading: isLoadingArchivedPositions } =
|
|
useArchivedPositions(selectedUnitId || undefined);
|
|
|
|
const { restoreUnit, isRestoringUnit, restorePosition, isRestoringPosition } =
|
|
useArchiveActions();
|
|
|
|
useEffect(() => {
|
|
if (activeTab === "units" && !selectedOrgId && myAdminOrganizations.length > 0) {
|
|
setSelectedOrgId(myAdminOrganizations[0].id);
|
|
}
|
|
}, [activeTab, myAdminOrganizations.length]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab === "positions" && !selectedUnitId && unitList.length > 0) {
|
|
setSelectedUnitId(unitList[0].id);
|
|
}
|
|
}, [activeTab, unitList.length]);
|
|
|
|
const handlePermanentDelete = async () => {
|
|
if (!unitToDelete) return;
|
|
|
|
setIsDeleting(true);
|
|
try {
|
|
await deleteUnit(unitToDelete.id);
|
|
toast.success(t("archive.unitDeletedPermanently", "Unit permanently deleted"));
|
|
setDeleteConfirmOpen(false);
|
|
setUnitToDelete(null);
|
|
queryClient.invalidateQueries({ queryKey: ["archived-units"] });
|
|
refetchArchivedUnits();
|
|
} catch (error: any) {
|
|
if (error?.response?.status === 400 && error?.response?.data?.message) {
|
|
const errorMessage = error?.response?.data?.message;
|
|
|
|
if (errorMessage.includes("Referenced Entity") || errorMessage.includes("referenced")) {
|
|
toast.error(
|
|
t("archive.cannotDeleteUnitWithPositions", "Cannot delete unit with related positions"),
|
|
{
|
|
description: t("archive.deletePositionsFirst", "Please delete or reassign all positions first."),
|
|
duration: 5000
|
|
}
|
|
);
|
|
} else {
|
|
toast.error(t("archive.deleteUnitFailed", "Failed to delete unit"));
|
|
}
|
|
} else {
|
|
toast.error(t("archive.deleteUnitFailed", "Failed to delete unit"));
|
|
}
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
const archivedUnits: any[] = useMemo(
|
|
() => archivedUnitsData?.items ?? archivedUnitsData ?? [],
|
|
[archivedUnitsData],
|
|
);
|
|
const archivedPositions: any[] = useMemo(
|
|
() => archivedPositionsData?.items ?? archivedPositionsData ?? [],
|
|
[archivedPositionsData],
|
|
);
|
|
|
|
const unitColumns = [
|
|
{
|
|
id: "name",
|
|
header: t("organization.name", "Name"),
|
|
cell: ({ row }: any) => (
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
<Building2 className="h-5 w-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">
|
|
{localizedName(row.original?.name) || "—"}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{t("archive.archivedAt", "Archived")}: {new Date(row.original?.archivedAt).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: "key",
|
|
header: t("organization.key", "Key"),
|
|
cell: ({ row }: any) => (
|
|
<Badge variant="outline" className="font-mono bg-gray-50">
|
|
{row.original?.key || "—"}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: "status",
|
|
header: t("common.status", "Status"),
|
|
cell: ({ row }: any) => (
|
|
<Badge variant="destructive" className="bg-amber-100 text-amber-800 hover:bg-amber-100">
|
|
{t("archive.archived", "Archived")}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: "actions",
|
|
header: t("userIncomingretun.Actions", "Actions"),
|
|
cell: ({ row }: any) => (
|
|
<div className="flex gap-2 justify-end">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
disabled={isRestoringUnit}
|
|
onClick={() => restoreUnit(row.original.id)}>
|
|
<ArchiveRestore className="h-4 w-4 text-emerald-600" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setUnitToDelete({
|
|
id: row.original.id,
|
|
name: localizedName(row.original?.name),
|
|
});
|
|
setDeleteConfirmOpen(true);
|
|
}}>
|
|
<Trash2 className="h-4 w-4 text-red-600" />
|
|
</Button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
const positionColumns = [
|
|
{
|
|
id: "name",
|
|
header: t("organization.name", "Name"),
|
|
cell: ({ row }: any) => (
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center flex-shrink-0">
|
|
<UserSquare2 className="h-5 w-5 text-primary-600" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-gray-900">
|
|
{localizedName(row.original?.name) || "—"}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{t("archive.archivedAt", "Archived")}: {new Date(row.original?.archivedAt).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: "key",
|
|
header: t("organization.key", "Key"),
|
|
cell: ({ row }: any) => (
|
|
<Badge variant="outline" className="font-mono bg-gray-50">
|
|
{row.original?.key || "—"}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: "status",
|
|
header: t("common.status", "Status"),
|
|
cell: ({ row }: any) => (
|
|
<Badge variant="destructive" className="bg-amber-100 text-amber-800 hover:bg-amber-100">
|
|
{t("archive.archived", "Archived")}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
id: "actions",
|
|
header: t("userIncomingretun.Actions", "Actions"),
|
|
cell: ({ row }: any) => (
|
|
<div className="flex gap-2 justify-end">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={isRestoringPosition}
|
|
onClick={() => restorePosition(row.original.id)}
|
|
className="gap-1.5 border-emerald-200 text-emerald-700 hover:bg-emerald-50 hover:text-emerald-800">
|
|
<ArchiveRestore className="h-4 w-4" />
|
|
<span>{t("archive.restore", "Restore")}</span>
|
|
</Button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
const isLoading = activeTab === "units" ? isLoadingArchivedUnits : isLoadingArchivedPositions;
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white p-6">
|
|
<div className="max-w-7xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 to-gray-700 bg-clip-text text-transparent">
|
|
{t("archive.archivedItems", "Archived Items")}
|
|
</h1>
|
|
<p className="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-gray-400"></span>
|
|
{t("archive.manageArchivedContent", "Restore or permanently delete archived content")}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="secondary" className="text-sm">
|
|
{activeTab === "units" ? archivedUnits.length : archivedPositions.length} {t("common.items", "items")}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tab Buttons */}
|
|
<div className="flex gap-2 border-b border-gray-200 bg-white rounded-t-xl p-1 shadow-sm">
|
|
<Button
|
|
variant={activeTab === "units" ? "default" : "ghost"}
|
|
onClick={() => setActiveTab("units")}
|
|
className={`flex-1 md:flex-none gap-2 transition-all ${
|
|
activeTab === "units"
|
|
? "shadow-md"
|
|
: "hover:bg-gray-50"
|
|
}`}>
|
|
<Building2 className="h-4 w-4" />
|
|
<span>{t("archive.archivedUnits", "Archived Units")}</span>
|
|
{activeTab === "units" && (
|
|
<span className="ml-1 text-xs bg-white/20 px-2 py-0.5 rounded-full">
|
|
{archivedUnits.length}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant={activeTab === "positions" ? "default" : "ghost"}
|
|
onClick={() => setActiveTab("positions")}
|
|
className={`flex-1 md:flex-none gap-2 transition-all ${
|
|
activeTab === "positions"
|
|
? "shadow-md"
|
|
: "hover:bg-gray-50"
|
|
}`}>
|
|
<UserSquare2 className="h-4 w-4" />
|
|
<span>{t("archive.archivedPositions", "Archived Positions")}</span>
|
|
{activeTab === "positions" && (
|
|
<span className="ml-1 text-xs bg-white/20 px-2 py-0.5 rounded-full">
|
|
{archivedPositions.length}
|
|
</span>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Filter Section */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-4">
|
|
<div className="flex flex-col md:flex-row md:items-end gap-4">
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
|
{activeTab === "units"
|
|
? t("organization.selectOrganization", "Select Organization")
|
|
: t("organization.selectUnit", "Select Unit")}
|
|
</label>
|
|
{activeTab === "units" ? (
|
|
myAdminOrganizations.length === 0 ? (
|
|
<Skeleton className="h-10 w-full" />
|
|
) : (
|
|
<Select
|
|
value={selectedOrgId}
|
|
onValueChange={(value) => setSelectedOrgId(value)}>
|
|
<SelectTrigger className="w-full border-gray-300 focus:ring-2 focus:ring-primary/20">
|
|
<SelectValue
|
|
placeholder={t("organization.selectOrganization")}
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{myAdminOrganizations.map((org: any) => (
|
|
<SelectItem key={org.id} value={org.id}>
|
|
<div className="flex items-center gap-2">
|
|
<Building2 className="h-4 w-4 text-gray-400" />
|
|
{localizedName(org.name)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)
|
|
) : (
|
|
unitList.length === 0 ? (
|
|
<div className="text-sm text-gray-500 text-center py-2">
|
|
{isLoadingUnits ? (
|
|
<Skeleton className="h-10 w-full" />
|
|
) : (
|
|
t("common.noData", "No units available")
|
|
)}
|
|
</div>
|
|
) : (
|
|
<Select
|
|
value={selectedUnitId}
|
|
onValueChange={(value) => setSelectedUnitId(value)}>
|
|
<SelectTrigger className="w-full border-gray-300 focus:ring-2 focus:ring-primary/20">
|
|
<SelectValue placeholder={t("organization.selectUnit")} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{unitList.map((unit) => (
|
|
<SelectItem key={unit.id} value={unit.id}>
|
|
<div className="flex items-center gap-2">
|
|
<Building2 className="h-4 w-4 text-gray-400" />
|
|
{localizedName(unit.name)}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
)
|
|
)}
|
|
</div>
|
|
{activeTab === "units" && (
|
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
<Filter className="h-4 w-4" />
|
|
<span>{t("common.filter", "Filter")}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<Card className="shadow-sm border border-gray-200 overflow-hidden">
|
|
<CardHeader className="bg-gray-50/50 border-b border-gray-200 px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
|
{activeTab === "units" ? (
|
|
<>
|
|
<Building2 className="h-5 w-5 text-primary" />
|
|
{t("archive.archivedUnits", "Archived Units")}
|
|
</>
|
|
) : (
|
|
<>
|
|
<UserSquare2 className="h-5 w-5 text-primary-600" />
|
|
{t("archive.archivedPositions", "Archived Positions")}
|
|
</>
|
|
)}
|
|
</CardTitle>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
if (activeTab === "units") {
|
|
refetchArchivedUnits();
|
|
} else {
|
|
refetchArchivedPositions();
|
|
}
|
|
}}
|
|
className="text-gray-500 hover:text-gray-700">
|
|
<span className="sr-only">{t("common.refresh", "Refresh")}</span>
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{isLoading ? (
|
|
<div className="p-8 space-y-3">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div key={i} className="flex items-center gap-4">
|
|
<Skeleton className="h-12 w-12 rounded-full" />
|
|
<div className="space-y-2 flex-1">
|
|
<Skeleton className="h-4 w-1/3" />
|
|
<Skeleton className="h-3 w-1/4" />
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Skeleton className="h-8 w-20" />
|
|
<Skeleton className="h-8 w-20" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<>
|
|
{activeTab === "units" ? (
|
|
archivedUnits.length === 0 ? (
|
|
<div className="p-8 text-center">
|
|
<div className="mx-auto w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
|
<Building2 className="h-8 w-8 text-gray-400" />
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-1">
|
|
{t("common.noData", "No archived units")}
|
|
</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{t("archive.noArchivedUnits", "No archived units found for this organization")}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<AdvancedTable
|
|
columns={unitColumns as any}
|
|
data={archivedUnits}
|
|
tableName="ArchivedUnits"
|
|
toolBarPosition="right"
|
|
itemCount={archivedUnits.length}
|
|
pageIndex={0}
|
|
onPageChange={() => {}}
|
|
nextFunction={() => {}}
|
|
prevFunction={() => {}}
|
|
refresh={refetchArchivedUnits}
|
|
/>
|
|
)
|
|
) : (
|
|
archivedPositions.length === 0 ? (
|
|
<div className="p-8 text-center">
|
|
<div className="mx-auto w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
|
<UserSquare2 className="h-8 w-8 text-gray-400" />
|
|
</div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-1">
|
|
{t("common.noData", "No archived positions")}
|
|
</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{t("archive.noArchivedPositions", "No archived positions found for this unit")}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<AdvancedTable
|
|
columns={positionColumns as any}
|
|
data={archivedPositions}
|
|
tableName="ArchivedPositions"
|
|
toolBarPosition="right"
|
|
itemCount={archivedPositions.length}
|
|
pageIndex={0}
|
|
onPageChange={() => {}}
|
|
nextFunction={() => {}}
|
|
prevFunction={() => {}}
|
|
refresh={refetchArchivedPositions}
|
|
/>
|
|
)
|
|
)}
|
|
</>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
|
<AlertDialogContent className="sm:max-w-md">
|
|
<AlertDialogHeader>
|
|
<div className="mx-auto w-12 h-12 rounded-full bg-red-100 flex items-center justify-center mb-4">
|
|
<Trash2 className="h-6 w-6 text-red-600" />
|
|
</div>
|
|
<AlertDialogTitle className="text-center">
|
|
{t("archive.deletePermanentlyConfirm", "Permanently delete unit?")}
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-center">
|
|
<span className="block mb-2">
|
|
{t("archive.deletePermanentlyDescription", "This will permanently delete")}
|
|
</span>
|
|
<span className="font-semibold text-gray-900 block text-base">
|
|
"{unitToDelete?.name}"
|
|
</span>
|
|
<span className="block mt-2 text-red-600 text-sm font-medium">
|
|
{t("archive.cannotBeUndone", "This action cannot be undone.")}
|
|
</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter className="flex gap-2">
|
|
<AlertDialogCancel disabled={isDeleting} className="flex-1">
|
|
{t("common.Cancel")}
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handlePermanentDelete}
|
|
disabled={isDeleting}
|
|
className="flex-1 bg-red-600 hover:bg-red-700 focus:ring-red-500">
|
|
{isDeleting ? (
|
|
<span className="flex items-center gap-2">
|
|
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
{t("common.deleting", "Deleting...")}
|
|
</span>
|
|
) : (
|
|
t("archive.deletePermanently", "Delete Permanently")
|
|
)}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ArchivedUnitsPositionsPage; |