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>
|
||||
<div className="bg-muted-subtle rounded-lg p-3">
|
||||
<p className="text-muted font-mono text-xs break-all">
|
||||
"Acme
|
||||
Corp","john@acme.com","INV-001","2024-01-15","2024-02-14","Web
|
||||
development work","40","75.00","8.5"
|
||||
"Acme
|
||||
Corp","john@acme.com","INV-001","2024-01-15","2024-02-14","Web
|
||||
development
|
||||
work","40","75.00","8.5"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -276,7 +277,7 @@ function ImportantNotes() {
|
||||
<ul className="text-muted-foreground space-y-1 text-sm">
|
||||
<li>• New clients will be created automatically</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>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -429,7 +430,7 @@ export default async function ImportPage() {
|
||||
<Suspense
|
||||
fallback={
|
||||
<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">
|
||||
<CardContent className="p-6">
|
||||
<div className="animate-pulse">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { api } from "~/trpc/react";
|
||||
@@ -227,9 +227,6 @@ export default function NewInvoicePage() {
|
||||
],
|
||||
});
|
||||
|
||||
// Floating action bar ref
|
||||
const footerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Queries
|
||||
const { data: clients, isLoading: clientsLoading } =
|
||||
api.clients.getAll.useQuery();
|
||||
@@ -386,7 +383,7 @@ export default function NewInvoicePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 pb-32">
|
||||
<PageHeader
|
||||
title="Create Invoice"
|
||||
description="Fill out the details below to create a new invoice"
|
||||
@@ -657,52 +654,27 @@ export default function NewInvoicePage() {
|
||||
</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>
|
||||
</Card>
|
||||
</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">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -10,13 +10,18 @@ interface AddressAutocompleteProps {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface NominatimResult {
|
||||
place_id: string;
|
||||
display_name: string;
|
||||
}
|
||||
|
||||
export function AddressAutocomplete({
|
||||
value,
|
||||
onChange,
|
||||
onSelect,
|
||||
placeholder,
|
||||
}: AddressAutocompleteProps) {
|
||||
const [suggestions, setSuggestions] = useState<any[]>([]);
|
||||
const [suggestions, setSuggestions] = useState<NominatimResult[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
@@ -28,7 +33,7 @@ export function AddressAutocomplete({
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as NominatimResult[];
|
||||
setSuggestions(data);
|
||||
};
|
||||
|
||||
@@ -37,7 +42,9 @@ export function AddressAutocomplete({
|
||||
onChange(val);
|
||||
setShowSuggestions(true);
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => fetchSuggestions(val), 300);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
void fetchSuggestions(val);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleSelect = (address: string) => {
|
||||
@@ -51,7 +58,7 @@ export function AddressAutocomplete({
|
||||
<Input
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder || "Start typing address..."}
|
||||
placeholder={placeholder ?? "Start typing address..."}
|
||||
autoComplete="off"
|
||||
onFocus={() => value && setShowSuggestions(true)}
|
||||
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
|
||||
@@ -59,7 +66,7 @@ export function AddressAutocomplete({
|
||||
{showSuggestions && suggestions.length > 0 && (
|
||||
<Card className="card-primary absolute z-10 mt-1 max-h-60 w-full overflow-auto">
|
||||
<ul>
|
||||
{suggestions.map((s, i) => (
|
||||
{suggestions.map((s) => (
|
||||
<li
|
||||
key={s.place_id}
|
||||
className="hover:bg-muted cursor-pointer px-4 py-2 text-sm"
|
||||
|
||||
@@ -72,7 +72,7 @@ export function ClientList() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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">
|
||||
<CardHeader>
|
||||
<div className="h-4 animate-pulse rounded bg-gray-200" />
|
||||
|
||||
@@ -26,7 +26,6 @@ import { toast } from "sonner";
|
||||
import {
|
||||
FileText,
|
||||
Calendar,
|
||||
DollarSign,
|
||||
Edit,
|
||||
Trash2,
|
||||
Eye,
|
||||
@@ -43,12 +42,12 @@ export function InvoiceList() {
|
||||
const deleteInvoice = api.invoices.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Invoice deleted successfully");
|
||||
refetch();
|
||||
void refetch();
|
||||
setDeleteDialogOpen(false);
|
||||
setInvoiceToDelete(null);
|
||||
},
|
||||
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()
|
||||
.includes(searchTerm.toLowerCase()) ||
|
||||
invoice.client.name.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
) || [];
|
||||
) ?? [];
|
||||
|
||||
const handleDelete = (invoiceId: string) => {
|
||||
setInvoiceToDelete(invoiceId);
|
||||
@@ -86,7 +85,7 @@ export function InvoiceList() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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}>
|
||||
<CardHeader>
|
||||
<div className="bg-muted h-4 animate-pulse rounded" />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import { api } from "~/trpc/react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
|
||||
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
@@ -19,15 +19,12 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Calendar,
|
||||
FileText,
|
||||
User,
|
||||
DollarSign,
|
||||
Trash2,
|
||||
Edit,
|
||||
Download,
|
||||
Send,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
MapPin,
|
||||
Mail,
|
||||
@@ -36,7 +33,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { generateInvoicePDF } from "~/lib/pdf-export";
|
||||
import { InvoiceViewSkeleton } from "~/components/ui/skeleton";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
|
||||
interface InvoiceViewProps {
|
||||
invoiceId: string;
|
||||
@@ -130,7 +127,23 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
invoice.status !== "paid";
|
||||
|
||||
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) {
|
||||
|
||||
@@ -47,7 +47,7 @@ export function StatusBadge({
|
||||
...props
|
||||
}: StatusBadgeProps) {
|
||||
const statusClass = statusClassMap[status];
|
||||
const label = children || statusLabelMap[status];
|
||||
const label = children ?? statusLabelMap[status];
|
||||
|
||||
return (
|
||||
<Badge className={cn(statusClass, className)} {...props}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* 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">
|
||||
<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">
|
||||
{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>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<FloatingActionBar
|
||||
triggerRef={footerRef}
|
||||
title={
|
||||
mode === "create"
|
||||
? "Creating a new business"
|
||||
: "Editing business details"
|
||||
}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* 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">
|
||||
<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">
|
||||
{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>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<FloatingActionBar
|
||||
triggerRef={footerRef}
|
||||
title={
|
||||
mode === "create" ? "Creating a new client" : "Editing client details"
|
||||
}
|
||||
>
|
||||
<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,18 +379,47 @@ 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 */}
|
||||
{/* 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 */}
|
||||
<LineItemRow
|
||||
{/* Desktop/Tablet Card with Drag and Drop */}
|
||||
<SortableLineItem
|
||||
item={item}
|
||||
index={index}
|
||||
canRemove={canRemoveItems}
|
||||
@@ -318,6 +446,8 @@ export function InvoiceLineItems({
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{/* Add Item Button */}
|
||||
<div className="px-3 pt-3">
|
||||
|
||||
@@ -1,107 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { Card, CardContent } from "~/components/ui/card";
|
||||
|
||||
interface FloatingActionBarProps {
|
||||
/** Ref to the element that triggers visibility when scrolled out of view */
|
||||
triggerRef: React.RefObject<HTMLElement | null>;
|
||||
/** Title text displayed on the left (deprecated - use leftContent instead) */
|
||||
title?: string;
|
||||
/** Custom content to display on the left */
|
||||
/** Content to display on the left side */
|
||||
leftContent?: React.ReactNode;
|
||||
/** Action buttons to display on the right */
|
||||
children: React.ReactNode;
|
||||
/** Additional className for styling */
|
||||
className?: string;
|
||||
/** Whether to show the floating bar (for manual control) */
|
||||
show?: boolean;
|
||||
/** Callback when visibility changes */
|
||||
onVisibilityChange?: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
export function FloatingActionBar({
|
||||
triggerRef,
|
||||
title,
|
||||
leftContent,
|
||||
children,
|
||||
className,
|
||||
show,
|
||||
onVisibilityChange,
|
||||
}: FloatingActionBarProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const floatingRef = useRef<HTMLDivElement>(null);
|
||||
const previousVisibleRef = useRef(false);
|
||||
const [isDocked, setIsDocked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// If show prop is provided, use it instead of auto-detection
|
||||
if (show !== undefined) {
|
||||
setIsVisible(show);
|
||||
onVisibilityChange?.(show);
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
const isInView = rect.top < window.innerHeight && rect.bottom >= 0;
|
||||
// Only dock when we're within 50px of the actual bottom AND there's content to scroll
|
||||
const hasScrollableContent = scrollHeight > clientHeight;
|
||||
const shouldDock = hasScrollableContent && distanceFromBottom <= 50;
|
||||
|
||||
// Show floating bar when trigger element is out of view
|
||||
const shouldShow = !isInView;
|
||||
// If content is too small, keep it at bottom of viewport
|
||||
const contentTooSmall = scrollHeight <= clientHeight + 200;
|
||||
|
||||
if (shouldShow !== previousVisibleRef.current) {
|
||||
previousVisibleRef.current = shouldShow;
|
||||
setIsVisible(shouldShow);
|
||||
onVisibilityChange?.(shouldShow);
|
||||
}
|
||||
setIsDocked(shouldDock && !contentTooSmall);
|
||||
};
|
||||
|
||||
// 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 });
|
||||
handleScroll(); // Check initial state
|
||||
|
||||
// Check initial state
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, [triggerRef, show, onVisibilityChange]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={floatingRef} className={cn("floating-action-bar", className)}>
|
||||
<div className="floating-action-bar-content">
|
||||
{leftContent || <p className="floating-action-bar-title">{title}</p>}
|
||||
<div
|
||||
className={cn(
|
||||
// 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 className="floating-action-bar-actions">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} 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 { Button, buttonVariants } from "~/components/ui/button";
|
||||
@@ -24,7 +28,7 @@ function Calendar({
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
formatters: _formatters,
|
||||
components,
|
||||
month,
|
||||
onMonthChange,
|
||||
@@ -49,8 +53,8 @@ function Calendar({
|
||||
"Dec",
|
||||
];
|
||||
|
||||
const currentYear = month?.getFullYear() || new Date().getFullYear();
|
||||
const currentMonth = month?.getMonth() || new Date().getMonth();
|
||||
const currentYear = month?.getFullYear() ?? new Date().getFullYear();
|
||||
const currentMonth = month?.getMonth() ?? new Date().getMonth();
|
||||
|
||||
const years = Array.from({ length: 11 }, (_, i) => currentYear - 5 + i);
|
||||
|
||||
@@ -173,9 +177,9 @@ function Calendar({
|
||||
);
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
MonthCaption: ({ calendarMonth }) => {
|
||||
MonthCaption: ({ calendarMonth: _calendarMonth }) => {
|
||||
if (captionLayout !== "dropdown") {
|
||||
return null;
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -248,7 +252,7 @@ function CalendarDayButton({
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
const _defaultClassNames = getDefaultClassNames();
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -104,7 +104,7 @@ const styles = StyleSheet.create({
|
||||
// Dense header (first page)
|
||||
denseHeader: {
|
||||
marginBottom: 30,
|
||||
borderBottom: "2px solid #16a34a",
|
||||
borderBottom: "2px solid #10b981",
|
||||
paddingBottom: 20,
|
||||
},
|
||||
|
||||
@@ -123,7 +123,7 @@ const styles = StyleSheet.create({
|
||||
businessName: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
marginBottom: 4,
|
||||
},
|
||||
|
||||
@@ -148,7 +148,7 @@ const styles = StyleSheet.create({
|
||||
invoiceTitle: {
|
||||
fontSize: 32,
|
||||
fontWeight: "bold",
|
||||
color: "#16a34a",
|
||||
color: "#10b981",
|
||||
marginBottom: 8,
|
||||
},
|
||||
|
||||
@@ -156,7 +156,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: 15,
|
||||
fontWeight: "semibold",
|
||||
fontFamily: "AzeretMono",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
marginBottom: 4,
|
||||
},
|
||||
|
||||
@@ -170,8 +170,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
statusPaid: {
|
||||
backgroundColor: "#dcfce7",
|
||||
color: "#166534",
|
||||
backgroundColor: "#ecfdf5",
|
||||
color: "#065f46",
|
||||
},
|
||||
|
||||
statusUnpaid: {
|
||||
@@ -194,14 +194,14 @@ const styles = StyleSheet.create({
|
||||
sectionTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
marginBottom: 12,
|
||||
},
|
||||
|
||||
clientName: {
|
||||
fontSize: 13,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
marginBottom: 4,
|
||||
},
|
||||
|
||||
@@ -233,7 +233,7 @@ const styles = StyleSheet.create({
|
||||
detailValue: {
|
||||
fontSize: 10,
|
||||
fontFamily: "AzeretMono",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
fontWeight: "semibold",
|
||||
flex: 1,
|
||||
textAlign: "right",
|
||||
@@ -252,7 +252,7 @@ const styles = StyleSheet.create({
|
||||
notesTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
marginBottom: 6,
|
||||
},
|
||||
|
||||
@@ -282,7 +282,7 @@ const styles = StyleSheet.create({
|
||||
abridgedBusinessName: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
},
|
||||
|
||||
abridgedInvoiceInfo: {
|
||||
@@ -294,14 +294,14 @@ const styles = StyleSheet.create({
|
||||
abridgedInvoiceTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
color: "#16a34a",
|
||||
color: "#10b981",
|
||||
},
|
||||
|
||||
abridgedInvoiceNumber: {
|
||||
fontSize: 13,
|
||||
fontWeight: "semibold",
|
||||
fontFamily: "AzeretMono",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
},
|
||||
|
||||
// Table styles
|
||||
@@ -313,7 +313,7 @@ const styles = StyleSheet.create({
|
||||
tableHeader: {
|
||||
flexDirection: "row",
|
||||
backgroundColor: "#f3f4f6",
|
||||
borderBottom: "2px solid #16a34a",
|
||||
borderBottom: "2px solid #10b981",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
@@ -321,7 +321,7 @@ const styles = StyleSheet.create({
|
||||
tableHeaderCell: {
|
||||
fontSize: 11,
|
||||
fontWeight: "bold",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
|
||||
@@ -362,7 +362,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
tableCell: {
|
||||
fontSize: 10,
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
@@ -433,7 +433,7 @@ const styles = StyleSheet.create({
|
||||
totalAmount: {
|
||||
fontSize: 10,
|
||||
fontFamily: "AzeretMono",
|
||||
color: "#1f2937",
|
||||
color: "#111827",
|
||||
fontWeight: "semibold",
|
||||
},
|
||||
|
||||
@@ -442,7 +442,7 @@ const styles = StyleSheet.create({
|
||||
justifyContent: "space-between",
|
||||
marginTop: 8,
|
||||
paddingTop: 8,
|
||||
borderTop: "2px solid #16a34a",
|
||||
borderTop: "2px solid #10b981",
|
||||
},
|
||||
|
||||
finalTotalLabel: {
|
||||
@@ -455,7 +455,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: 15,
|
||||
fontFamily: "AzeretMono",
|
||||
fontWeight: "bold",
|
||||
color: "#16a34a",
|
||||
color: "#10b981",
|
||||
},
|
||||
|
||||
itemCount: {
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--border: oklch(0.82 0.02 150);
|
||||
--input: oklch(0.82 0.02 150);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
@@ -81,7 +81,7 @@
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 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);
|
||||
|
||||
/* Brand colors */
|
||||
@@ -1380,20 +1380,8 @@
|
||||
}
|
||||
|
||||
/* Floating Action Bar Utility Classes */
|
||||
.floating-action-bar {
|
||||
@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;
|
||||
}
|
||||
|
||||
.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;
|
||||
.pb-safe-area-inset-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* Form Action Footer Utility Classes */
|
||||
|
||||
Reference in New Issue
Block a user