mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 17:44:44 -05:00
- Replace custom invoice items table with responsive DataTable component - Fix server/client component error by creating InvoiceItemsTable client component - Merge danger zone with actions sidebar and use destructive button variant - Standardize button text sizing across all action buttons - Remove false claims from homepage (testimonials, ratings, fake user counts) - Focus homepage messaging on freelancers with honest feature descriptions - Fix dark mode support throughout app by replacing hard-coded colors with semantic classes - Remove aggressive red styling from settings, add subtle red accents only - Align import/export buttons and improve delete confirmation UX - Update dark mode background to have subtle green tint instead of pure black - Fix HTML nesting error in AlertDialog by using div instead of nested p tags This update makes the invoice view properly responsive, removes misleading marketing claims, and ensures consistent dark mode support across the entire application.
239 lines
6.6 KiB
TypeScript
239 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import type { ColumnDef } from "@tanstack/react-table";
|
|
import { Button } from "~/components/ui/button";
|
|
import { DataTable, DataTableColumnHeader } from "~/components/data/data-table";
|
|
import { Building, Pencil, Trash2, ExternalLink } from "lucide-react";
|
|
import { useState } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "~/components/ui/dialog";
|
|
import { api } from "~/trpc/react";
|
|
import { toast } from "sonner";
|
|
|
|
// Type for business data
|
|
interface Business {
|
|
id: string;
|
|
name: string;
|
|
email: string | null;
|
|
phone: string | null;
|
|
addressLine1: string | null;
|
|
addressLine2: string | null;
|
|
city: string | null;
|
|
state: string | null;
|
|
postalCode: string | null;
|
|
country: string | null;
|
|
website: string | null;
|
|
taxId: string | null;
|
|
logoUrl: string | null;
|
|
createdById: string;
|
|
createdAt: Date;
|
|
updatedAt: Date | null;
|
|
}
|
|
|
|
interface BusinessesDataTableProps {
|
|
businesses: Business[];
|
|
}
|
|
|
|
const formatAddress = (business: Business) => {
|
|
const parts = [
|
|
business.addressLine1,
|
|
business.addressLine2,
|
|
business.city,
|
|
business.state,
|
|
business.postalCode,
|
|
].filter(Boolean);
|
|
return parts.join(", ") || "—";
|
|
};
|
|
|
|
export function BusinessesDataTable({
|
|
businesses: initialBusinesses,
|
|
}: BusinessesDataTableProps) {
|
|
const [businesses, setBusinesses] = useState(initialBusinesses);
|
|
const [businessToDelete, setBusinessToDelete] = useState<Business | null>(
|
|
null,
|
|
);
|
|
|
|
const utils = api.useUtils();
|
|
|
|
const deleteBusinessMutation = api.businesses.delete.useMutation({
|
|
onSuccess: () => {
|
|
toast.success("Business deleted successfully");
|
|
setBusinesses(businesses.filter((b) => b.id !== businessToDelete?.id));
|
|
setBusinessToDelete(null);
|
|
void utils.businesses.getAll.invalidate();
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to delete business: ${error.message}`);
|
|
},
|
|
});
|
|
|
|
const handleDelete = () => {
|
|
if (!businessToDelete) return;
|
|
deleteBusinessMutation.mutate({ id: businessToDelete.id });
|
|
};
|
|
|
|
const columns: ColumnDef<Business>[] = [
|
|
{
|
|
accessorKey: "name",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Name" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const business = row.original;
|
|
return (
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-status-info-muted hidden rounded-lg p-2 sm:flex">
|
|
<Building className="text-status-info h-4 w-4" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="truncate font-medium">{business.name}</p>
|
|
<p className="text-muted-foreground truncate text-sm">
|
|
{business.email ?? "—"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "phone",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Phone" />
|
|
),
|
|
cell: ({ row }) => row.original.phone ?? "—",
|
|
meta: {
|
|
headerClassName: "hidden md:table-cell",
|
|
cellClassName: "hidden md:table-cell",
|
|
},
|
|
},
|
|
{
|
|
id: "address",
|
|
header: "Address",
|
|
cell: ({ row }) => formatAddress(row.original),
|
|
meta: {
|
|
headerClassName: "hidden lg:table-cell",
|
|
cellClassName: "hidden lg:table-cell",
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "taxId",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Tax ID" />
|
|
),
|
|
cell: ({ row }) => row.original.taxId ?? "—",
|
|
meta: {
|
|
headerClassName: "hidden xl:table-cell",
|
|
cellClassName: "hidden xl:table-cell",
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "website",
|
|
header: ({ column }) => (
|
|
<DataTableColumnHeader column={column} title="Website" />
|
|
),
|
|
cell: ({ row }) => {
|
|
const website = row.original.website;
|
|
if (!website) return "—";
|
|
|
|
// Add https:// if not present
|
|
const url = website.startsWith("http") ? website : `https://${website}`;
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop: Show full URL */}
|
|
<a
|
|
href={url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary hidden hover:underline sm:inline"
|
|
>
|
|
{website}
|
|
</a>
|
|
{/* Mobile: Show link button */}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 p-0 sm:hidden"
|
|
asChild
|
|
>
|
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
|
<ExternalLink className="h-3.5 w-3.5" />
|
|
</a>
|
|
</Button>
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => {
|
|
const business = row.original;
|
|
return (
|
|
<div className="flex items-center justify-end gap-1">
|
|
<Link href={`/dashboard/businesses/${business.id}/edit`}>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</Link>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 p-0"
|
|
onClick={() => setBusinessToDelete(business)}
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<DataTable
|
|
columns={columns}
|
|
data={businesses}
|
|
searchKey="name"
|
|
searchPlaceholder="Search businesses..."
|
|
/>
|
|
|
|
{/* Delete confirmation dialog */}
|
|
<Dialog
|
|
open={!!businessToDelete}
|
|
onOpenChange={(open) => !open && setBusinessToDelete(null)}
|
|
>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Are you sure?</DialogTitle>
|
|
<DialogDescription>
|
|
This action cannot be undone. This will permanently delete the
|
|
business "{businessToDelete?.name}" and remove all associated
|
|
data.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setBusinessToDelete(null)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDelete}
|
|
disabled={deleteBusinessMutation.isPending}
|
|
>
|
|
Delete
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|