"use client";
import * as React from "react";
import { useEffect, useState } from "react";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { Input } from "~/components/ui/input";
import { Button } from "~/components/ui/button";
import { Label } from "~/components/ui/label";
import { DatePicker } from "~/components/ui/date-picker";
import { NumberInput } from "~/components/ui/number-input";
import { Textarea } from "~/components/ui/textarea";
import { Trash2, GripVertical, ChevronUp, ChevronDown } from "lucide-react";
interface InvoiceItem {
id: string;
date: Date;
description: string;
hours: number;
rate: number;
amount: number;
}
interface EditableInvoiceItemsProps {
items: InvoiceItem[];
onItemsChange: (items: InvoiceItem[]) => void;
onRemoveItem: (index: number) => void;
}
function SortableItem({
item,
index,
onItemChange,
onRemove,
onMoveUp,
onMoveDown,
canMoveUp,
canMoveDown,
}: {
item: InvoiceItem;
index: number;
onItemChange: (
index: number,
field: string,
value: string | number | Date,
) => void;
onRemove: (index: number) => void;
onMoveUp: (index: number) => void;
onMoveDown: (index: number) => void;
canMoveUp: boolean;
canMoveDown: boolean;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: item.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const handleItemChange = (field: string, value: string | number | Date) => {
onItemChange(index, field, value);
};
return (
{/* Desktop Layout - Hidden on Mobile */}
{/* Drag Handle */}
{/* Date */}
handleItemChange("date", date ?? new Date())
}
size="sm"
className="w-full"
/>
{/* Description */}
handleItemChange("description", e.target.value)}
placeholder="Work description"
className="h-9"
/>
{/* Hours */}
handleItemChange("hours", value)}
min={0}
step={0.25}
placeholder="0"
width="full"
/>
{/* Rate */}
handleItemChange("rate", value)}
min={0}
step={0.01}
placeholder="0.00"
prefix="$"
width="full"
/>
{/* Amount */}
${item.amount.toFixed(2)}
{/* Remove Button */}
{/* Mobile Layout - Visible on Mobile Only */}
{/* Header with Item Number and Controls */}
Item {index + 1}
{/* Description */}
{/* Date */}
handleItemChange("date", date ?? new Date())
}
size="sm"
className="w-full"
/>
{/* Hours and Rate */}
handleItemChange("hours", value)}
min={0}
step={0.25}
placeholder="0"
width="full"
/>
handleItemChange("rate", value)}
min={0}
step={0.01}
placeholder="0.00"
prefix="$"
width="full"
/>
{/* Amount */}
Total Amount:
${item.amount.toFixed(2)}
);
}
export function EditableInvoiceItems({
items,
onItemsChange,
onRemoveItem,
}: EditableInvoiceItemsProps) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const 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);
onItemsChange(newItems);
}
};
const handleItemChange = (
index: number,
field: string,
value: string | number | Date,
) => {
const newItems = [...items];
if (field === "hours" || field === "rate") {
if (newItems[index]) {
const numValue =
typeof value === "string"
? parseFloat(value)
: typeof value === "number"
? value
: 0;
newItems[index][field] = numValue || 0;
newItems[index].amount = newItems[index].hours * newItems[index].rate;
}
} else if (field === "date") {
if (newItems[index]) {
const dateValue =
value instanceof Date ? value : new Date(String(value));
newItems[index].date = dateValue;
}
} else {
if (newItems[index]) {
const stringValue = typeof value === "string" ? value : String(value);
newItems[index].description = stringValue;
}
}
onItemsChange(newItems);
};
const handleMoveUp = (index: number) => {
if (index > 0) {
const newItems = arrayMove(items, index, index - 1);
onItemsChange(newItems);
}
};
const handleMoveDown = (index: number) => {
if (index < items.length - 1) {
const newItems = arrayMove(items, index, index + 1);
onItemsChange(newItems);
}
};
// Show skeleton loading on server-side
if (!isClient) {
return (
{items.map((item, _index) => (
{/* Desktop Skeleton */}
{/* Mobile Skeleton */}
))}
);
}
return (
<>
{/* Desktop Header Labels - Hidden on Mobile */}
Date
Description
Hours
Rate
Amount
item.id)}
strategy={verticalListSortingStrategy}
>
{items.map((item, index) => (
0}
canMoveDown={index < items.length - 1}
/>
))}
>
);
}