mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 01:24:44 -05:00
feat: enhance dashboard layout and UI components for improved responsiveness
- Introduce new 'xs' screen size in Tailwind configuration for better mobile support - Update dashboard layout to use a cosmic gradient background for a modern look - Refactor Quick Actions component for improved styling and layout consistency - Add Current Open Invoice Card for quick access to ongoing invoices - Standardize button sizes across various components for a cohesive user experience - Implement responsive design adjustments in invoice forms and data tables This update enhances the overall user experience by improving responsiveness and visual appeal across the dashboard and related components.
This commit is contained in:
@@ -722,9 +722,10 @@ function InvoiceEditor({ invoiceId }: { invoiceId: string }) {
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
size="sm"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
<ArrowLeft className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Cancel</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
@@ -732,25 +733,27 @@ function InvoiceEditor({ invoiceId }: { invoiceId: string }) {
|
||||
disabled={isLoading || !isFormValid()}
|
||||
variant="outline"
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
size="sm"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
<Save className="h-4 w-4 sm:mr-2" />
|
||||
)}
|
||||
Save Draft
|
||||
<span className="hidden sm:inline">Save Draft</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdateInvoice}
|
||||
disabled={isLoading || !isFormValid()}
|
||||
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"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
) : (
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
<Send className="h-4 w-4 sm:mr-2" />
|
||||
)}
|
||||
Update Invoice
|
||||
<span className="hidden sm:inline">Update Invoice</span>
|
||||
</Button>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
|
||||
@@ -79,28 +79,6 @@ const formatCurrency = (amount: number) => {
|
||||
};
|
||||
|
||||
const columns: ColumnDef<Invoice>[] = [
|
||||
{
|
||||
accessorKey: "invoiceNumber",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Invoice" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const invoice = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-status-success-muted hidden rounded-lg p-2 sm:flex">
|
||||
<FileText className="text-status-success h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{invoice.invoiceNumber}</p>
|
||||
<p className="text-muted-foreground truncate text-sm">
|
||||
{invoice.items?.length || 0} items
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "client.name",
|
||||
header: ({ column }) => (
|
||||
@@ -109,10 +87,10 @@ const columns: ColumnDef<Invoice>[] = [
|
||||
cell: ({ row }) => {
|
||||
const invoice = row.original;
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{invoice.client?.name || "—"}</p>
|
||||
<p className="text-muted-foreground truncate text-sm">
|
||||
{invoice.client?.email || "—"}
|
||||
<div className="min-w-0 max-w-[80px] sm:max-w-[200px] lg:max-w-[300px]">
|
||||
<p className="truncate font-medium">{invoice.client?.name ?? "—"}</p>
|
||||
<p className="text-muted-foreground truncate text-xs sm:text-sm">
|
||||
{invoice.invoiceNumber}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -121,33 +99,18 @@ const columns: ColumnDef<Invoice>[] = [
|
||||
{
|
||||
accessorKey: "issueDate",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Issue Date" />
|
||||
),
|
||||
cell: ({ row }) => formatDate(row.getValue("issueDate")),
|
||||
meta: {
|
||||
headerClassName: "hidden md:table-cell",
|
||||
cellClassName: "hidden md:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "dueDate",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Due Date" />
|
||||
),
|
||||
cell: ({ row }) => formatDate(row.getValue("dueDate")),
|
||||
meta: {
|
||||
headerClassName: "hidden lg:table-cell",
|
||||
cellClassName: "hidden lg:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "totalAmount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Amount" />
|
||||
<DataTableColumnHeader column={column} title="Date" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const amount = row.getValue("totalAmount") as number;
|
||||
return <p className="font-semibold">{formatCurrency(amount)}</p>;
|
||||
const date = row.getValue("issueDate") as Date;
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm">{formatDate(date)}</p>
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
Due {formatDate(new Date(row.original.dueDate))}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -164,6 +127,31 @@ const columns: ColumnDef<Invoice>[] = [
|
||||
const status = getStatusType(invoice);
|
||||
return value.includes(status);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: "hidden xs:table-cell",
|
||||
cellClassName: "hidden xs:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "totalAmount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Amount" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const amount = row.getValue("totalAmount") as number;
|
||||
return (
|
||||
<div className="text-right">
|
||||
<p className="font-semibold text-sm">{formatCurrency(amount)}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{row.original.items?.length ?? 0} items
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
headerClassName: "hidden xs:table-cell",
|
||||
cellClassName: "hidden xs:table-cell",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
|
||||
@@ -704,9 +704,10 @@ export default function NewInvoicePage() {
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
size="sm"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
<ArrowLeft className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Cancel</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Button
|
||||
@@ -714,25 +715,27 @@ export default function NewInvoicePage() {
|
||||
disabled={isLoading || !isFormValid()}
|
||||
variant="outline"
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
size="sm"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
<Save className="h-4 w-4 sm:mr-2" />
|
||||
)}
|
||||
Save Draft
|
||||
<span className="hidden sm:inline">Save Draft</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateInvoice}
|
||||
disabled={isLoading || !isFormValid()}
|
||||
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"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
) : (
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
<Send className="h-4 w-4 sm:mr-2" />
|
||||
)}
|
||||
Create Invoice
|
||||
<span className="hidden sm:inline">Create Invoice</span>
|
||||
</Button>
|
||||
</FloatingActionBar>
|
||||
</div>
|
||||
|
||||
@@ -8,23 +8,23 @@ export default function DashboardLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-cosmic-gradient bg-nebula-overlay relative min-h-screen">
|
||||
<Navbar />
|
||||
<Sidebar />
|
||||
{/* Mobile layout - no left margin */}
|
||||
<main className="min-h-screen pt-20 md:hidden">
|
||||
<main className="relative z-10 min-h-screen pt-20 md:hidden">
|
||||
<div className="px-4 pt-4 pb-6 sm:px-6">
|
||||
<DashboardBreadcrumbs />
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
{/* Desktop layout - with sidebar margin */}
|
||||
<main className="hidden min-h-screen pt-20 md:ml-[276px] md:block">
|
||||
<main className="relative z-10 hidden min-h-screen pt-20 md:ml-[276px] md:block">
|
||||
<div className="px-6 pt-6 pb-6">
|
||||
<DashboardBreadcrumbs />
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
|
||||
import { StatusBadge, type StatusType } from "~/components/data/status-badge";
|
||||
import { DataTableSkeleton } from "~/components/data/data-table";
|
||||
import { CurrentOpenInvoiceCard } from "~/components/data/current-open-invoice-card";
|
||||
import { auth } from "~/server/auth";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -98,39 +99,43 @@ async function DashboardStats() {
|
||||
// Quick Actions Component
|
||||
function QuickActions() {
|
||||
return (
|
||||
<Card className="mb-6 border-0 shadow-sm">
|
||||
<CardContent className="p-4 py-0">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:gap-4">
|
||||
<Button
|
||||
asChild
|
||||
className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-sm hover:from-emerald-700 hover:to-teal-700"
|
||||
>
|
||||
<Link href="/dashboard/invoices/new">
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Create Invoice
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="flex-1 border-0 shadow-sm"
|
||||
>
|
||||
<Link href="/dashboard/clients/new">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Add Client
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="flex-1 border-0 shadow-sm"
|
||||
>
|
||||
<Link href="/dashboard/businesses/new">
|
||||
<TrendingUp className="mr-2 h-4 w-4" />
|
||||
Add Business
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Plus className="h-5 w-5 text-emerald-600" />
|
||||
Quick Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Button
|
||||
asChild
|
||||
className="w-full bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-sm hover:from-emerald-700 hover:to-teal-700"
|
||||
>
|
||||
<Link href="/dashboard/invoices/new">
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
Create Invoice
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full border-0 shadow-sm"
|
||||
>
|
||||
<Link href="/dashboard/clients/new">
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Add Client
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="w-full border-0 shadow-sm"
|
||||
>
|
||||
<Link href="/dashboard/businesses/new">
|
||||
<TrendingUp className="mr-2 h-4 w-4" />
|
||||
Add Business
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
@@ -245,10 +250,13 @@ export default async function DashboardPage() {
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
|
||||
<QuickActions />
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<CurrentOpenInvoiceCard />
|
||||
<QuickActions />
|
||||
</div>
|
||||
|
||||
<HydrateClient>
|
||||
<Suspense fallback={<DataTableSkeleton columns={1} rows={3} />}>
|
||||
<Suspense fallback={<DataTableSkeleton columns={1} rows={5} />}>
|
||||
<RecentActivity />
|
||||
</Suspense>
|
||||
</HydrateClient>
|
||||
|
||||
152
src/components/data/current-open-invoice-card.tsx
Normal file
152
src/components/data/current-open-invoice-card.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
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 { Skeleton } from "~/components/ui/skeleton";
|
||||
import {
|
||||
FileText,
|
||||
Clock,
|
||||
Plus,
|
||||
Edit,
|
||||
Eye,
|
||||
DollarSign,
|
||||
User,
|
||||
Calendar
|
||||
} from "lucide-react";
|
||||
|
||||
export function CurrentOpenInvoiceCard() {
|
||||
const { data: currentInvoice, isLoading } = api.invoices.getCurrentOpen.useQuery();
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}).format(new Date(date));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="h-5 w-5 text-emerald-600" />
|
||||
Current Open Invoice
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentInvoice) {
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="h-5 w-5 text-emerald-600" />
|
||||
Current Open Invoice
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="text-center py-6">
|
||||
<FileText className="mx-auto mb-3 h-8 w-8 text-muted-foreground" />
|
||||
<p className="text-muted-foreground text-sm mb-4">
|
||||
No open invoice found. Create a new invoice to start tracking your time.
|
||||
</p>
|
||||
<Button asChild className="bg-gradient-to-r from-emerald-600 to-teal-600 text-white hover:from-emerald-700 hover:to-teal-700">
|
||||
<Link href="/dashboard/invoices/new">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create New Invoice
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const totalHours = currentInvoice.items?.reduce((sum, item) => sum + item.hours, 0) ?? 0;
|
||||
const totalAmount = currentInvoice.totalAmount;
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<FileText className="h-5 w-5 text-emerald-600" />
|
||||
Current Open Invoice
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{currentInvoice.invoiceNumber}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Draft
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-emerald-600">
|
||||
{formatCurrency(totalAmount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<User className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Client:</span>
|
||||
<span className="font-medium">{currentInvoice.client?.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Calendar className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Due:</span>
|
||||
<span className="font-medium">{formatDate(currentInvoice.dueDate)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Hours:</span>
|
||||
<span className="font-medium">{totalHours.toFixed(1)}h</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<Button asChild variant="outline" size="sm" className="flex-1">
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}`}>
|
||||
<Eye className="mr-2 h-3 w-3" />
|
||||
View
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" className="flex-1 bg-gradient-to-r from-emerald-600 to-teal-600 text-white hover:from-emerald-700 hover:to-teal-700">
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}>
|
||||
<Edit className="mr-2 h-3 w-3" />
|
||||
Continue
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -410,11 +410,11 @@ export function DataTable<TData, TValue>({
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<span className="text-muted-foreground text-xs sm:text-sm">
|
||||
Page{" "}
|
||||
<span className="hidden sm:inline">Page </span>
|
||||
<span className="text-foreground font-medium">
|
||||
{table.getState().pagination.pageIndex + 1}
|
||||
</span>{" "}
|
||||
of{" "}
|
||||
</span>
|
||||
<span className="sm:inline"> of </span>
|
||||
<span className="text-foreground font-medium">
|
||||
{table.getPageCount() || 1}
|
||||
</span>
|
||||
|
||||
@@ -22,10 +22,10 @@ const statusVariantMap: Record<
|
||||
StatusType,
|
||||
VariantProps<typeof badgeVariants>["variant"]
|
||||
> = {
|
||||
draft: "secondary",
|
||||
sent: "info",
|
||||
paid: "success",
|
||||
overdue: "error",
|
||||
draft: "outline-draft",
|
||||
sent: "outline-sent",
|
||||
paid: "outline-paid",
|
||||
overdue: "outline-overdue",
|
||||
success: "success",
|
||||
warning: "warning",
|
||||
error: "error",
|
||||
|
||||
@@ -517,24 +517,26 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
size="sm"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
<ArrowLeft className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Cancel</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
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"
|
||||
size="sm"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{mode === "create" ? "Creating..." : "Saving..."}
|
||||
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
<span className="hidden sm:inline">{mode === "create" ? "Creating..." : "Saving..."}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{mode === "create" ? "Create Business" : "Save Changes"}
|
||||
<Save className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">{mode === "create" ? "Create Business" : "Save Changes"}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -397,24 +397,26 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
className="border-border/40 hover:bg-accent/50"
|
||||
size="sm"
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
<ArrowLeft className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Cancel</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
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"
|
||||
size="sm"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{mode === "create" ? "Creating..." : "Saving..."}
|
||||
<Loader2 className="h-4 w-4 animate-spin sm:mr-2" />
|
||||
<span className="hidden sm:inline">{mode === "create" ? "Creating..." : "Saving..."}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{mode === "create" ? "Create Client" : "Save Changes"}
|
||||
<Save className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">{mode === "create" ? "Create Client" : "Save Changes"}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -99,7 +99,9 @@ export function FloatingActionBar({
|
||||
)}
|
||||
>
|
||||
<p className="text-muted-foreground text-sm">{title}</p>
|
||||
<div className="flex items-center gap-3">{children}</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,15 @@ import { Button } from "~/components/ui/button";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { Logo } from "~/components/branding/logo";
|
||||
import { SidebarTrigger } from "~/components/navigation/sidebar-trigger";
|
||||
import { api } from "~/trpc/react";
|
||||
import { FileText, Edit } from "lucide-react";
|
||||
|
||||
export function Navbar() {
|
||||
const { data: session, status } = useSession();
|
||||
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
|
||||
|
||||
// Get current open invoice for quick access
|
||||
const { data: currentInvoice } = api.invoices.getCurrentOpen.useQuery();
|
||||
|
||||
return (
|
||||
<header className="fixed top-2 right-2 left-2 z-30 md:top-3 md:right-3 md:left-3">
|
||||
@@ -25,6 +30,22 @@ export function Navbar() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
{/* Quick access to current open invoice */}
|
||||
{session?.user && currentInvoice && (
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="hidden border-border/40 hover:bg-accent/50 text-xs md:flex md:text-sm"
|
||||
>
|
||||
<Link href={`/dashboard/invoices/${currentInvoice.id}/edit`}>
|
||||
<FileText className="mr-1 h-3 w-3 md:mr-2 md:h-4 md:w-4" />
|
||||
<span className="hidden lg:inline">Continue Invoice</span>
|
||||
<span className="lg:hidden">Continue</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{status === "loading" ? (
|
||||
<>
|
||||
<Skeleton className="bg-muted/20 hidden h-5 w-20 sm:inline" />
|
||||
|
||||
@@ -21,6 +21,11 @@ const badgeVariants = cva(
|
||||
warning: "border-transparent bg-status-warning [a&]:hover:opacity-90",
|
||||
error: "border-transparent bg-status-error [a&]:hover:opacity-90",
|
||||
info: "border-transparent bg-status-info [a&]:hover:opacity-90",
|
||||
// Outlined variants for status badges
|
||||
"outline-draft": "border-gray-400 text-gray-600 dark:border-gray-500 dark:text-gray-300 bg-transparent",
|
||||
"outline-sent": "border-blue-400 text-blue-600 dark:border-blue-500 dark:text-blue-300 bg-transparent",
|
||||
"outline-paid": "border-green-400 text-green-600 dark:border-green-500 dark:text-green-300 bg-transparent",
|
||||
"outline-overdue": "border-red-400 text-red-600 dark:border-red-500 dark:text-red-300 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -58,6 +58,36 @@ export const invoicesRouter = createTRPCRouter({
|
||||
}
|
||||
}),
|
||||
|
||||
getCurrentOpen: protectedProcedure.query(async ({ ctx }) => {
|
||||
try {
|
||||
// Get the most recent draft invoice
|
||||
const currentInvoice = await ctx.db.query.invoices.findFirst({
|
||||
where: eq(invoices.createdById, ctx.session.user.id),
|
||||
with: {
|
||||
business: true,
|
||||
client: true,
|
||||
items: {
|
||||
orderBy: (items, { asc }) => [asc(items.position)],
|
||||
},
|
||||
},
|
||||
orderBy: (invoices, { desc }) => [desc(invoices.createdAt)],
|
||||
});
|
||||
|
||||
// Return null if no draft invoice exists
|
||||
if (!currentInvoice || currentInvoice.status !== "draft") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return currentInvoice;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to fetch current open invoice",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
getById: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
|
||||
@@ -107,19 +107,19 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: oklch(0.145 0.02 160);
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0.02 160);
|
||||
--card: oklch(0.25 0.08 170);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0.02 160);
|
||||
--popover: oklch(0.25 0.08 170);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0.015 160);
|
||||
--secondary: oklch(0.30 0.05 170);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0.015 160);
|
||||
--muted: oklch(0.30 0.05 170);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0.015 160);
|
||||
--accent: oklch(0.30 0.05 170);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0 0);
|
||||
@@ -133,7 +133,7 @@
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0.02 160);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary: oklch(0.696 0.17 162.48);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0.015 160);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
@@ -376,8 +376,8 @@
|
||||
.bg-gradient-auth {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
oklch(0.145 0 0) 0%,
|
||||
oklch(0.185 0 0) 100%
|
||||
oklch(0.12 0.05 280) 0%,
|
||||
oklch(0.16 0.03 260) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -390,9 +390,11 @@
|
||||
.bg-gradient-dashboard {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
oklch(0.145 0 0) 0%,
|
||||
oklch(0.185 0 0) 40%,
|
||||
oklch(0.205 0 0) 100%
|
||||
oklch(0.15 0.06 175) 0%,
|
||||
oklch(0.18 0.08 180) 25%,
|
||||
oklch(0.22 0.10 185) 50%,
|
||||
oklch(0.26 0.12 190) 75%,
|
||||
oklch(0.30 0.14 195) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -414,12 +416,83 @@
|
||||
);
|
||||
}
|
||||
|
||||
/* New colorful background patterns for dark mode */
|
||||
.bg-cosmic-gradient {
|
||||
background: linear-gradient(135deg, #f0fdf4 0%, #ecfdf5 50%, #d1fae5 100%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg-cosmic-gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
oklch(0.18 0.07 170) 0%,
|
||||
oklch(0.22 0.09 175) 20%,
|
||||
oklch(0.26 0.11 180) 40%,
|
||||
oklch(0.30 0.13 185) 60%,
|
||||
oklch(0.34 0.15 190) 80%,
|
||||
oklch(0.38 0.17 195) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.bg-aurora-gradient {
|
||||
background: linear-gradient(45deg, #f0fdf4 0%, #ecfdf5 100%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg-aurora-gradient {
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
oklch(0.12 0.06 300) 0%,
|
||||
oklch(0.14 0.05 270) 25%,
|
||||
oklch(0.16 0.04 240) 50%,
|
||||
oklch(0.15 0.03 210) 75%,
|
||||
oklch(0.13 0.04 280) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.bg-nebula-overlay::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg-nebula-overlay::before {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 20% 20%,
|
||||
oklch(0.25 0.08 300 / 0.15) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 80% 80%,
|
||||
oklch(0.22 0.06 240 / 0.12) 0%,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 60% 10%,
|
||||
oklch(0.18 0.04 270 / 0.08) 0%,
|
||||
transparent 40%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg-radial-overlay::before {
|
||||
background: radial-gradient(
|
||||
ellipse at 80% 0%,
|
||||
oklch(0.696 0.17 162.48 / 0.15) 0%,
|
||||
transparent 60%
|
||||
oklch(0.5 0.12 185 / 0.12) 0%,
|
||||
oklch(0.4 0.10 190 / 0.08) 30%,
|
||||
oklch(0.3 0.08 195 / 0.04) 60%,
|
||||
transparent 80%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ export default {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
xs: "475px",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
"var(--font-geist-sans)",
|
||||
|
||||
Reference in New Issue
Block a user