"use client"; import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { ChevronDown, ChevronUp, GripVertical, Plus, Trash2, } from "lucide-react"; import * as React from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Button } from "~/components/ui/button"; import { DatePicker } from "~/components/ui/date-picker"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { NumberInput } from "~/components/ui/number-input"; import { cn } from "~/lib/utils"; interface InvoiceItem { id: string; date: Date; description: string; hours: number; rate: number; amount: number; } interface InvoiceLineItemsProps { items: InvoiceItem[]; onAddItem: () => void; onRemoveItem: (index: number) => void; onUpdateItem: ( index: number, field: string, value: string | number | Date, ) => void; onMoveUp: (index: number) => void; onMoveDown: (index: number) => void; onReorderItems: (items: InvoiceItem[]) => void; className?: string; } interface LineItemRowProps { item: InvoiceItem; index: number; canRemove: boolean; onRemove: (index: number) => void; onUpdate: ( index: number, field: string, value: string | number | Date, ) => void; onMoveUp: (index: number) => void; onMoveDown: (index: number) => void; isFirst: boolean; isLast: boolean; } interface SortableLineItemProps { item: InvoiceItem; index: number; canRemove: boolean; onRemove: (index: number) => void; onUpdate: ( index: number, field: string, value: string | number | Date, ) => void; onMoveUp: (index: number) => void; onMoveDown: (index: number) => void; isFirst: boolean; isLast: boolean; } function SortableLineItem({ item, index, canRemove, onRemove, onUpdate, onMoveUp, onMoveDown, isFirst, isLast, }: SortableLineItemProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id }); const style = { transform: CSS.Transform.toString(transform), transition, }; return ( ); } function MobileLineItem({ item, index, canRemove, onRemove, onUpdate, onMoveUp, onMoveDown, isFirst, isLast, }: LineItemRowProps) { return (
{/* Description */}
onUpdate(index, "description", e.target.value)} placeholder="Describe the work performed..." className="pl-3 text-sm" />
{/* Date */}
onUpdate(index, "date", date ?? new Date())} size="sm" />
{/* Hours and Rate in a row */}
onUpdate(index, "hours", value)} min={0} step={0.25} width="full" />
onUpdate(index, "rate", value)} min={0} step={1} prefix="$" width="full" />
{/* Bottom section with controls, item name, and total */}
Item # {index + 1}
Total ${(item.hours * item.rate).toFixed(2)}
); } export function InvoiceLineItems({ items, onAddItem, onRemoveItem, onUpdateItem, onMoveUp, onMoveDown, onReorderItems, className, }: InvoiceLineItemsProps) { const canRemoveItems = items.length > 1; const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (active.id !== over?.id) { const oldIndex = items.findIndex((item) => item.id === active.id); const newIndex = items.findIndex((item) => item.id === over?.id); const newItems = arrayMove(items, oldIndex, newIndex); onReorderItems(newItems); } } return (
{/* Desktop Cards with Drag and Drop */} item.id)} strategy={verticalListSortingStrategy} >
{items.map((item, index) => ( {/* Desktop/Tablet Card with Drag and Drop */} {/* Mobile Card */} ))}
{/* Add Item Button */}
); }