mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
fix ui
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EditIcon, TrashIcon, PlusIcon } from "lucide-react";
|
||||
import { Button } from "@/shared/common/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/shared/common/ui/alert-dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/common/ui/dialog";
|
||||
import { useAuth } from "@/shared/context/AuthContext";
|
||||
import {
|
||||
useServiceCategoryList,
|
||||
useServiceCategoryMutations,
|
||||
} from "@/performance-management/hooks/useServiceCategory";
|
||||
import { ServiceCategory } from "@/performance-management/services/api/serviceCategoryService";
|
||||
import { ServiceCategoryForm } from "@/performance-management/components/ServiceCategory/ServiceCategoryForm";
|
||||
import { useLocalizedName } from "@/shared/common/localizedName";
|
||||
import { AdvancedTable } from "@/shared/common/ui/table/AdvancedTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { useToast } from "@/shared/common/ui/use-toast";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/shared/common/ui/tooltip";
|
||||
import { Skeleton } from "@/shared/common/ui/skeleton";
|
||||
|
||||
export const ServiceCategoryPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const localizedName = useLocalizedName();
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const { delete: deleteCategory } = useServiceCategoryMutations();
|
||||
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [editingCategory, setEditingCategory] =
|
||||
useState<ServiceCategory | null>(null);
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
useState<ServiceCategory | null>(null);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
|
||||
const organizationId = user?.employee?.[0]?.organizationId;
|
||||
|
||||
const {
|
||||
data: serviceCategoryList,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
} = useServiceCategoryList(organizationId ?? "");
|
||||
|
||||
const categories = (serviceCategoryList?.data?.items || []).sort(
|
||||
(a: ServiceCategory, b: ServiceCategory) => {
|
||||
const dateA = new Date(a.createdAt).getTime();
|
||||
const dateB = new Date(b.createdAt).getTime();
|
||||
return dateB - dateA; // Descending order (newest first)
|
||||
}
|
||||
);
|
||||
|
||||
const handleEdit = (category: ServiceCategory) => {
|
||||
setEditingCategory(category);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedCategory) return;
|
||||
|
||||
try {
|
||||
await deleteCategory.mutateAsync(selectedCategory.id);
|
||||
toast({
|
||||
title: t("serviceCategory.deleteSuccess", "Deleted"),
|
||||
description: t(
|
||||
"serviceCategory.deleteSuccessMessage",
|
||||
"Service category deleted successfully"
|
||||
),
|
||||
});
|
||||
setIsDeleteDialogOpen(false);
|
||||
setSelectedCategory(null);
|
||||
refetch();
|
||||
} catch {
|
||||
toast({
|
||||
title: t("common.error", "Error"),
|
||||
description: t(
|
||||
"serviceCategory.deleteError",
|
||||
"Failed to delete service category"
|
||||
),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSuccess = () => {
|
||||
setIsCreateDialogOpen(false);
|
||||
setEditingCategory(null);
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleFormCancel = () => {
|
||||
setIsCreateDialogOpen(false);
|
||||
setEditingCategory(null);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<ServiceCategory>[] = [
|
||||
{
|
||||
accessorKey: "slug",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="h-auto p-0 font-semibold"
|
||||
>
|
||||
{t("serviceCategory.slug", "Slug")}
|
||||
{column.getIsSorted() === "asc"
|
||||
? " ↑"
|
||||
: column.getIsSorted() === "desc"
|
||||
? " ↓"
|
||||
: ""}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => <div>{row.original.slug}</div>,
|
||||
size: 150,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="h-auto p-0 font-semibold"
|
||||
>
|
||||
{t("serviceCategory.name", "Name")}
|
||||
{column.getIsSorted() === "asc"
|
||||
? " ↑"
|
||||
: column.getIsSorted() === "desc"
|
||||
? " ↓"
|
||||
: ""}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => <div>{localizedName(row.original.name)}</div>,
|
||||
size: 200,
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const nameA = localizedName(rowA.original.name).toLowerCase();
|
||||
const nameB = localizedName(rowB.original.name).toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="h-auto p-0 font-semibold"
|
||||
>
|
||||
{t("serviceCategory.description", "Description")}
|
||||
{column.getIsSorted() === "asc"
|
||||
? " ↑"
|
||||
: column.getIsSorted() === "desc"
|
||||
? " ↓"
|
||||
: ""}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div
|
||||
className="max-w-[300px] truncate"
|
||||
title={localizedName(row.original.description)}
|
||||
>
|
||||
{localizedName(row.original.description)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const descA = localizedName(rowA.original.description).toLowerCase();
|
||||
const descB = localizedName(rowB.original.description).toLowerCase();
|
||||
return descA.localeCompare(descB);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
className="h-auto p-0 font-semibold"
|
||||
>
|
||||
{t("serviceCategory.createdAt", "Created At")}
|
||||
{column.getIsSorted() === "asc"
|
||||
? " ↑"
|
||||
: column.getIsSorted() === "desc"
|
||||
? " ↓"
|
||||
: " ↓"}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),
|
||||
size: 150,
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const dateA = new Date(rowA.original.createdAt).getTime();
|
||||
const dateB = new Date(rowB.original.createdAt).getTime();
|
||||
return dateA - dateB;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: t("common.actions", "Actions"),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.edit", "Edit")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedCategory(row.original);
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}
|
||||
disabled={deleteCategory.isPending}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("common.delete", "Delete")}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
),
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const extraToolbar = (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => setIsCreateDialogOpen(true)}
|
||||
className="bg-gradient-to-r from-purple-500 to-violet-600 text-white"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
{t("serviceCategory.createNew", "Create Category")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Loading UI
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
<Skeleton className="h-12 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error UI
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
<p className="text-destructive font-semibold">
|
||||
{t("serviceCategory.loadError", "Failed to load service categories")}
|
||||
</p>
|
||||
<Button onClick={() => refetch()}>{t("common.retry", "Retry")}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<AdvancedTable<ServiceCategory, unknown>
|
||||
columns={columns}
|
||||
data={categories}
|
||||
tableName={t("serviceCategory.title", "Service Categories")}
|
||||
extraToolbar={extraToolbar}
|
||||
toolBarPosition="right"
|
||||
itemCount={categories.length}
|
||||
pageIndex={pageIndex}
|
||||
onPageChange={setPageIndex}
|
||||
nextFunction={() => setPageIndex((p) => p + 1)}
|
||||
prevFunction={() => setPageIndex((p) => Math.max(0, p - 1))}
|
||||
refresh={refetch}
|
||||
/>
|
||||
|
||||
{/* Create Dialog */}
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader></DialogHeader>
|
||||
<ServiceCategoryForm
|
||||
onSuccess={handleFormSuccess}
|
||||
onCancel={handleFormCancel}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Dialog */}
|
||||
<Dialog
|
||||
open={!!editingCategory}
|
||||
onOpenChange={() => setEditingCategory(null)}
|
||||
>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("serviceCategory.editTitle", "Edit Service Category")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{editingCategory && (
|
||||
<ServiceCategoryForm
|
||||
category={editingCategory}
|
||||
onSuccess={handleFormSuccess}
|
||||
onCancel={handleFormCancel}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Dialog */}
|
||||
<AlertDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("serviceCategory.deleteTitle", "Delete Service Category")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t(
|
||||
"serviceCategory.deleteConfirmation",
|
||||
'Are you sure you want to delete "{{name}}"? This action cannot be undone.',
|
||||
{ name: localizedName(selectedCategory?.name) }
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>
|
||||
{t("common.cancel", "Cancel")}
|
||||
</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
disabled={deleteCategory.isPending}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{deleteCategory.isPending
|
||||
? t("common.deleting", "Deleting...")
|
||||
: t("common.delete", "Delete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user