Files
edr-platform/apps/edr-freight-web/backoffice/src/pages/ArchivedUnitsPositionsPage.tsx
yaschalew 697799b00d fix
2026-07-10 14:24:58 +03:00

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-blue-100 flex items-center justify-center flex-shrink-0">
<UserSquare2 className="h-5 w-5 text-blue-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-blue-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;