Fix escaped quotes in CSV sample and data loading
This commit is contained in:
@@ -11,16 +11,17 @@ import {
|
||||
Star,
|
||||
Loader2,
|
||||
ArrowLeft,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { FormSkeleton } from "~/components/ui/skeleton";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { Switch } from "~/components/ui/switch";
|
||||
import { AddressForm } from "~/components/forms/address-form";
|
||||
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
||||
@@ -90,7 +91,6 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const footerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Fetch business data if editing
|
||||
const { data: business, isLoading: isLoadingBusiness } =
|
||||
@@ -246,11 +246,35 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
};
|
||||
|
||||
if (mode === "edit" && isLoadingBusiness) {
|
||||
return <FormSkeleton />;
|
||||
return (
|
||||
<div className="space-y-6 pb-32">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="mx-auto max-w-6xl pb-32">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Main Form Container - styled like data table */}
|
||||
<div className="space-y-4">
|
||||
@@ -460,55 +484,27 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Form Actions - original position */}
|
||||
<div
|
||||
ref={footerRef}
|
||||
className="border-border/40 bg-background/60 flex items-center justify-between rounded-2xl border p-4 shadow-lg backdrop-blur-xl backdrop-saturate-150"
|
||||
>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{mode === "create"
|
||||
? "Creating a new business"
|
||||
: "Editing business details"}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isDirty}
|
||||
className="btn-brand-primary shadow-md"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{mode === "create" ? "Creating..." : "Saving..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{mode === "create" ? "Create Business" : "Save Changes"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<FloatingActionBar
|
||||
triggerRef={footerRef}
|
||||
title={
|
||||
mode === "create"
|
||||
? "Creating a new business"
|
||||
: "Editing business details"
|
||||
leftContent={
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{mode === "create"
|
||||
? "Creating a new business"
|
||||
: "Editing business details"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{mode === "create"
|
||||
? "Complete the form to create your business"
|
||||
: "Update your business information"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { UserPlus, Save, Loader2, ArrowLeft, DollarSign } from "lucide-react";
|
||||
import {
|
||||
UserPlus,
|
||||
Save,
|
||||
Loader2,
|
||||
ArrowLeft,
|
||||
DollarSign,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { FormSkeleton } from "~/components/ui/skeleton";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { AddressForm } from "~/components/forms/address-form";
|
||||
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
||||
import { NumberInput } from "~/components/ui/number-input";
|
||||
@@ -70,7 +77,6 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const footerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Fetch client data if editing
|
||||
const { data: client, isLoading: isLoadingClient } =
|
||||
@@ -212,11 +218,35 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
};
|
||||
|
||||
if (mode === "edit" && isLoadingClient) {
|
||||
return <FormSkeleton />;
|
||||
return (
|
||||
<div className="space-y-6 pb-32">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="mx-auto max-w-6xl pb-32">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Main Form Container - styled like data table */}
|
||||
<div className="space-y-4">
|
||||
@@ -390,53 +420,27 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Form Actions - original position */}
|
||||
<div
|
||||
ref={footerRef}
|
||||
className="border-border/40 bg-background/60 flex items-center justify-between rounded-2xl border p-4 shadow-lg backdrop-blur-xl backdrop-saturate-150"
|
||||
>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{mode === "create"
|
||||
? "Creating a new client"
|
||||
: "Editing client details"}
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting || !isDirty}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{mode === "create" ? "Creating..." : "Saving..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{mode === "create" ? "Create Client" : "Save Changes"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<FloatingActionBar
|
||||
triggerRef={footerRef}
|
||||
title={
|
||||
mode === "create" ? "Creating a new client" : "Editing client details"
|
||||
leftContent={
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{mode === "create"
|
||||
? "Creating a new client"
|
||||
: "Editing client details"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{mode === "create"
|
||||
? "Complete the form to create your client"
|
||||
: "Update your client information"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Input } from "~/components/ui/input";
|
||||
@@ -52,10 +52,10 @@ function InvoiceFormSkeleton() {
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Left Column - Content with Tabs */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Tabs - Mobile stacked, desktop side-by-side */}
|
||||
<div className="bg-muted grid w-full grid-cols-1 gap-1 rounded-lg p-1 sm:grid-cols-2">
|
||||
<div className="bg-background h-9 rounded-md shadow-sm sm:h-10"></div>
|
||||
<div className="bg-muted/30 h-9 rounded-md sm:h-10"></div>
|
||||
{/* Tabs - Match actual TabsList structure */}
|
||||
<div className="bg-muted text-muted-foreground inline-flex h-9 w-full items-center justify-center rounded-lg p-[3px]">
|
||||
<div className="bg-background h-[calc(100%-1px)] flex-1 rounded-md shadow-sm"></div>
|
||||
<div className="bg-muted/30 h-[calc(100%-1px)] flex-1 rounded-md"></div>
|
||||
</div>
|
||||
|
||||
{/* Invoice Details Card */}
|
||||
@@ -233,7 +233,7 @@ function InvoiceFormSkeleton() {
|
||||
|
||||
export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
const router = useRouter();
|
||||
const footerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`,
|
||||
businessId: "",
|
||||
@@ -406,6 +406,14 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
});
|
||||
};
|
||||
|
||||
// Reorder items
|
||||
const reorderItems = (newItems: typeof formData.items) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
items: newItems,
|
||||
}));
|
||||
};
|
||||
|
||||
// tRPC mutations
|
||||
const createInvoice = api.invoices.create.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -470,14 +478,33 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 pb-32">
|
||||
<PageHeader
|
||||
title={invoiceId ? "Edit Invoice" : "Create Invoice"}
|
||||
description={
|
||||
invoiceId ? "Update invoice details" : "Create a new invoice"
|
||||
}
|
||||
variant="gradient"
|
||||
/>
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
form="invoice-form"
|
||||
disabled={loading}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
<span className="hidden sm:inline">Saving...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Save Invoice</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
{/* Form Content */}
|
||||
<form id="invoice-form" onSubmit={handleSubmit} className="space-y-6">
|
||||
@@ -698,6 +725,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
onUpdateItem={updateItem}
|
||||
onMoveUp={moveItemUp}
|
||||
onMoveDown={moveItemDown}
|
||||
onReorderItems={reorderItems}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -768,63 +796,21 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Footer for floating bar trigger */}
|
||||
<div
|
||||
ref={footerRef}
|
||||
className="border-border/40 bg-background/60 flex items-center justify-between rounded-2xl border p-4 shadow-lg backdrop-blur-xl backdrop-saturate-150"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
|
||||
<FileText className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{invoiceId ? "Edit Invoice" : "Create Invoice"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{invoiceId ? "Update invoice details" : "Create a new invoice"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="submit"
|
||||
form="invoice-form"
|
||||
disabled={loading}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 shadow-md transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-lg"
|
||||
size="sm"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
<span className="hidden sm:inline">Saving...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Save Invoice</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Action Bar */}
|
||||
<FloatingActionBar
|
||||
triggerRef={footerRef}
|
||||
leftContent={
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
|
||||
<FileText className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
||||
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
|
||||
<FileText className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{invoiceId ? "Edit Invoice" : "Create Invoice"}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{invoiceId ? "Update invoice details" : "Create a new invoice"}
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Update invoice details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,23 @@ import {
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { cn } from "~/lib/utils";
|
||||
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";
|
||||
|
||||
interface InvoiceItem {
|
||||
id: string;
|
||||
@@ -36,6 +53,7 @@ interface InvoiceLineItemsProps {
|
||||
) => void;
|
||||
onMoveUp: (index: number) => void;
|
||||
onMoveDown: (index: number) => void;
|
||||
onReorderItems: (items: InvoiceItem[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -55,19 +73,100 @@ interface LineItemRowProps {
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
function LineItemRow({
|
||||
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,
|
||||
}: LineItemRowProps) {
|
||||
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 (
|
||||
<div className="card-secondary hidden rounded-lg p-4 md:block">
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"card-secondary hidden rounded-lg p-4 md:block",
|
||||
isDragging && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Drag Handle */}
|
||||
<div className="mt-1 flex items-center justify-center">
|
||||
<GripVertical className="text-muted-foreground h-4 w-4 cursor-grab" />
|
||||
{/* Drag Handle and Arrow Controls */}
|
||||
<div className="mt-1 flex flex-col items-center gap-1">
|
||||
<div
|
||||
className="cursor-grab active:cursor-grabbing"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<GripVertical className="text-muted-foreground h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMoveUp(index)}
|
||||
className={cn(
|
||||
"h-6 w-6 p-0 transition-colors",
|
||||
isFirst
|
||||
? "text-muted-foreground/50 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
disabled={isFirst}
|
||||
aria-label="Move up"
|
||||
>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onMoveDown(index)}
|
||||
className={cn(
|
||||
"h-6 w-6 p-0 transition-colors",
|
||||
isLast
|
||||
? "text-muted-foreground/50 cursor-not-allowed"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
disabled={isLast}
|
||||
aria-label="Move down"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -280,44 +379,75 @@ export function InvoiceLineItems({
|
||||
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 (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
{/* Desktop and Mobile Cards */}
|
||||
<div className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={item.id}>
|
||||
{/* Desktop/Tablet Card */}
|
||||
<LineItemRow
|
||||
item={item}
|
||||
index={index}
|
||||
canRemove={canRemoveItems}
|
||||
onRemove={onRemoveItem}
|
||||
onUpdate={onUpdateItem}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
isFirst={index === 0}
|
||||
isLast={index === items.length - 1}
|
||||
/>
|
||||
{/* Desktop Cards with Drag and Drop */}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={items.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<React.Fragment key={item.id}>
|
||||
{/* Desktop/Tablet Card with Drag and Drop */}
|
||||
<SortableLineItem
|
||||
item={item}
|
||||
index={index}
|
||||
canRemove={canRemoveItems}
|
||||
onRemove={onRemoveItem}
|
||||
onUpdate={onUpdateItem}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
isFirst={index === 0}
|
||||
isLast={index === items.length - 1}
|
||||
/>
|
||||
|
||||
{/* Mobile Card */}
|
||||
<MobileLineItem
|
||||
item={item}
|
||||
index={index}
|
||||
canRemove={canRemoveItems}
|
||||
onRemove={onRemoveItem}
|
||||
onUpdate={onUpdateItem}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
isFirst={index === 0}
|
||||
isLast={index === items.length - 1}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{/* Mobile Card */}
|
||||
<MobileLineItem
|
||||
item={item}
|
||||
index={index}
|
||||
canRemove={canRemoveItems}
|
||||
onRemove={onRemoveItem}
|
||||
onUpdate={onUpdateItem}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
isFirst={index === 0}
|
||||
isLast={index === items.length - 1}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{/* Add Item Button */}
|
||||
<div className="px-3 pt-3">
|
||||
|
||||
Reference in New Issue
Block a user