"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 (
{/* Drag Handle and Arrow Controls */}
{/* Main Content */}
{/* Description */}
onUpdate(index, "description", e.target.value)}
placeholder="Describe the work performed..."
className="w-full text-sm font-medium"
/>
{/* Controls Row */}
{/* Date */}
onUpdate(index, "date", date ?? new Date())
}
size="sm"
className="h-9 w-36"
/>
{/* Hours */}
onUpdate(index, "hours", value)}
min={0}
step={0.25}
width="auto"
className="h-9 w-32"
/>
{/* Rate */}
onUpdate(index, "rate", value)}
min={0}
step={1}
prefix="$"
width="auto"
className="h-9 w-32"
/>
{/* Amount */}
${(item.hours * item.rate).toFixed(2)}
{/* Actions */}
);
}
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 */}
);
}