Ver código fonte

feat(layout-configurations): add delete functionality for layout sections

- Add deleteLayoutSection server action to remove sections and associated fields
- Create DeleteSectionDialog component for user confirmation
- Update LayoutSectionCard with delete dropdown menu and dialog integration
- Refresh sections list after successful deletion in detail view
- Add proper error handling and user feedback with toast notifications
vtugulan 6 meses atrás
pai
commit
346e2179a6

+ 24 - 4
app/actions/layout-configurations.ts

@@ -257,8 +257,28 @@ export async function createLayoutSection(
       },
     });
     return { success: true, data: section };
-  } catch (error) {
-    console.error("Error creating layout section:", error);
-    return { success: false, error: "Failed to create layout section" };
+    } catch (error) {
+      console.error("Error creating layout section:", error);
+      return { success: false, error: "Failed to create layout section" };
+    }
   }
-}
+  
+  // Delete layout section
+  export async function deleteLayoutSection(id: number) {
+    try {
+      // First delete all fields associated with this section
+      await prisma.layoutSectionField.deleteMany({
+        where: { layoutSectionId: id },
+      });
+      
+      // Then delete the section itself
+      await prisma.layoutSection.delete({
+        where: { id },
+      });
+      
+      return { success: true };
+    } catch (error) {
+      console.error("Error deleting layout section:", error);
+      return { success: false, error: "Failed to delete layout section" };
+    }
+  }

+ 94 - 0
app/components/layout-configurations/DeleteSectionDialog.tsx

@@ -0,0 +1,94 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { deleteLayoutSection } from "@/app/actions/layout-configurations";
+import { useToast } from "@/hooks/use-toast";
+
+interface DeleteSectionDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  section: {
+    id: number;
+    name: string;
+  };
+  onSuccess?: () => void;
+}
+
+export function DeleteSectionDialog({
+  open,
+  onOpenChange,
+  section,
+  onSuccess,
+}: DeleteSectionDialogProps) {
+  const [isDeleting, setIsDeleting] = useState(false);
+  const { toast } = useToast();
+
+  const handleDelete = async () => {
+    setIsDeleting(true);
+    try {
+      const result = await deleteLayoutSection(section.id);
+      
+      if (result.success) {
+        toast({
+          title: "Section deleted",
+          description: `Successfully deleted section "${section.name}"`,
+        });
+        onOpenChange(false);
+        onSuccess?.();
+      } else {
+        toast({
+          title: "Error",
+          description: result.error || "Failed to delete section",
+          variant: "destructive",
+        });
+      }
+    } catch {
+      toast({
+        title: "Error",
+        description: "An unexpected error occurred while deleting the section",
+        variant: "destructive",
+      });
+    } finally {
+      setIsDeleting(false);
+    }
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent>
+        <DialogHeader>
+          <DialogTitle>Delete Section</DialogTitle>
+          <DialogDescription>
+            Are you sure you want to delete the section &quot;{section.name}&quot;? This action
+            cannot be undone. All fields within this section will also be deleted.
+          </DialogDescription>
+        </DialogHeader>
+        <DialogFooter>
+          <Button
+            variant="outline"
+            onClick={() => onOpenChange(false)}
+            disabled={isDeleting}
+          >
+            Cancel
+          </Button>
+          <Button
+            variant="destructive"
+            onClick={handleDelete}
+            disabled={isDeleting}
+          >
+            {isDeleting ? "Deleting..." : "Delete"}
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 13 - 5
app/components/layout-configurations/LayoutConfigurationDetail.tsx

@@ -45,6 +45,7 @@ interface LayoutConfigurationDetailProps {
 export function LayoutConfigurationDetail({ configuration }: LayoutConfigurationDetailProps) {
   const router = useRouter();
   const [isDialogOpen, setIsDialogOpen] = useState(false);
+  const [sections, setSections] = useState(configuration.sections);
 
   return (
     <div className="space-y-6">
@@ -70,10 +71,10 @@ export function LayoutConfigurationDetail({ configuration }: LayoutConfiguration
             </div>
             <div className="flex gap-2">
               <Badge variant="secondary">
-                {configuration.sections.length} sections
+                {sections.length} sections
               </Badge>
               <Badge variant="outline">
-                {configuration.sections.reduce((total, section) => total + section.fields.length, 0)} fields
+                {sections.reduce((total, section) => total + section.fields.length, 0)} fields
               </Badge>
             </div>
           </div>
@@ -102,7 +103,7 @@ export function LayoutConfigurationDetail({ configuration }: LayoutConfiguration
           </Button>
         </div>
 
-        {configuration.sections.length === 0 ? (
+        {sections.length === 0 ? (
           <Card>
             <CardContent className="pt-6">
               <p className="text-center text-muted-foreground">
@@ -112,8 +113,15 @@ export function LayoutConfigurationDetail({ configuration }: LayoutConfiguration
           </Card>
         ) : (
           <div className="grid gap-4">
-            {configuration.sections.map((section) => (
-              <LayoutSectionCard key={section.id} section={section} />
+            {sections.map((section) => (
+              <LayoutSectionCard
+                key={section.id}
+                section={section}
+                onSectionDeleted={() => {
+                  // Refresh the sections list
+                  setSections(prev => prev.filter(s => s.id !== section.id));
+                }}
+              />
             ))}
           </div>
         )}

+ 112 - 61
app/components/layout-configurations/LayoutSectionCard.tsx

@@ -1,8 +1,13 @@
 "use client";
 
+import { useState } from "react";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
 import { Badge } from "@/components/ui/badge";
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import { Button } from "@/components/ui/button";
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
+import { MoreVertical, Trash2 } from "lucide-react";
+import { DeleteSectionDialog } from "./DeleteSectionDialog";
 
 interface LayoutSection {
   id: number;
@@ -27,9 +32,12 @@ interface LayoutSectionField {
 
 interface LayoutSectionCardProps {
   section: LayoutSection;
+  onSectionDeleted?: () => void;
 }
 
-export function LayoutSectionCard({ section }: LayoutSectionCardProps) {
+export function LayoutSectionCard({ section, onSectionDeleted }: LayoutSectionCardProps) {
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  
   const getTypeColor = (type: string) => {
     switch (type.toLowerCase()) {
       case "properties":
@@ -41,72 +49,115 @@ export function LayoutSectionCard({ section }: LayoutSectionCardProps) {
     }
   };
 
+  const handleCardClick = (e: React.MouseEvent) => {
+    // Prevent navigation when clicking on the dropdown menu
+    if ((e.target as HTMLElement).closest('.section-actions')) {
+      return;
+    }
+    window.location.href = `/layout-configurations/sections/${section.id}/fields`;
+  };
+
   return (
-    <Card
-      className="cursor-pointer hover:shadow-lg transition-shadow"
-      onClick={() => window.location.href = `/layout-configurations/sections/${section.id}/fields`}
-    >
-      <CardHeader>
-        <div className="flex justify-between items-start">
-          <div>
-            <CardTitle className="text-lg">{section.name}</CardTitle>
-            <CardDescription>
-              Sheet: {section.sheetName} | Table: {section.tableName}
-            </CardDescription>
+    <>
+      <Card
+        className="cursor-pointer hover:shadow-lg transition-shadow relative"
+        onClick={handleCardClick}
+      >
+        <CardHeader>
+          <div className="flex justify-between items-start">
+            <div>
+              <CardTitle className="text-lg">{section.name}</CardTitle>
+              <CardDescription>
+                Sheet: {section.sheetName} | Table: {section.tableName}
+              </CardDescription>
+            </div>
+            <div className="section-actions flex items-center gap-2">
+              <Badge className={getTypeColor(section.type)}>
+                {section.type}
+              </Badge>
+              <DropdownMenu>
+                <DropdownMenuTrigger asChild>
+                  <Button
+                    variant="ghost"
+                    size="sm"
+                    className="h-8 w-8 p-0"
+                    onClick={(e) => e.stopPropagation()}
+                  >
+                    <MoreVertical className="h-4 w-4" />
+                  </Button>
+                </DropdownMenuTrigger>
+                <DropdownMenuContent align="end">
+                  <DropdownMenuItem
+                    className="text-destructive"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      setDeleteDialogOpen(true);
+                    }}
+                  >
+                    <Trash2 className="mr-2 h-4 w-4" />
+                    Delete
+                  </DropdownMenuItem>
+                </DropdownMenuContent>
+              </DropdownMenu>
+            </div>
           </div>
-          <Badge className={getTypeColor(section.type)}>
-            {section.type}
-          </Badge>
-        </div>
-      </CardHeader>
-      <CardContent>
-        <div className="space-y-4">
-          <div className="flex gap-4 text-sm">
-            {section.startingRow && (
+        </CardHeader>
+        <CardContent>
+          <div className="space-y-4">
+            <div className="flex gap-4 text-sm">
+              {section.startingRow && (
+                <span>
+                  <strong>Rows:</strong> {section.startingRow} - {section.endingRow || "end"}
+                </span>
+              )}
               <span>
-                <strong>Rows:</strong> {section.startingRow} - {section.endingRow || "end"}
+                <strong>Fields:</strong> {section.fields.length}
               </span>
-            )}
-            <span>
-              <strong>Fields:</strong> {section.fields.length}
-            </span>
-          </div>
+            </div>
 
-          {section.fields.length > 0 && (
-            <div>
-              <h4 className="font-medium mb-2">Fields</h4>
-              <div className="overflow-x-auto">
-                <Table>
-                  <TableHeader>
-                    <TableRow>
-                      <TableHead>Name</TableHead>
-                      <TableHead>Type</TableHead>
-                      <TableHead>Position</TableHead>
-                      <TableHead>Column</TableHead>
-                      <TableHead>Order</TableHead>
-                    </TableRow>
-                  </TableHeader>
-                  <TableBody>
-                    {section.fields.map((field) => (
-                      <TableRow key={field.id}>
-                        <TableCell className="font-medium">{field.name}</TableCell>
-                        <TableCell>
-                          <Badge variant="outline" className="text-xs">
-                            {field.dataType}
-                          </Badge>
-                        </TableCell>
-                        <TableCell>{field.cellPosition}</TableCell>
-                        <TableCell>{field.importTableColumnName}</TableCell>
-                        <TableCell>{field.importColumnOrderNumber}</TableCell>
+            {section.fields.length > 0 && (
+              <div>
+                <h4 className="font-medium mb-2">Fields</h4>
+                <div className="overflow-x-auto">
+                  <Table>
+                    <TableHeader>
+                      <TableRow>
+                        <TableHead>Name</TableHead>
+                        <TableHead>Type</TableHead>
+                        <TableHead>Position</TableHead>
+                        <TableHead>Column</TableHead>
+                        <TableHead>Order</TableHead>
                       </TableRow>
-                    ))}
-                  </TableBody>
-                </Table>
+                    </TableHeader>
+                    <TableBody>
+                      {section.fields.map((field) => (
+                        <TableRow key={field.id}>
+                          <TableCell className="font-medium">{field.name}</TableCell>
+                          <TableCell>
+                            <Badge variant="outline" className="text-xs">
+                              {field.dataType}
+                            </Badge>
+                          </TableCell>
+                          <TableCell>{field.cellPosition}</TableCell>
+                          <TableCell>{field.importTableColumnName}</TableCell>
+                          <TableCell>{field.importColumnOrderNumber}</TableCell>
+                        </TableRow>
+                      ))}
+                    </TableBody>
+                  </Table>
+                </div>
               </div>
-            </div>
-          )}
-        </div>
-      </CardContent>
-    </Card>
+            )}
+          </div>
+        </CardContent>
+      </Card>
+
+      <DeleteSectionDialog
+        open={deleteDialogOpen}
+        onOpenChange={setDeleteDialogOpen}
+        section={{ id: section.id, name: section.name }}
+        onSuccess={onSectionDeleted}
+      />
+    </>
   );
 }