mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 17:44:44 -05:00
Fix escaped quotes in CSV sample and data loading
This commit is contained in:
@@ -238,9 +238,10 @@ function FormatInstructions() {
|
|||||||
<h4 className="text-sm font-semibold">Sample Row:</h4>
|
<h4 className="text-sm font-semibold">Sample Row:</h4>
|
||||||
<div className="bg-muted-subtle rounded-lg p-3">
|
<div className="bg-muted-subtle rounded-lg p-3">
|
||||||
<p className="text-muted font-mono text-xs break-all">
|
<p className="text-muted font-mono text-xs break-all">
|
||||||
"Acme
|
"Acme
|
||||||
Corp","john@acme.com","INV-001","2024-01-15","2024-02-14","Web
|
Corp","john@acme.com","INV-001","2024-01-15","2024-02-14","Web
|
||||||
development work","40","75.00","8.5"
|
development
|
||||||
|
work","40","75.00","8.5"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,7 +277,7 @@ function ImportantNotes() {
|
|||||||
<ul className="text-muted-foreground space-y-1 text-sm">
|
<ul className="text-muted-foreground space-y-1 text-sm">
|
||||||
<li>• New clients will be created automatically</li>
|
<li>• New clients will be created automatically</li>
|
||||||
<li>• Existing clients will be matched by email</li>
|
<li>• Existing clients will be matched by email</li>
|
||||||
<li>• Invoices will be created in "draft" status</li>
|
<li>• Invoices will be created in "draft" status</li>
|
||||||
<li>• You can review and edit before sending</li>
|
<li>• You can review and edit before sending</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -429,7 +430,7 @@ export default async function ImportPage() {
|
|||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
{[...Array(4)].map((_, i) => (
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
<Card key={i} className="card-primary">
|
<Card key={i} className="card-primary">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="animate-pulse">
|
<div className="animate-pulse">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
@@ -227,9 +227,6 @@ export default function NewInvoicePage() {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Floating action bar ref
|
|
||||||
const footerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const { data: clients, isLoading: clientsLoading } =
|
const { data: clients, isLoading: clientsLoading } =
|
||||||
api.clients.getAll.useQuery();
|
api.clients.getAll.useQuery();
|
||||||
@@ -386,7 +383,7 @@ export default function NewInvoicePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pb-32">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Create Invoice"
|
title="Create Invoice"
|
||||||
description="Fill out the details below to create a new invoice"
|
description="Fill out the details below to create a new invoice"
|
||||||
@@ -657,52 +654,27 @@ export default function NewInvoicePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div
|
|
||||||
ref={footerRef}
|
|
||||||
className="flex flex-col gap-3 border-t pt-6 md:flex-row md:justify-between"
|
|
||||||
>
|
|
||||||
<Link href="/dashboard/invoices">
|
|
||||||
<Button variant="outline" className="w-full md:w-auto">
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 md:flex-row md:items-center">
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveDraft}
|
|
||||||
disabled={isLoading || !isFormValid()}
|
|
||||||
variant="outline"
|
|
||||||
className="w-full md:w-auto"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Save Draft
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateInvoice}
|
|
||||||
disabled={isLoading || !isFormValid()}
|
|
||||||
className="btn-brand-primary w-full md:w-auto"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Send className="mr-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Create Invoice
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FloatingActionBar triggerRef={footerRef} title="Creating a new invoice">
|
<FloatingActionBar
|
||||||
|
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">
|
||||||
|
Creating a new invoice
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Complete the form to create your invoice
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Link href="/dashboard/invoices">
|
<Link href="/dashboard/invoices">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -10,13 +10,18 @@ interface AddressAutocompleteProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NominatimResult {
|
||||||
|
place_id: string;
|
||||||
|
display_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function AddressAutocomplete({
|
export function AddressAutocomplete({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
onSelect,
|
onSelect,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: AddressAutocompleteProps) {
|
}: AddressAutocompleteProps) {
|
||||||
const [suggestions, setSuggestions] = useState<any[]>([]);
|
const [suggestions, setSuggestions] = useState<NominatimResult[]>([]);
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
@@ -28,7 +33,7 @@ export function AddressAutocomplete({
|
|||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`,
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`,
|
||||||
);
|
);
|
||||||
const data = await res.json();
|
const data = (await res.json()) as NominatimResult[];
|
||||||
setSuggestions(data);
|
setSuggestions(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,7 +42,9 @@ export function AddressAutocomplete({
|
|||||||
onChange(val);
|
onChange(val);
|
||||||
setShowSuggestions(true);
|
setShowSuggestions(true);
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
timeoutRef.current = setTimeout(() => fetchSuggestions(val), 300);
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
void fetchSuggestions(val);
|
||||||
|
}, 300);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (address: string) => {
|
const handleSelect = (address: string) => {
|
||||||
@@ -51,7 +58,7 @@ export function AddressAutocomplete({
|
|||||||
<Input
|
<Input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
placeholder={placeholder || "Start typing address..."}
|
placeholder={placeholder ?? "Start typing address..."}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onFocus={() => value && setShowSuggestions(true)}
|
onFocus={() => value && setShowSuggestions(true)}
|
||||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||||
@@ -59,7 +66,7 @@ export function AddressAutocomplete({
|
|||||||
{showSuggestions && suggestions.length > 0 && (
|
{showSuggestions && suggestions.length > 0 && (
|
||||||
<Card className="card-primary absolute z-10 mt-1 max-h-60 w-full overflow-auto">
|
<Card className="card-primary absolute z-10 mt-1 max-h-60 w-full overflow-auto">
|
||||||
<ul>
|
<ul>
|
||||||
{suggestions.map((s, i) => (
|
{suggestions.map((s) => (
|
||||||
<li
|
<li
|
||||||
key={s.place_id}
|
key={s.place_id}
|
||||||
className="hover:bg-muted cursor-pointer px-4 py-2 text-sm"
|
className="hover:bg-muted cursor-pointer px-4 py-2 text-sm"
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function ClientList() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{[...Array(3)].map((_, i: number) => (
|
{Array.from({ length: 3 }, (_, i: number) => (
|
||||||
<Card key={i} className="card-primary">
|
<Card key={i} className="card-primary">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="h-4 animate-pulse rounded bg-gray-200" />
|
<div className="h-4 animate-pulse rounded bg-gray-200" />
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { toast } from "sonner";
|
|||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
Calendar,
|
Calendar,
|
||||||
DollarSign,
|
|
||||||
Edit,
|
Edit,
|
||||||
Trash2,
|
Trash2,
|
||||||
Eye,
|
Eye,
|
||||||
@@ -43,12 +42,12 @@ export function InvoiceList() {
|
|||||||
const deleteInvoice = api.invoices.delete.useMutation({
|
const deleteInvoice = api.invoices.delete.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Invoice deleted successfully");
|
toast.success("Invoice deleted successfully");
|
||||||
refetch();
|
void refetch();
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
setInvoiceToDelete(null);
|
setInvoiceToDelete(null);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message || "Failed to delete invoice");
|
toast.error(error.message ?? "Failed to delete invoice");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ export function InvoiceList() {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(searchTerm.toLowerCase()) ||
|
.includes(searchTerm.toLowerCase()) ||
|
||||||
invoice.client.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
invoice.client.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
) || [];
|
) ?? [];
|
||||||
|
|
||||||
const handleDelete = (invoiceId: string) => {
|
const handleDelete = (invoiceId: string) => {
|
||||||
setInvoiceToDelete(invoiceId);
|
setInvoiceToDelete(invoiceId);
|
||||||
@@ -86,7 +85,7 @@ export function InvoiceList() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{[...Array(3)].map((_, i) => (
|
{Array.from({ length: 3 }, (_, i) => (
|
||||||
<Card key={i}>
|
<Card key={i}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="bg-muted h-4 animate-pulse rounded" />
|
<div className="bg-muted h-4 animate-pulse rounded" />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Badge } from "~/components/ui/badge";
|
|
||||||
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
|
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
import {
|
import {
|
||||||
@@ -19,15 +19,12 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
Calendar,
|
|
||||||
FileText,
|
FileText,
|
||||||
User,
|
User,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Trash2,
|
Trash2,
|
||||||
Edit,
|
|
||||||
Download,
|
Download,
|
||||||
Send,
|
Send,
|
||||||
ArrowLeft,
|
|
||||||
Clock,
|
Clock,
|
||||||
MapPin,
|
MapPin,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -36,7 +33,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { generateInvoicePDF } from "~/lib/pdf-export";
|
import { generateInvoicePDF } from "~/lib/pdf-export";
|
||||||
import { InvoiceViewSkeleton } from "~/components/ui/skeleton";
|
import { Skeleton } from "~/components/ui/skeleton";
|
||||||
|
|
||||||
interface InvoiceViewProps {
|
interface InvoiceViewProps {
|
||||||
invoiceId: string;
|
invoiceId: string;
|
||||||
@@ -130,7 +127,23 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
|||||||
invoice.status !== "paid";
|
invoice.status !== "paid";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <InvoiceViewSkeleton />;
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
<Skeleton className="h-4 w-24" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Skeleton className="h-4 w-full" />
|
||||||
|
<Skeleton className="h-4 w-3/4" />
|
||||||
|
<Skeleton className="h-4 w-1/2" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invoice) {
|
if (!invoice) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function StatusBadge({
|
|||||||
...props
|
...props
|
||||||
}: StatusBadgeProps) {
|
}: StatusBadgeProps) {
|
||||||
const statusClass = statusClassMap[status];
|
const statusClass = statusClassMap[status];
|
||||||
const label = children || statusLabelMap[status];
|
const label = children ?? statusLabelMap[status];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge className={cn(statusClass, className)} {...props}>
|
<Badge className={cn(statusClass, className)} {...props}>
|
||||||
|
|||||||
@@ -11,16 +11,17 @@ import {
|
|||||||
Star,
|
Star,
|
||||||
Loader2,
|
Loader2,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Label } from "~/components/ui/label";
|
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 { Switch } from "~/components/ui/switch";
|
||||||
import { AddressForm } from "~/components/forms/address-form";
|
import { AddressForm } from "~/components/forms/address-form";
|
||||||
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
||||||
@@ -90,7 +91,6 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
const [errors, setErrors] = useState<FormErrors>({});
|
const [errors, setErrors] = useState<FormErrors>({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const footerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Fetch business data if editing
|
// Fetch business data if editing
|
||||||
const { data: business, isLoading: isLoadingBusiness } =
|
const { data: business, isLoading: isLoadingBusiness } =
|
||||||
@@ -246,11 +246,35 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (mode === "edit" && isLoadingBusiness) {
|
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 (
|
return (
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl pb-32">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* Main Form Container - styled like data table */}
|
{/* Main Form Container - styled like data table */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -460,55 +484,27 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
|
|
||||||
<FloatingActionBar
|
<FloatingActionBar
|
||||||
triggerRef={footerRef}
|
leftContent={
|
||||||
title={
|
<div className="flex items-center space-x-3">
|
||||||
mode === "create"
|
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30">
|
||||||
? "Creating a new business"
|
<FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
: "Editing business details"
|
</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
|
<Button
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
"use client";
|
"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 { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Label } from "~/components/ui/label";
|
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 { AddressForm } from "~/components/forms/address-form";
|
||||||
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
import { FloatingActionBar } from "~/components/layout/floating-action-bar";
|
||||||
import { NumberInput } from "~/components/ui/number-input";
|
import { NumberInput } from "~/components/ui/number-input";
|
||||||
@@ -70,7 +77,6 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
const [errors, setErrors] = useState<FormErrors>({});
|
const [errors, setErrors] = useState<FormErrors>({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const footerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Fetch client data if editing
|
// Fetch client data if editing
|
||||||
const { data: client, isLoading: isLoadingClient } =
|
const { data: client, isLoading: isLoadingClient } =
|
||||||
@@ -212,11 +218,35 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (mode === "edit" && isLoadingClient) {
|
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 (
|
return (
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl pb-32">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* Main Form Container - styled like data table */}
|
{/* Main Form Container - styled like data table */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -390,53 +420,27 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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>
|
</form>
|
||||||
|
|
||||||
<FloatingActionBar
|
<FloatingActionBar
|
||||||
triggerRef={footerRef}
|
leftContent={
|
||||||
title={
|
<div className="flex items-center space-x-3">
|
||||||
mode === "create" ? "Creating a new client" : "Editing client details"
|
<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
|
<Button
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState, useRef } from "react";
|
import { useState } from "react";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
@@ -52,10 +52,10 @@ function InvoiceFormSkeleton() {
|
|||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
{/* Left Column - Content with Tabs */}
|
{/* Left Column - Content with Tabs */}
|
||||||
<div className="space-y-6 lg:col-span-2">
|
<div className="space-y-6 lg:col-span-2">
|
||||||
{/* Tabs - Mobile stacked, desktop side-by-side */}
|
{/* Tabs - Match actual TabsList structure */}
|
||||||
<div className="bg-muted grid w-full grid-cols-1 gap-1 rounded-lg p-1 sm:grid-cols-2">
|
<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-9 rounded-md shadow-sm sm:h-10"></div>
|
<div className="bg-background h-[calc(100%-1px)] flex-1 rounded-md shadow-sm"></div>
|
||||||
<div className="bg-muted/30 h-9 rounded-md sm:h-10"></div>
|
<div className="bg-muted/30 h-[calc(100%-1px)] flex-1 rounded-md"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Invoice Details Card */}
|
{/* Invoice Details Card */}
|
||||||
@@ -233,7 +233,7 @@ function InvoiceFormSkeleton() {
|
|||||||
|
|
||||||
export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const footerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`,
|
invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`,
|
||||||
businessId: "",
|
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
|
// tRPC mutations
|
||||||
const createInvoice = api.invoices.create.useMutation({
|
const createInvoice = api.invoices.create.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -470,14 +478,33 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 pb-32">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={invoiceId ? "Edit Invoice" : "Create Invoice"}
|
title={invoiceId ? "Edit Invoice" : "Create Invoice"}
|
||||||
description={
|
description={
|
||||||
invoiceId ? "Update invoice details" : "Create a new invoice"
|
invoiceId ? "Update invoice details" : "Create a new invoice"
|
||||||
}
|
}
|
||||||
variant="gradient"
|
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 Content */}
|
||||||
<form id="invoice-form" onSubmit={handleSubmit} className="space-y-6">
|
<form id="invoice-form" onSubmit={handleSubmit} className="space-y-6">
|
||||||
@@ -698,6 +725,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
onUpdateItem={updateItem}
|
onUpdateItem={updateItem}
|
||||||
onMoveUp={moveItemUp}
|
onMoveUp={moveItemUp}
|
||||||
onMoveDown={moveItemDown}
|
onMoveDown={moveItemDown}
|
||||||
|
onReorderItems={reorderItems}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -768,63 +796,21 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Floating Action Bar */}
|
{/* Floating Action Bar */}
|
||||||
<FloatingActionBar
|
<FloatingActionBar
|
||||||
triggerRef={footerRef}
|
|
||||||
leftContent={
|
leftContent={
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
|
<div className="rounded-lg bg-green-100 p-2 dark:bg-green-900/30">
|
||||||
<FileText className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
<FileText className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">
|
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{invoiceId ? "Edit Invoice" : "Create Invoice"}
|
{invoiceId ? "Edit Invoice" : "Create Invoice"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{invoiceId ? "Update invoice details" : "Create a new invoice"}
|
Update invoice details
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,23 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "~/lib/utils";
|
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 {
|
interface InvoiceItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,6 +53,7 @@ interface InvoiceLineItemsProps {
|
|||||||
) => void;
|
) => void;
|
||||||
onMoveUp: (index: number) => void;
|
onMoveUp: (index: number) => void;
|
||||||
onMoveDown: (index: number) => void;
|
onMoveDown: (index: number) => void;
|
||||||
|
onReorderItems: (items: InvoiceItem[]) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,19 +73,100 @@ interface LineItemRowProps {
|
|||||||
isLast: boolean;
|
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,
|
item,
|
||||||
index,
|
index,
|
||||||
canRemove,
|
canRemove,
|
||||||
onRemove,
|
onRemove,
|
||||||
onUpdate,
|
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 (
|
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">
|
<div className="flex items-start gap-3">
|
||||||
{/* Drag Handle */}
|
{/* Drag Handle and Arrow Controls */}
|
||||||
<div className="mt-1 flex items-center justify-center">
|
<div className="mt-1 flex flex-col items-center gap-1">
|
||||||
<GripVertical className="text-muted-foreground h-4 w-4 cursor-grab" />
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
@@ -280,44 +379,75 @@ export function InvoiceLineItems({
|
|||||||
onUpdateItem,
|
onUpdateItem,
|
||||||
onMoveUp,
|
onMoveUp,
|
||||||
onMoveDown,
|
onMoveDown,
|
||||||
|
onReorderItems,
|
||||||
className,
|
className,
|
||||||
}: InvoiceLineItemsProps) {
|
}: InvoiceLineItemsProps) {
|
||||||
const canRemoveItems = items.length > 1;
|
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 (
|
return (
|
||||||
<div className={cn("space-y-2", className)}>
|
<div className={cn("space-y-2", className)}>
|
||||||
{/* Desktop and Mobile Cards */}
|
{/* Desktop Cards with Drag and Drop */}
|
||||||
<div className="space-y-2">
|
<DndContext
|
||||||
{items.map((item, index) => (
|
sensors={sensors}
|
||||||
<React.Fragment key={item.id}>
|
collisionDetection={closestCenter}
|
||||||
{/* Desktop/Tablet Card */}
|
onDragEnd={handleDragEnd}
|
||||||
<LineItemRow
|
>
|
||||||
item={item}
|
<SortableContext
|
||||||
index={index}
|
items={items.map((item) => item.id)}
|
||||||
canRemove={canRemoveItems}
|
strategy={verticalListSortingStrategy}
|
||||||
onRemove={onRemoveItem}
|
>
|
||||||
onUpdate={onUpdateItem}
|
<div className="space-y-2">
|
||||||
onMoveUp={onMoveUp}
|
{items.map((item, index) => (
|
||||||
onMoveDown={onMoveDown}
|
<React.Fragment key={item.id}>
|
||||||
isFirst={index === 0}
|
{/* Desktop/Tablet Card with Drag and Drop */}
|
||||||
isLast={index === items.length - 1}
|
<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 */}
|
{/* Mobile Card */}
|
||||||
<MobileLineItem
|
<MobileLineItem
|
||||||
item={item}
|
item={item}
|
||||||
index={index}
|
index={index}
|
||||||
canRemove={canRemoveItems}
|
canRemove={canRemoveItems}
|
||||||
onRemove={onRemoveItem}
|
onRemove={onRemoveItem}
|
||||||
onUpdate={onUpdateItem}
|
onUpdate={onUpdateItem}
|
||||||
onMoveUp={onMoveUp}
|
onMoveUp={onMoveUp}
|
||||||
onMoveDown={onMoveDown}
|
onMoveDown={onMoveDown}
|
||||||
isFirst={index === 0}
|
isFirst={index === 0}
|
||||||
isLast={index === items.length - 1}
|
isLast={index === items.length - 1}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
|
||||||
{/* Add Item Button */}
|
{/* Add Item Button */}
|
||||||
<div className="px-3 pt-3">
|
<div className="px-3 pt-3">
|
||||||
|
|||||||
@@ -1,107 +1,84 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
|
||||||
interface FloatingActionBarProps {
|
interface FloatingActionBarProps {
|
||||||
/** Ref to the element that triggers visibility when scrolled out of view */
|
/** Content to display on the left side */
|
||||||
triggerRef: React.RefObject<HTMLElement | null>;
|
|
||||||
/** Title text displayed on the left (deprecated - use leftContent instead) */
|
|
||||||
title?: string;
|
|
||||||
/** Custom content to display on the left */
|
|
||||||
leftContent?: React.ReactNode;
|
leftContent?: React.ReactNode;
|
||||||
/** Action buttons to display on the right */
|
/** Action buttons to display on the right */
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
/** Additional className for styling */
|
/** Additional className for styling */
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Whether to show the floating bar (for manual control) */
|
|
||||||
show?: boolean;
|
|
||||||
/** Callback when visibility changes */
|
|
||||||
onVisibilityChange?: (visible: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FloatingActionBar({
|
export function FloatingActionBar({
|
||||||
triggerRef,
|
|
||||||
title,
|
|
||||||
leftContent,
|
leftContent,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
show,
|
|
||||||
onVisibilityChange,
|
|
||||||
}: FloatingActionBarProps) {
|
}: FloatingActionBarProps) {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isDocked, setIsDocked] = useState(false);
|
||||||
const floatingRef = useRef<HTMLDivElement>(null);
|
|
||||||
const previousVisibleRef = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If show prop is provided, use it instead of auto-detection
|
|
||||||
if (show !== undefined) {
|
|
||||||
setIsVisible(show);
|
|
||||||
onVisibilityChange?.(show);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!triggerRef.current) return;
|
// Check if we're truly at the bottom of the page
|
||||||
|
const scrollHeight = document.documentElement.scrollHeight;
|
||||||
|
const scrollTop = document.documentElement.scrollTop;
|
||||||
|
const clientHeight = document.documentElement.clientHeight;
|
||||||
|
const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
|
||||||
|
|
||||||
const rect = triggerRef.current.getBoundingClientRect();
|
// Only dock when we're within 50px of the actual bottom AND there's content to scroll
|
||||||
const isInView = rect.top < window.innerHeight && rect.bottom >= 0;
|
const hasScrollableContent = scrollHeight > clientHeight;
|
||||||
|
const shouldDock = hasScrollableContent && distanceFromBottom <= 50;
|
||||||
|
|
||||||
// Show floating bar when trigger element is out of view
|
// If content is too small, keep it at bottom of viewport
|
||||||
const shouldShow = !isInView;
|
const contentTooSmall = scrollHeight <= clientHeight + 200;
|
||||||
|
|
||||||
if (shouldShow !== previousVisibleRef.current) {
|
setIsDocked(shouldDock && !contentTooSmall);
|
||||||
previousVisibleRef.current = shouldShow;
|
|
||||||
setIsVisible(shouldShow);
|
|
||||||
onVisibilityChange?.(shouldShow);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use IntersectionObserver for better detection
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const entry = entries[0];
|
|
||||||
if (entry) {
|
|
||||||
const shouldShow = !entry.isIntersecting;
|
|
||||||
if (shouldShow !== previousVisibleRef.current) {
|
|
||||||
previousVisibleRef.current = shouldShow;
|
|
||||||
setIsVisible(shouldShow);
|
|
||||||
onVisibilityChange?.(shouldShow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Trigger when element is completely out of view
|
|
||||||
threshold: 0,
|
|
||||||
rootMargin: "0px 0px -100% 0px",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start observing when trigger element is available
|
|
||||||
if (triggerRef.current) {
|
|
||||||
observer.observe(triggerRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also add scroll listener as fallback
|
|
||||||
window.addEventListener("scroll", handleScroll, { passive: true });
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
handleScroll(); // Check initial state
|
||||||
|
|
||||||
// Check initial state
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
handleScroll();
|
}, []);
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
window.removeEventListener("scroll", handleScroll);
|
|
||||||
};
|
|
||||||
}, [triggerRef, show, onVisibilityChange]);
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={floatingRef} className={cn("floating-action-bar", className)}>
|
<div
|
||||||
<div className="floating-action-bar-content">
|
className={cn(
|
||||||
{leftContent || <p className="floating-action-bar-title">{title}</p>}
|
// Base positioning - always at bottom
|
||||||
|
"fixed right-0 left-0 z-50",
|
||||||
|
// Safe area and sidebar adjustments
|
||||||
|
"pb-safe-area-inset-bottom md:left-[276px]",
|
||||||
|
// Conditional centering based on dock state
|
||||||
|
isDocked ? "flex justify-center" : "",
|
||||||
|
// Dynamic bottom positioning
|
||||||
|
isDocked ? "bottom-4" : "bottom-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Content container - full width when floating, content width when docked */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full transition-all duration-300",
|
||||||
|
isDocked ? "mx-auto px-4 mb-0" : "px-4 mb-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Card className="card-primary">
|
||||||
|
<CardContent className="flex items-center justify-between p-4">
|
||||||
|
{/* Left content */}
|
||||||
|
{leftContent && (
|
||||||
|
<div className="flex flex-1 items-center gap-3">
|
||||||
|
{leftContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right actions */}
|
||||||
|
<div className="flex items-center gap-2 sm:gap-3">{children}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="floating-action-bar-actions">{children}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import {
|
|||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
|
import {
|
||||||
|
type DayButton,
|
||||||
|
DayPicker,
|
||||||
|
getDefaultClassNames,
|
||||||
|
} from "react-day-picker";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { Button, buttonVariants } from "~/components/ui/button";
|
import { Button, buttonVariants } from "~/components/ui/button";
|
||||||
@@ -24,7 +28,7 @@ function Calendar({
|
|||||||
showOutsideDays = true,
|
showOutsideDays = true,
|
||||||
captionLayout = "label",
|
captionLayout = "label",
|
||||||
buttonVariant = "ghost",
|
buttonVariant = "ghost",
|
||||||
formatters,
|
formatters: _formatters,
|
||||||
components,
|
components,
|
||||||
month,
|
month,
|
||||||
onMonthChange,
|
onMonthChange,
|
||||||
@@ -49,8 +53,8 @@ function Calendar({
|
|||||||
"Dec",
|
"Dec",
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentYear = month?.getFullYear() || new Date().getFullYear();
|
const currentYear = month?.getFullYear() ?? new Date().getFullYear();
|
||||||
const currentMonth = month?.getMonth() || new Date().getMonth();
|
const currentMonth = month?.getMonth() ?? new Date().getMonth();
|
||||||
|
|
||||||
const years = Array.from({ length: 11 }, (_, i) => currentYear - 5 + i);
|
const years = Array.from({ length: 11 }, (_, i) => currentYear - 5 + i);
|
||||||
|
|
||||||
@@ -173,9 +177,9 @@ function Calendar({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
DayButton: CalendarDayButton,
|
DayButton: CalendarDayButton,
|
||||||
MonthCaption: ({ calendarMonth }) => {
|
MonthCaption: ({ calendarMonth: _calendarMonth }) => {
|
||||||
if (captionLayout !== "dropdown") {
|
if (captionLayout !== "dropdown") {
|
||||||
return null;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -248,7 +252,7 @@ function CalendarDayButton({
|
|||||||
modifiers,
|
modifiers,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DayButton>) {
|
}: React.ComponentProps<typeof DayButton>) {
|
||||||
const defaultClassNames = getDefaultClassNames();
|
const _defaultClassNames = getDefaultClassNames();
|
||||||
|
|
||||||
const ref = React.useRef<HTMLButtonElement>(null);
|
const ref = React.useRef<HTMLButtonElement>(null);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const styles = StyleSheet.create({
|
|||||||
// Dense header (first page)
|
// Dense header (first page)
|
||||||
denseHeader: {
|
denseHeader: {
|
||||||
marginBottom: 30,
|
marginBottom: 30,
|
||||||
borderBottom: "2px solid #16a34a",
|
borderBottom: "2px solid #10b981",
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ const styles = StyleSheet.create({
|
|||||||
businessName: {
|
businessName: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ const styles = StyleSheet.create({
|
|||||||
invoiceTitle: {
|
invoiceTitle: {
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#16a34a",
|
color: "#10b981",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: "semibold",
|
fontWeight: "semibold",
|
||||||
fontFamily: "AzeretMono",
|
fontFamily: "AzeretMono",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -170,8 +170,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
|
|
||||||
statusPaid: {
|
statusPaid: {
|
||||||
backgroundColor: "#dcfce7",
|
backgroundColor: "#ecfdf5",
|
||||||
color: "#166534",
|
color: "#065f46",
|
||||||
},
|
},
|
||||||
|
|
||||||
statusUnpaid: {
|
statusUnpaid: {
|
||||||
@@ -194,14 +194,14 @@ const styles = StyleSheet.create({
|
|||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
},
|
},
|
||||||
|
|
||||||
clientName: {
|
clientName: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ const styles = StyleSheet.create({
|
|||||||
detailValue: {
|
detailValue: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: "AzeretMono",
|
fontFamily: "AzeretMono",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
fontWeight: "semibold",
|
fontWeight: "semibold",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
@@ -252,7 +252,7 @@ const styles = StyleSheet.create({
|
|||||||
notesTitle: {
|
notesTitle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ const styles = StyleSheet.create({
|
|||||||
abridgedBusinessName: {
|
abridgedBusinessName: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
},
|
},
|
||||||
|
|
||||||
abridgedInvoiceInfo: {
|
abridgedInvoiceInfo: {
|
||||||
@@ -294,14 +294,14 @@ const styles = StyleSheet.create({
|
|||||||
abridgedInvoiceTitle: {
|
abridgedInvoiceTitle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#16a34a",
|
color: "#10b981",
|
||||||
},
|
},
|
||||||
|
|
||||||
abridgedInvoiceNumber: {
|
abridgedInvoiceNumber: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: "semibold",
|
fontWeight: "semibold",
|
||||||
fontFamily: "AzeretMono",
|
fontFamily: "AzeretMono",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Table styles
|
// Table styles
|
||||||
@@ -313,7 +313,7 @@ const styles = StyleSheet.create({
|
|||||||
tableHeader: {
|
tableHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
backgroundColor: "#f3f4f6",
|
backgroundColor: "#f3f4f6",
|
||||||
borderBottom: "2px solid #16a34a",
|
borderBottom: "2px solid #10b981",
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
},
|
},
|
||||||
@@ -321,7 +321,7 @@ const styles = StyleSheet.create({
|
|||||||
tableHeaderCell: {
|
tableHeaderCell: {
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
tableCell: {
|
tableCell: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
paddingHorizontal: 4,
|
paddingHorizontal: 4,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
},
|
},
|
||||||
@@ -433,7 +433,7 @@ const styles = StyleSheet.create({
|
|||||||
totalAmount: {
|
totalAmount: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: "AzeretMono",
|
fontFamily: "AzeretMono",
|
||||||
color: "#1f2937",
|
color: "#111827",
|
||||||
fontWeight: "semibold",
|
fontWeight: "semibold",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -442,7 +442,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
borderTop: "2px solid #16a34a",
|
borderTop: "2px solid #10b981",
|
||||||
},
|
},
|
||||||
|
|
||||||
finalTotalLabel: {
|
finalTotalLabel: {
|
||||||
@@ -455,7 +455,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: "AzeretMono",
|
fontFamily: "AzeretMono",
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
color: "#16a34a",
|
color: "#10b981",
|
||||||
},
|
},
|
||||||
|
|
||||||
itemCount: {
|
itemCount: {
|
||||||
|
|||||||
@@ -67,8 +67,8 @@
|
|||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--destructive-foreground: oklch(0.985 0 0);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.82 0.02 150);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.82 0.02 150);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.708 0 0);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.82 0.02 150);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
|
||||||
/* Brand colors */
|
/* Brand colors */
|
||||||
@@ -1380,20 +1380,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Floating Action Bar Utility Classes */
|
/* Floating Action Bar Utility Classes */
|
||||||
.floating-action-bar {
|
.pb-safe-area-inset-bottom {
|
||||||
@apply border-border/40 bg-background/60 animate-in slide-in-from-bottom-4 sticky bottom-4 z-20 flex items-center justify-between rounded-2xl border p-4 shadow-lg backdrop-blur-xl backdrop-saturate-150 duration-300;
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
}
|
|
||||||
|
|
||||||
.floating-action-bar-content {
|
|
||||||
@apply flex-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-action-bar-title {
|
|
||||||
@apply text-muted-foreground text-sm;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-action-bar-actions {
|
|
||||||
@apply flex items-center gap-2 sm:gap-3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form Action Footer Utility Classes */
|
/* Form Action Footer Utility Classes */
|
||||||
|
|||||||
Reference in New Issue
Block a user