mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 09:38:55 -04:00
remove reordering controls, add auto sort
This commit is contained in:
@@ -108,10 +108,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount: number, currency = "USD") => {
|
||||||
return new Intl.NumberFormat("en-US", {
|
return new Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: "USD",
|
currency,
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
|||||||
onClick={handlePDFExport}
|
onClick={handlePDFExport}
|
||||||
disabled={isExportingPDF}
|
disabled={isExportingPDF}
|
||||||
variant="default"
|
variant="default"
|
||||||
className="transform-none flex-shrink-0"
|
className="flex-shrink-0 transform-none"
|
||||||
>
|
>
|
||||||
{isExportingPDF ? (
|
{isExportingPDF ? (
|
||||||
<>
|
<>
|
||||||
@@ -432,7 +432,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
|||||||
Tax ({((invoice.taxRate ?? 0) * 100).toFixed(1)}%)
|
Tax ({((invoice.taxRate ?? 0) * 100).toFixed(1)}%)
|
||||||
</span>
|
</span>
|
||||||
<span className="text-foreground font-medium">
|
<span className="text-foreground font-medium">
|
||||||
{formatCurrency(invoice.totalAmount * (invoice.taxRate ?? 0), invoice.currency)}
|
{formatCurrency(
|
||||||
|
invoice.totalAmount * (invoice.taxRate ?? 0),
|
||||||
|
invoice.currency,
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -440,7 +443,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
|||||||
<div className="flex justify-between text-lg font-bold">
|
<div className="flex justify-between text-lg font-bold">
|
||||||
<span className="text-foreground">Total</span>
|
<span className="text-foreground">Total</span>
|
||||||
<span className="text-primary">
|
<span className="text-primary">
|
||||||
{formatCurrency(invoice.totalAmount * (1 + (invoice.taxRate ?? 0)), invoice.currency)}
|
{formatCurrency(
|
||||||
|
invoice.totalAmount * (1 + (invoice.taxRate ?? 0)),
|
||||||
|
invoice.currency,
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,15 @@ import { InvoiceLineItems } from "./invoice-line-items";
|
|||||||
import { InvoiceCalendarView } from "./invoice-calendar-view";
|
import { InvoiceCalendarView } from "./invoice-calendar-view";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Save, Calendar as CalendarIcon, Tag, User, List, FileText, ChevronDown } from "lucide-react";
|
import {
|
||||||
|
Save,
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
Tag,
|
||||||
|
User,
|
||||||
|
List,
|
||||||
|
FileText,
|
||||||
|
ChevronDown,
|
||||||
|
} from "lucide-react";
|
||||||
import { SUPPORTED_CURRENCIES } from "~/lib/currency";
|
import { SUPPORTED_CURRENCIES } from "~/lib/currency";
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
@@ -101,7 +109,9 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
// Queries (Same as before)
|
// Queries (Same as before)
|
||||||
const { data: clients, isLoading: loadingClients } =
|
const { data: clients, isLoading: loadingClients } =
|
||||||
api.clients.getAll.useQuery();
|
api.clients.getAll.useQuery();
|
||||||
const { data: noteTemplates } = api.invoiceTemplates.getByType.useQuery({ type: "notes" });
|
const { data: noteTemplates } = api.invoiceTemplates.getByType.useQuery({
|
||||||
|
type: "notes",
|
||||||
|
});
|
||||||
const { data: businesses, isLoading: loadingBusinesses } =
|
const { data: businesses, isLoading: loadingBusinesses } =
|
||||||
api.businesses.getAll.useQuery();
|
api.businesses.getAll.useQuery();
|
||||||
const { data: existingInvoice, isLoading: loadingInvoice } =
|
const { data: existingInvoice, isLoading: loadingInvoice } =
|
||||||
@@ -231,32 +241,6 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
const moveItemUp = (idx: number) => {
|
|
||||||
if (idx === 0) return;
|
|
||||||
setFormData((prev) => {
|
|
||||||
const newItems = [...prev.items];
|
|
||||||
if (newItems[idx] && newItems[idx - 1]) {
|
|
||||||
const temp = newItems[idx - 1]!;
|
|
||||||
newItems[idx - 1] = newItems[idx];
|
|
||||||
newItems[idx] = temp;
|
|
||||||
}
|
|
||||||
return { ...prev, items: newItems };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const moveItemDown = (idx: number) => {
|
|
||||||
if (idx === formData.items.length - 1) return;
|
|
||||||
setFormData((prev) => {
|
|
||||||
const newItems = [...prev.items];
|
|
||||||
if (newItems[idx] && newItems[idx + 1]) {
|
|
||||||
const temp = newItems[idx + 1]!;
|
|
||||||
newItems[idx + 1] = newItems[idx];
|
|
||||||
newItems[idx] = temp;
|
|
||||||
}
|
|
||||||
return { ...prev, items: newItems };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const reorderItems = (newItems: InvoiceItem[]) =>
|
|
||||||
setFormData((prev) => ({ ...prev, items: newItems }));
|
|
||||||
|
|
||||||
const createInvoice = api.invoices.create.useMutation({
|
const createInvoice = api.invoices.create.useMutation({
|
||||||
onSuccess: (inv) => {
|
onSuccess: (inv) => {
|
||||||
@@ -453,13 +437,24 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
? selectedClient.defaultHourlyRate
|
? selectedClient.defaultHourlyRate
|
||||||
: null;
|
: null;
|
||||||
const businessRate =
|
const businessRate =
|
||||||
currentBusiness && "defaultHourlyRate" in currentBusiness
|
currentBusiness &&
|
||||||
|
"defaultHourlyRate" in currentBusiness
|
||||||
? currentBusiness.defaultHourlyRate
|
? currentBusiness.defaultHourlyRate
|
||||||
: null;
|
: null;
|
||||||
updateField("defaultHourlyRate", (clientRate ?? businessRate ?? 0) as number);
|
updateField(
|
||||||
|
"defaultHourlyRate",
|
||||||
|
(clientRate ?? businessRate ?? 0) as number,
|
||||||
|
);
|
||||||
// Auto-fill currency from client
|
// Auto-fill currency from client
|
||||||
if (selectedClient && "currency" in selectedClient && selectedClient.currency) {
|
if (
|
||||||
updateField("currency", selectedClient.currency as string);
|
selectedClient &&
|
||||||
|
"currency" in selectedClient &&
|
||||||
|
selectedClient.currency
|
||||||
|
) {
|
||||||
|
updateField(
|
||||||
|
"currency",
|
||||||
|
selectedClient.currency as string,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -599,7 +594,11 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
{noteTemplates && noteTemplates.length > 0 && (
|
{noteTemplates && noteTemplates.length > 0 && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 gap-1 text-xs"
|
||||||
|
>
|
||||||
Use template <ChevronDown className="h-3 w-3" />
|
Use template <ChevronDown className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -674,9 +673,6 @@ export default function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
onAddItem={addItem}
|
onAddItem={addItem}
|
||||||
onRemoveItem={removeItem}
|
onRemoveItem={removeItem}
|
||||||
onUpdateItem={updateItem}
|
onUpdateItem={updateItem}
|
||||||
onMoveUp={moveItemUp}
|
|
||||||
onMoveDown={moveItemDown}
|
|
||||||
onReorderItems={reorderItems}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
ChevronDown,
|
|
||||||
ChevronUp,
|
|
||||||
Plus,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
@@ -33,9 +28,6 @@ interface InvoiceLineItemsProps {
|
|||||||
field: string,
|
field: string,
|
||||||
value: string | number | Date,
|
value: string | number | Date,
|
||||||
) => void;
|
) => void;
|
||||||
onMoveUp: (index: number) => void;
|
|
||||||
onMoveDown: (index: number) => void;
|
|
||||||
onReorderItems: (items: InvoiceItem[]) => void;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,61 +41,18 @@ interface LineItemRowProps {
|
|||||||
field: string,
|
field: string,
|
||||||
value: string | number | Date,
|
value: string | number | Date,
|
||||||
) => void;
|
) => void;
|
||||||
onMoveUp: (index: number) => void;
|
|
||||||
onMoveDown: (index: number) => void;
|
|
||||||
isFirst: boolean;
|
|
||||||
isLast: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const LineItemCard = React.forwardRef<HTMLDivElement, LineItemRowProps>(
|
const LineItemCard = React.forwardRef<HTMLDivElement, LineItemRowProps>(
|
||||||
(
|
({ item, index, canRemove, onRemove, onUpdate }, ref) => {
|
||||||
{
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
canRemove,
|
|
||||||
onRemove,
|
|
||||||
onUpdate,
|
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
isFirst,
|
|
||||||
isLast,
|
|
||||||
},
|
|
||||||
ref,
|
|
||||||
) => {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-card border hidden rounded-xl p-4 md:block transition-all shadow-sm group hover:border-primary/20",
|
"bg-card group hover:border-primary/20 hidden rounded-xl border p-4 shadow-sm transition-all md:block",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Arrow Controls */}
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onMoveUp(index)}
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
disabled={isFirst}
|
|
||||||
aria-label="Move up"
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onMoveDown(index)}
|
|
||||||
className="h-6 w-6 p-0"
|
|
||||||
disabled={isLast}
|
|
||||||
aria-label="Move down"
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 space-y-3">
|
<div className="flex-1 space-y-3">
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
@@ -136,7 +85,7 @@ const LineItemCard = React.forwardRef<HTMLDivElement, LineItemRowProps>(
|
|||||||
min={0}
|
min={0}
|
||||||
step={0.25}
|
step={0.25}
|
||||||
width="auto"
|
width="auto"
|
||||||
className="h-9 flex-1 min-w-[100px] font-mono"
|
className="h-9 min-w-[100px] flex-1 font-mono"
|
||||||
suffix="h"
|
suffix="h"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -148,7 +97,7 @@ const LineItemCard = React.forwardRef<HTMLDivElement, LineItemRowProps>(
|
|||||||
step={1}
|
step={1}
|
||||||
prefix="$"
|
prefix="$"
|
||||||
width="auto"
|
width="auto"
|
||||||
className="h-9 flex-1 min-w-[100px] font-mono"
|
className="h-9 min-w-[100px] flex-1 font-mono"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Amount */}
|
{/* Amount */}
|
||||||
@@ -185,10 +134,6 @@ function MobileLineItem({
|
|||||||
canRemove,
|
canRemove,
|
||||||
onRemove,
|
onRemove,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
isFirst,
|
|
||||||
isLast,
|
|
||||||
}: LineItemRowProps) {
|
}: LineItemRowProps) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -253,28 +198,6 @@ function MobileLineItem({
|
|||||||
{/* Bottom section with controls, item name, and total */}
|
{/* Bottom section with controls, item name, and total */}
|
||||||
<div className="border-border bg-muted/50 flex items-center justify-between border-t px-4 py-2">
|
<div className="border-border bg-muted/50 flex items-center justify-between border-t px-4 py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onMoveUp(index)}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
disabled={isFirst}
|
|
||||||
aria-label="Move up"
|
|
||||||
>
|
|
||||||
<ChevronUp className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onMoveDown(index)}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
disabled={isLast}
|
|
||||||
aria-label="Move down"
|
|
||||||
>
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -310,8 +233,6 @@ export function InvoiceLineItems({
|
|||||||
onAddItem,
|
onAddItem,
|
||||||
onRemoveItem,
|
onRemoveItem,
|
||||||
onUpdateItem,
|
onUpdateItem,
|
||||||
onMoveUp,
|
|
||||||
onMoveDown,
|
|
||||||
className,
|
className,
|
||||||
}: InvoiceLineItemsProps) {
|
}: InvoiceLineItemsProps) {
|
||||||
const canRemoveItems = items.length > 1;
|
const canRemoveItems = items.length > 1;
|
||||||
@@ -337,10 +258,6 @@ export function InvoiceLineItems({
|
|||||||
canRemove={canRemoveItems}
|
canRemove={canRemoveItems}
|
||||||
onRemove={onRemoveItem}
|
onRemove={onRemoveItem}
|
||||||
onUpdate={onUpdateItem}
|
onUpdate={onUpdateItem}
|
||||||
onMoveUp={onMoveUp}
|
|
||||||
onMoveDown={onMoveDown}
|
|
||||||
isFirst={index === 0}
|
|
||||||
isLast={index === items.length - 1}
|
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -351,10 +268,6 @@ export function InvoiceLineItems({
|
|||||||
canRemove={canRemoveItems}
|
canRemove={canRemoveItems}
|
||||||
onRemove={onRemoveItem}
|
onRemove={onRemoveItem}
|
||||||
onUpdate={onUpdateItem}
|
onUpdate={onUpdateItem}
|
||||||
onMoveUp={onMoveUp}
|
|
||||||
onMoveDown={onMoveDown}
|
|
||||||
isFirst={index === 0}
|
|
||||||
isLast={index === items.length - 1}
|
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
@@ -368,7 +281,7 @@ export function InvoiceLineItems({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={onAddItem}
|
onClick={onAddItem}
|
||||||
className="w-full border-dashed border-border py-8 text-muted-foreground hover:text-primary hover:bg-accent/50 hover:border-primary/50 transition-all"
|
className="border-border text-muted-foreground hover:text-primary hover:bg-accent/50 hover:border-primary/50 w-full border-dashed py-8 transition-all"
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add Line Item
|
Add Line Item
|
||||||
|
|||||||
@@ -9,98 +9,93 @@ import { InvoiceCalendarView } from "../invoice-calendar-view";
|
|||||||
import type { InvoiceFormData } from "./types";
|
import type { InvoiceFormData } from "./types";
|
||||||
|
|
||||||
interface InvoiceWorkspaceProps {
|
interface InvoiceWorkspaceProps {
|
||||||
formData: InvoiceFormData;
|
formData: InvoiceFormData;
|
||||||
viewMode: "list" | "calendar";
|
viewMode: "list" | "calendar";
|
||||||
setViewMode: (mode: "list" | "calendar") => void;
|
setViewMode: (mode: "list" | "calendar") => void;
|
||||||
addItem: (date?: Date) => void;
|
addItem: (date?: Date) => void;
|
||||||
removeItem: (index: number) => void;
|
removeItem: (index: number) => void;
|
||||||
updateItem: (index: number, field: string, value: string | number | Date) => void;
|
updateItem: (
|
||||||
moveItemUp: (index: number) => void;
|
index: number,
|
||||||
moveItemDown: (index: number) => void;
|
field: string,
|
||||||
reorderItems: (items: InvoiceFormData['items']) => void;
|
value: string | number | Date,
|
||||||
className?: string;
|
) => void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InvoiceWorkspace({
|
export function InvoiceWorkspace({
|
||||||
formData,
|
formData,
|
||||||
viewMode,
|
viewMode,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
addItem,
|
addItem,
|
||||||
removeItem,
|
removeItem,
|
||||||
updateItem,
|
updateItem,
|
||||||
moveItemUp,
|
className,
|
||||||
moveItemDown,
|
|
||||||
reorderItems,
|
|
||||||
className,
|
|
||||||
}: InvoiceWorkspaceProps) {
|
}: InvoiceWorkspaceProps) {
|
||||||
|
return (
|
||||||
return (
|
<div className={cn("flex h-full flex-col", className)}>
|
||||||
<div className={cn("flex flex-col h-full", className)}>
|
{/* Workspace Header / View Toggle */}
|
||||||
{/* Workspace Header / View Toggle */}
|
<div className="bg-background/50 sticky top-0 z-10 flex items-center justify-between border-b p-4 backdrop-blur-sm">
|
||||||
<div className="flex items-center justify-between p-4 border-b bg-background/50 backdrop-blur-sm sticky top-0 z-10">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<h2 className="text-lg font-semibold tracking-tight">
|
||||||
<h2 className="text-lg font-semibold tracking-tight">
|
{viewMode === "list" ? "Line Items" : "Timesheet"}
|
||||||
{viewMode === 'list' ? 'Line Items' : 'Timesheet'}
|
</h2>
|
||||||
</h2>
|
<div className="text-muted-foreground ml-2 text-sm">
|
||||||
<div className="text-sm text-muted-foreground ml-2">
|
{formData.items.length}{" "}
|
||||||
{formData.items.length} {formData.items.length === 1 ? 'entry' : 'entries'}
|
{formData.items.length === 1 ? "entry" : "entries"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center bg-secondary/50 p-1 rounded-lg">
|
|
||||||
<Button
|
|
||||||
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setViewMode('list')}
|
|
||||||
className="h-8 gap-2 text-xs"
|
|
||||||
>
|
|
||||||
<List className="w-3.5 h-3.5" />
|
|
||||||
List
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={viewMode === 'calendar' ? 'secondary' : 'ghost'}
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setViewMode('calendar')}
|
|
||||||
className="h-8 gap-2 text-xs"
|
|
||||||
>
|
|
||||||
<CalendarIcon className="w-3.5 h-3.5" />
|
|
||||||
Calendar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Workspace Content */}
|
|
||||||
<div className="flex-1 overflow-hidden relative">
|
|
||||||
<div className="absolute inset-0 overflow-y-auto p-6 md:p-8">
|
|
||||||
{viewMode === 'list' ? (
|
|
||||||
<div className="max-w-4xl mx-auto space-y-6">
|
|
||||||
<div className="bg-background/40 backdrop-blur-md rounded-xl border border-white/10 p-1">
|
|
||||||
<InvoiceLineItems
|
|
||||||
items={formData.items}
|
|
||||||
onAddItem={() => addItem()}
|
|
||||||
onRemoveItem={removeItem}
|
|
||||||
onUpdateItem={updateItem}
|
|
||||||
onMoveUp={moveItemUp}
|
|
||||||
onMoveDown={moveItemDown}
|
|
||||||
onReorderItems={reorderItems}
|
|
||||||
className="p-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-full">
|
|
||||||
<InvoiceCalendarView
|
|
||||||
items={formData.items}
|
|
||||||
onAddItem={addItem}
|
|
||||||
onRemoveItem={removeItem}
|
|
||||||
onUpdateItem={updateItem}
|
|
||||||
defaultHourlyRate={formData.defaultHourlyRate}
|
|
||||||
className="h-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
|
<div className="bg-secondary/50 flex items-center rounded-lg p-1">
|
||||||
|
<Button
|
||||||
|
variant={viewMode === "list" ? "secondary" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setViewMode("list")}
|
||||||
|
className="h-8 gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<List className="h-3.5 w-3.5" />
|
||||||
|
List
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={viewMode === "calendar" ? "secondary" : "ghost"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setViewMode("calendar")}
|
||||||
|
className="h-8 gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="h-3.5 w-3.5" />
|
||||||
|
Calendar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Workspace Content */}
|
||||||
|
<div className="relative flex-1 overflow-hidden">
|
||||||
|
<div className="absolute inset-0 overflow-y-auto p-6 md:p-8">
|
||||||
|
{viewMode === "list" ? (
|
||||||
|
<div className="mx-auto max-w-4xl space-y-6">
|
||||||
|
<div className="bg-background/40 rounded-xl border border-white/10 p-1 backdrop-blur-md">
|
||||||
|
<InvoiceLineItems
|
||||||
|
items={formData.items}
|
||||||
|
onAddItem={() => addItem()}
|
||||||
|
onRemoveItem={removeItem}
|
||||||
|
onUpdateItem={updateItem}
|
||||||
|
className="p-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full">
|
||||||
|
<InvoiceCalendarView
|
||||||
|
items={formData.items}
|
||||||
|
onAddItem={addItem}
|
||||||
|
onRemoveItem={removeItem}
|
||||||
|
onUpdateItem={updateItem}
|
||||||
|
defaultHourlyRate={formData.defaultHourlyRate}
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user