mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-02-05 08:16:31 -05:00
Begin dark mode!
This commit is contained in:
@@ -8,55 +8,54 @@ import { SidebarTrigger } from "./SidebarTrigger";
|
||||
export function Navbar() {
|
||||
const { data: session } = useSession();
|
||||
return (
|
||||
<header className="fixed top-4 left-4 right-4 md:top-6 md:left-6 md:right-6 z-30">
|
||||
<div className="bg-white/60 backdrop-blur-md shadow-2xl rounded-xl border-0">
|
||||
<div className="flex h-14 md:h-16 items-center justify-between px-4 md:px-8">
|
||||
<header className="fixed top-4 right-4 left-4 z-30 md:top-6 md:right-6 md:left-6">
|
||||
<div className="rounded-xl border-0 bg-white/60 shadow-2xl backdrop-blur-md dark:bg-gray-900/60">
|
||||
<div className="flex h-14 items-center justify-between px-4 md:h-16 md:px-8">
|
||||
<div className="flex items-center gap-4 md:gap-6">
|
||||
<SidebarTrigger />
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<Logo size="md" />
|
||||
</Link>
|
||||
</div>
|
||||
<Link href="/dashboard" className="flex items-center gap-2">
|
||||
<Logo size="md" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:gap-4">
|
||||
{session?.user ? (
|
||||
<>
|
||||
<span className="text-xs md:text-sm text-gray-700 hidden sm:inline font-medium">
|
||||
{session?.user ? (
|
||||
<>
|
||||
<span className="hidden text-xs font-medium text-gray-700 sm:inline md:text-sm dark:text-gray-300">
|
||||
{session.user.name ?? session.user.email}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => signOut({ callbackUrl: "/" })}
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50 text-xs md:text-sm"
|
||||
className="border-gray-300 text-xs text-gray-700 hover:bg-gray-50 md:text-sm dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/auth/signin">
|
||||
<Button
|
||||
variant="ghost"
|
||||
Sign Out
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/auth/signin">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-gray-700 hover:bg-gray-100 text-xs md:text-sm"
|
||||
className="text-xs text-gray-700 hover:bg-gray-100 md:text-sm dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
</Link>
|
||||
<Link href="/auth/register">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium text-xs md:text-sm"
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 text-xs font-medium text-white hover:from-emerald-700 hover:to-teal-700 md:text-sm dark:from-emerald-500 dark:to-teal-500 dark:hover:from-emerald-600 dark:hover:to-teal-600"
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Settings, LayoutDashboard, Users, FileText, Building } from "lucide-react";
|
||||
import {
|
||||
Settings,
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
FileText,
|
||||
Building,
|
||||
} from "lucide-react";
|
||||
|
||||
const navLinks = [
|
||||
{ name: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
|
||||
@@ -15,43 +21,47 @@ export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<aside className="hidden md:flex flex-col justify-between fixed left-6 top-28 bottom-6 w-64 z-20 bg-white/60 backdrop-blur-md shadow-2xl rounded-xl border-0 p-8">
|
||||
<nav className="flex flex-col gap-1">
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Main</div>
|
||||
{navLinks.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
aria-current={pathname === link.href ? "page" : undefined}
|
||||
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
|
||||
pathname === link.href
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div>
|
||||
<div className="border-t border-gray-200 my-4" />
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Account</div>
|
||||
<Link
|
||||
href="/dashboard/settings"
|
||||
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
|
||||
pathname === "/dashboard/settings"
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
Settings
|
||||
</Link>
|
||||
<aside className="fixed top-28 bottom-6 left-6 z-20 hidden w-64 flex-col justify-between rounded-xl border-0 bg-white/60 p-8 shadow-2xl backdrop-blur-md md:flex dark:bg-gray-900/60">
|
||||
<nav className="flex flex-col gap-1">
|
||||
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
|
||||
Main
|
||||
</div>
|
||||
</aside>
|
||||
{navLinks.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
aria-current={pathname === link.href ? "page" : undefined}
|
||||
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
|
||||
pathname === link.href
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div>
|
||||
<div className="my-4 border-t border-gray-200 dark:border-gray-700" />
|
||||
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
|
||||
Account
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/settings"
|
||||
className={`flex items-center gap-3 rounded-lg px-4 py-2 text-base font-medium transition-all duration-200 ${
|
||||
pathname === "/dashboard/settings"
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { Sheet, SheetContent, SheetTrigger, SheetHeader, SheetTitle } from "~/components/ui/sheet";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "~/components/ui/sheet";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { MenuIcon, Settings, LayoutDashboard, Users, FileText } from "lucide-react";
|
||||
import {
|
||||
MenuIcon,
|
||||
Settings,
|
||||
LayoutDashboard,
|
||||
Users,
|
||||
FileText,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -20,26 +32,28 @@ export function SidebarTrigger() {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Open sidebar"
|
||||
className="md:hidden bg-white/80 backdrop-blur-sm border-gray-200 shadow-lg hover:bg-white h-8 w-8"
|
||||
className="h-8 w-8 border-gray-200 bg-white/80 shadow-lg backdrop-blur-sm hover:bg-white md:hidden dark:border-gray-600 dark:bg-gray-900/80 dark:hover:bg-gray-800"
|
||||
>
|
||||
<MenuIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="p-0 w-80 max-w-[85vw] bg-white/95 border-0 backdrop-blur-sm"
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="w-80 max-w-[85vw] border-0 bg-white/95 p-0 backdrop-blur-sm dark:bg-gray-900/95"
|
||||
>
|
||||
<SheetHeader className="p-4 border-b border-gray-200">
|
||||
<SheetTitle>Navigation</SheetTitle>
|
||||
<SheetHeader className="border-b border-gray-200 p-4 dark:border-gray-700">
|
||||
<SheetTitle className="dark:text-white">Navigation</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 flex flex-col gap-1 p-4">
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Main</div>
|
||||
<nav className="flex flex-1 flex-col gap-1 p-4">
|
||||
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
|
||||
Main
|
||||
</div>
|
||||
{navLinks.map((link) => {
|
||||
const Icon = link.icon;
|
||||
return (
|
||||
@@ -48,9 +62,9 @@ export function SidebarTrigger() {
|
||||
href={link.href}
|
||||
aria-current={pathname === link.href ? "page" : undefined}
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-3 text-base font-medium transition-all duration-200 ${
|
||||
pathname === link.href
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
pathname === link.href
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
@@ -59,15 +73,17 @@ export function SidebarTrigger() {
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="border-t border-gray-200 my-4" />
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400 tracking-wider uppercase">Account</div>
|
||||
|
||||
<div className="my-4 border-t border-gray-200 dark:border-gray-700" />
|
||||
<div className="mb-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500">
|
||||
Account
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/settings"
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-3 text-base font-medium transition-all duration-200 ${
|
||||
pathname === "/dashboard/settings"
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg"
|
||||
: "text-gray-700 hover:bg-gray-100"
|
||||
? "bg-emerald-100 text-emerald-700 shadow-lg dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
}`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
@@ -78,4 +94,4 @@ export function SidebarTrigger() {
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { Building, Mail, MapPin, Phone, Save, Globe, BadgeDollarSign, Image, Star } from "lucide-react";
|
||||
import {
|
||||
Building,
|
||||
Mail,
|
||||
MapPin,
|
||||
Phone,
|
||||
Save,
|
||||
Globe,
|
||||
BadgeDollarSign,
|
||||
Image,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -38,10 +48,11 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Fetch business data if editing
|
||||
const { data: business, isLoading: isLoadingBusiness } = api.businesses.getById.useQuery(
|
||||
{ id: businessId! },
|
||||
{ enabled: mode === "edit" && !!businessId }
|
||||
);
|
||||
const { data: business, isLoading: isLoadingBusiness } =
|
||||
api.businesses.getById.useQuery(
|
||||
{ id: businessId! },
|
||||
{ enabled: mode === "edit" && !!businessId },
|
||||
);
|
||||
|
||||
const createBusiness = api.businesses.create.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -102,12 +113,12 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string | boolean) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// Phone number formatting (reuse from client-form)
|
||||
const formatPhoneNumber = (value: string) => {
|
||||
const phoneNumber = value.replace(/\D/g, '');
|
||||
const phoneNumber = value.replace(/\D/g, "");
|
||||
if (phoneNumber.length <= 3) {
|
||||
return phoneNumber;
|
||||
} else if (phoneNumber.length <= 6) {
|
||||
@@ -174,7 +185,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
{ value: "WA", label: "Washington" },
|
||||
{ value: "WV", label: "West Virginia" },
|
||||
{ value: "WI", label: "Wisconsin" },
|
||||
{ value: "WY", label: "Wyoming" }
|
||||
{ value: "WY", label: "Wyoming" },
|
||||
];
|
||||
|
||||
const MOST_USED_COUNTRIES = [
|
||||
@@ -184,27 +195,223 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
{ value: "Australia", label: "Australia" },
|
||||
{ value: "Germany", label: "Germany" },
|
||||
{ value: "France", label: "France" },
|
||||
{ value: "India", label: "India" }
|
||||
{ value: "India", label: "India" },
|
||||
];
|
||||
|
||||
const ALL_COUNTRIES = [
|
||||
"Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China", "Colombia", "Comoros", "Congo", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Democratic Republic of the Congo", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", "Palau", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"
|
||||
"Afghanistan",
|
||||
"Albania",
|
||||
"Algeria",
|
||||
"Andorra",
|
||||
"Angola",
|
||||
"Antigua and Barbuda",
|
||||
"Argentina",
|
||||
"Armenia",
|
||||
"Australia",
|
||||
"Austria",
|
||||
"Azerbaijan",
|
||||
"Bahamas",
|
||||
"Bahrain",
|
||||
"Bangladesh",
|
||||
"Barbados",
|
||||
"Belarus",
|
||||
"Belgium",
|
||||
"Belize",
|
||||
"Benin",
|
||||
"Bhutan",
|
||||
"Bolivia",
|
||||
"Bosnia and Herzegovina",
|
||||
"Botswana",
|
||||
"Brazil",
|
||||
"Brunei",
|
||||
"Bulgaria",
|
||||
"Burkina Faso",
|
||||
"Burundi",
|
||||
"Cabo Verde",
|
||||
"Cambodia",
|
||||
"Cameroon",
|
||||
"Canada",
|
||||
"Central African Republic",
|
||||
"Chad",
|
||||
"Chile",
|
||||
"China",
|
||||
"Colombia",
|
||||
"Comoros",
|
||||
"Congo",
|
||||
"Costa Rica",
|
||||
"Croatia",
|
||||
"Cuba",
|
||||
"Cyprus",
|
||||
"Czech Republic",
|
||||
"Democratic Republic of the Congo",
|
||||
"Denmark",
|
||||
"Djibouti",
|
||||
"Dominica",
|
||||
"Dominican Republic",
|
||||
"East Timor",
|
||||
"Ecuador",
|
||||
"Egypt",
|
||||
"El Salvador",
|
||||
"Equatorial Guinea",
|
||||
"Eritrea",
|
||||
"Estonia",
|
||||
"Eswatini",
|
||||
"Ethiopia",
|
||||
"Fiji",
|
||||
"Finland",
|
||||
"France",
|
||||
"Gabon",
|
||||
"Gambia",
|
||||
"Georgia",
|
||||
"Germany",
|
||||
"Ghana",
|
||||
"Greece",
|
||||
"Grenada",
|
||||
"Guatemala",
|
||||
"Guinea",
|
||||
"Guinea-Bissau",
|
||||
"Guyana",
|
||||
"Haiti",
|
||||
"Honduras",
|
||||
"Hungary",
|
||||
"Iceland",
|
||||
"India",
|
||||
"Indonesia",
|
||||
"Iran",
|
||||
"Iraq",
|
||||
"Ireland",
|
||||
"Israel",
|
||||
"Italy",
|
||||
"Ivory Coast",
|
||||
"Jamaica",
|
||||
"Japan",
|
||||
"Jordan",
|
||||
"Kazakhstan",
|
||||
"Kenya",
|
||||
"Kiribati",
|
||||
"Kuwait",
|
||||
"Kyrgyzstan",
|
||||
"Laos",
|
||||
"Latvia",
|
||||
"Lebanon",
|
||||
"Lesotho",
|
||||
"Liberia",
|
||||
"Libya",
|
||||
"Liechtenstein",
|
||||
"Lithuania",
|
||||
"Luxembourg",
|
||||
"Madagascar",
|
||||
"Malawi",
|
||||
"Malaysia",
|
||||
"Maldives",
|
||||
"Mali",
|
||||
"Malta",
|
||||
"Marshall Islands",
|
||||
"Mauritania",
|
||||
"Mauritius",
|
||||
"Mexico",
|
||||
"Micronesia",
|
||||
"Moldova",
|
||||
"Monaco",
|
||||
"Mongolia",
|
||||
"Montenegro",
|
||||
"Morocco",
|
||||
"Mozambique",
|
||||
"Myanmar",
|
||||
"Namibia",
|
||||
"Nauru",
|
||||
"Nepal",
|
||||
"Netherlands",
|
||||
"New Zealand",
|
||||
"Nicaragua",
|
||||
"Niger",
|
||||
"Nigeria",
|
||||
"North Korea",
|
||||
"North Macedonia",
|
||||
"Norway",
|
||||
"Oman",
|
||||
"Pakistan",
|
||||
"Palau",
|
||||
"Palestine",
|
||||
"Panama",
|
||||
"Papua New Guinea",
|
||||
"Paraguay",
|
||||
"Peru",
|
||||
"Philippines",
|
||||
"Poland",
|
||||
"Portugal",
|
||||
"Qatar",
|
||||
"Romania",
|
||||
"Russia",
|
||||
"Rwanda",
|
||||
"Saint Kitts and Nevis",
|
||||
"Saint Lucia",
|
||||
"Saint Vincent and the Grenadines",
|
||||
"Samoa",
|
||||
"San Marino",
|
||||
"Sao Tome and Principe",
|
||||
"Saudi Arabia",
|
||||
"Senegal",
|
||||
"Serbia",
|
||||
"Seychelles",
|
||||
"Sierra Leone",
|
||||
"Singapore",
|
||||
"Slovakia",
|
||||
"Slovenia",
|
||||
"Solomon Islands",
|
||||
"Somalia",
|
||||
"South Africa",
|
||||
"South Korea",
|
||||
"South Sudan",
|
||||
"Spain",
|
||||
"Sri Lanka",
|
||||
"Sudan",
|
||||
"Suriname",
|
||||
"Sweden",
|
||||
"Switzerland",
|
||||
"Syria",
|
||||
"Taiwan",
|
||||
"Tajikistan",
|
||||
"Tanzania",
|
||||
"Thailand",
|
||||
"Togo",
|
||||
"Tonga",
|
||||
"Trinidad and Tobago",
|
||||
"Tunisia",
|
||||
"Turkey",
|
||||
"Turkmenistan",
|
||||
"Tuvalu",
|
||||
"Uganda",
|
||||
"Ukraine",
|
||||
"United Arab Emirates",
|
||||
"United Kingdom",
|
||||
"United States",
|
||||
"Uruguay",
|
||||
"Uzbekistan",
|
||||
"Vanuatu",
|
||||
"Vatican City",
|
||||
"Venezuela",
|
||||
"Vietnam",
|
||||
"Yemen",
|
||||
"Zambia",
|
||||
"Zimbabwe",
|
||||
];
|
||||
|
||||
const OTHER_COUNTRIES = ALL_COUNTRIES
|
||||
.filter(c => !MOST_USED_COUNTRIES.some(mc => mc.value === c))
|
||||
.map(country => ({ value: country, label: country }))
|
||||
const OTHER_COUNTRIES = ALL_COUNTRIES.filter(
|
||||
(c) => !MOST_USED_COUNTRIES.some((mc) => mc.value === c),
|
||||
)
|
||||
.map((country) => ({ value: country, label: country }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const ALL_COUNTRIES_OPTIONS = [
|
||||
{ value: "__placeholder__", label: "Select country" },
|
||||
...MOST_USED_COUNTRIES,
|
||||
...OTHER_COUNTRIES
|
||||
...MOST_USED_COUNTRIES,
|
||||
...OTHER_COUNTRIES,
|
||||
];
|
||||
|
||||
if (mode === "edit" && isLoadingBusiness) {
|
||||
return (
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
|
||||
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardContent className="p-8">
|
||||
<FormSkeleton />
|
||||
</CardContent>
|
||||
@@ -213,18 +420,23 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
|
||||
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Basic Information Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2 text-emerald-700">
|
||||
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
|
||||
<Building className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Business Information</h3>
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Business Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="name"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Business Name *
|
||||
</Label>
|
||||
<Input
|
||||
@@ -233,22 +445,25 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
required
|
||||
placeholder="Enter business name"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="email"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Email Address
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Mail className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
placeholder="business@example.com"
|
||||
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,40 +472,50 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
|
||||
{/* Contact Information Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2 text-emerald-700">
|
||||
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
|
||||
<Phone className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Contact Information</h3>
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Contact Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="phone"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Phone Number
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Phone className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handlePhoneChange(e.target.value)}
|
||||
placeholder="(555) 123-4567"
|
||||
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="website" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="website"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Website
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Globe className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Globe className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Input
|
||||
id="website"
|
||||
type="url"
|
||||
value={formData.website}
|
||||
onChange={(e) => handleInputChange("website", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("website", e.target.value)
|
||||
}
|
||||
placeholder="https://yourbusiness.com"
|
||||
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,37 +524,52 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
|
||||
{/* Address Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2 text-emerald-700">
|
||||
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
|
||||
<MapPin className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Address</h3>
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Address Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="addressLine1" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="addressLine1"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Address Line 1
|
||||
</Label>
|
||||
<Input
|
||||
id="addressLine1"
|
||||
value={formData.addressLine1}
|
||||
onChange={(e) => handleInputChange("addressLine1", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("addressLine1", e.target.value)
|
||||
}
|
||||
placeholder="123 Main St"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="addressLine2" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="addressLine2"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Address Line 2
|
||||
</Label>
|
||||
<Input
|
||||
id="addressLine2"
|
||||
value={formData.addressLine2}
|
||||
onChange={(e) => handleInputChange("addressLine2", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("addressLine2", e.target.value)
|
||||
}
|
||||
placeholder="Suite 100"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="city"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
City
|
||||
</Label>
|
||||
<Input
|
||||
@@ -337,11 +577,14 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
value={formData.city}
|
||||
onChange={(e) => handleInputChange("city", e.target.value)}
|
||||
placeholder="City"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="state" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="state"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
State/Province
|
||||
</Label>
|
||||
<SearchableSelect
|
||||
@@ -353,19 +596,27 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postalCode" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="postalCode"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Postal Code
|
||||
</Label>
|
||||
<Input
|
||||
id="postalCode"
|
||||
value={formData.postalCode}
|
||||
onChange={(e) => handleInputChange("postalCode", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("postalCode", e.target.value)
|
||||
}
|
||||
placeholder="ZIP or postal code"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="country" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="country"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Country
|
||||
</Label>
|
||||
<SearchableSelect
|
||||
@@ -381,13 +632,18 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
|
||||
{/* Tax, Logo, Default Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2 text-emerald-700">
|
||||
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
|
||||
<BadgeDollarSign className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Other Details</h3>
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Business Details
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="taxId" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="taxId"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Tax ID / VAT Number
|
||||
</Label>
|
||||
<Input
|
||||
@@ -395,40 +651,51 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
value={formData.taxId}
|
||||
onChange={(e) => handleInputChange("taxId", e.target.value)}
|
||||
placeholder="Tax ID or VAT number"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="logoUrl" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="logoUrl"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Logo URL
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Image className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Image className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Input
|
||||
id="logoUrl"
|
||||
value={formData.logoUrl}
|
||||
onChange={(e) => handleInputChange("logoUrl", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("logoUrl", e.target.value)
|
||||
}
|
||||
placeholder="https://yourbusiness.com/logo.png"
|
||||
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-4">
|
||||
<div className="mt-4 flex items-center space-x-2">
|
||||
<input
|
||||
id="isDefault"
|
||||
type="checkbox"
|
||||
checked={formData.isDefault}
|
||||
onChange={(e) => handleInputChange("isDefault", e.target.checked)}
|
||||
className="h-5 w-5 text-emerald-600 border-gray-300 rounded focus:ring-emerald-500"
|
||||
onChange={(e) =>
|
||||
handleInputChange("isDefault", e.target.checked)
|
||||
}
|
||||
className="h-5 w-5 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500"
|
||||
/>
|
||||
<Label htmlFor="isDefault" className="text-sm font-medium text-gray-700 flex items-center">
|
||||
<Star className="h-4 w-4 mr-1 text-yellow-400" /> Set as default business
|
||||
<Label
|
||||
htmlFor="isDefault"
|
||||
className="flex items-center text-sm font-medium text-gray-700"
|
||||
>
|
||||
<Star className="mr-1 h-4 w-4 text-yellow-400" /> Set as
|
||||
default business
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-8">
|
||||
<div className="mt-8 flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -440,7 +707,7 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl"
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
|
||||
disabled={loading}
|
||||
>
|
||||
<Save className="mr-2 h-5 w-5" />
|
||||
@@ -451,4 +718,4 @@ export function BusinessForm({ businessId, mode }: BusinessFormProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Fetch client data if editing
|
||||
const { data: client, isLoading: isLoadingClient } = api.clients.getById.useQuery(
|
||||
{ id: clientId! },
|
||||
{ enabled: mode === "edit" && !!clientId }
|
||||
);
|
||||
const { data: client, isLoading: isLoadingClient } =
|
||||
api.clients.getById.useQuery(
|
||||
{ id: clientId! },
|
||||
{ enabled: mode === "edit" && !!clientId },
|
||||
);
|
||||
|
||||
const createClient = api.clients.create.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -94,12 +95,12 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
};
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// Phone number formatting
|
||||
const formatPhoneNumber = (value: string) => {
|
||||
const phoneNumber = value.replace(/\D/g, '');
|
||||
const phoneNumber = value.replace(/\D/g, "");
|
||||
if (phoneNumber.length <= 3) {
|
||||
return phoneNumber;
|
||||
} else if (phoneNumber.length <= 6) {
|
||||
@@ -115,22 +116,272 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
};
|
||||
|
||||
const US_STATES = [
|
||||
"", "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY"
|
||||
"",
|
||||
"AL",
|
||||
"AK",
|
||||
"AZ",
|
||||
"AR",
|
||||
"CA",
|
||||
"CO",
|
||||
"CT",
|
||||
"DE",
|
||||
"FL",
|
||||
"GA",
|
||||
"HI",
|
||||
"ID",
|
||||
"IL",
|
||||
"IN",
|
||||
"IA",
|
||||
"KS",
|
||||
"KY",
|
||||
"LA",
|
||||
"ME",
|
||||
"MD",
|
||||
"MA",
|
||||
"MI",
|
||||
"MN",
|
||||
"MS",
|
||||
"MO",
|
||||
"MT",
|
||||
"NE",
|
||||
"NV",
|
||||
"NH",
|
||||
"NJ",
|
||||
"NM",
|
||||
"NY",
|
||||
"NC",
|
||||
"ND",
|
||||
"OH",
|
||||
"OK",
|
||||
"OR",
|
||||
"PA",
|
||||
"RI",
|
||||
"SC",
|
||||
"SD",
|
||||
"TN",
|
||||
"TX",
|
||||
"UT",
|
||||
"VT",
|
||||
"VA",
|
||||
"WA",
|
||||
"WV",
|
||||
"WI",
|
||||
"WY",
|
||||
];
|
||||
|
||||
const MOST_USED_COUNTRIES = [
|
||||
"United States", "United Kingdom", "Canada", "Australia", "Germany", "France", "India"
|
||||
"United States",
|
||||
"United Kingdom",
|
||||
"Canada",
|
||||
"Australia",
|
||||
"Germany",
|
||||
"France",
|
||||
"India",
|
||||
];
|
||||
const ALL_COUNTRIES = [
|
||||
"Afghanistan", "Albania", "Algeria", "Andorra", "Angola", "Antigua and Barbuda", "Argentina", "Armenia", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bhutan", "Bolivia", "Bosnia and Herzegovina", "Botswana", "Brazil", "Brunei", "Bulgaria", "Burkina Faso", "Burundi", "Cabo Verde", "Cambodia", "Cameroon", "Canada", "Central African Republic", "Chad", "Chile", "China", "Colombia", "Comoros", "Congo", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Eswatini", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Greece", "Grenada", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Honduras", "Hungary", "Iceland", "India", "Indonesia", "Iran", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Kuwait", "Kyrgyzstan", "Laos", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Mauritania", "Mauritius", "Mexico", "Micronesia", "Moldova", "Monaco", "Mongolia", "Montenegro", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "North Korea", "North Macedonia", "Norway", "Oman", "Pakistan", "Palau", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Korea", "South Sudan", "Spain", "Sri Lanka", "Sudan", "Suriname", "Sweden", "Switzerland", "Syria", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Togo", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City", "Venezuela", "Vietnam", "Yemen", "Zambia", "Zimbabwe"
|
||||
"Afghanistan",
|
||||
"Albania",
|
||||
"Algeria",
|
||||
"Andorra",
|
||||
"Angola",
|
||||
"Antigua and Barbuda",
|
||||
"Argentina",
|
||||
"Armenia",
|
||||
"Australia",
|
||||
"Austria",
|
||||
"Azerbaijan",
|
||||
"Bahamas",
|
||||
"Bahrain",
|
||||
"Bangladesh",
|
||||
"Barbados",
|
||||
"Belarus",
|
||||
"Belgium",
|
||||
"Belize",
|
||||
"Benin",
|
||||
"Bhutan",
|
||||
"Bolivia",
|
||||
"Bosnia and Herzegovina",
|
||||
"Botswana",
|
||||
"Brazil",
|
||||
"Brunei",
|
||||
"Bulgaria",
|
||||
"Burkina Faso",
|
||||
"Burundi",
|
||||
"Cabo Verde",
|
||||
"Cambodia",
|
||||
"Cameroon",
|
||||
"Canada",
|
||||
"Central African Republic",
|
||||
"Chad",
|
||||
"Chile",
|
||||
"China",
|
||||
"Colombia",
|
||||
"Comoros",
|
||||
"Congo",
|
||||
"Costa Rica",
|
||||
"Croatia",
|
||||
"Cuba",
|
||||
"Cyprus",
|
||||
"Czech Republic",
|
||||
"Denmark",
|
||||
"Djibouti",
|
||||
"Dominica",
|
||||
"Dominican Republic",
|
||||
"East Timor",
|
||||
"Ecuador",
|
||||
"Egypt",
|
||||
"El Salvador",
|
||||
"Equatorial Guinea",
|
||||
"Eritrea",
|
||||
"Estonia",
|
||||
"Eswatini",
|
||||
"Ethiopia",
|
||||
"Fiji",
|
||||
"Finland",
|
||||
"France",
|
||||
"Gabon",
|
||||
"Gambia",
|
||||
"Georgia",
|
||||
"Germany",
|
||||
"Ghana",
|
||||
"Greece",
|
||||
"Grenada",
|
||||
"Guatemala",
|
||||
"Guinea",
|
||||
"Guinea-Bissau",
|
||||
"Guyana",
|
||||
"Haiti",
|
||||
"Honduras",
|
||||
"Hungary",
|
||||
"Iceland",
|
||||
"India",
|
||||
"Indonesia",
|
||||
"Iran",
|
||||
"Iraq",
|
||||
"Ireland",
|
||||
"Israel",
|
||||
"Italy",
|
||||
"Ivory Coast",
|
||||
"Jamaica",
|
||||
"Japan",
|
||||
"Jordan",
|
||||
"Kazakhstan",
|
||||
"Kenya",
|
||||
"Kiribati",
|
||||
"Kuwait",
|
||||
"Kyrgyzstan",
|
||||
"Laos",
|
||||
"Latvia",
|
||||
"Lebanon",
|
||||
"Lesotho",
|
||||
"Liberia",
|
||||
"Libya",
|
||||
"Liechtenstein",
|
||||
"Lithuania",
|
||||
"Luxembourg",
|
||||
"Madagascar",
|
||||
"Malawi",
|
||||
"Malaysia",
|
||||
"Maldives",
|
||||
"Mali",
|
||||
"Malta",
|
||||
"Marshall Islands",
|
||||
"Mauritania",
|
||||
"Mauritius",
|
||||
"Mexico",
|
||||
"Micronesia",
|
||||
"Moldova",
|
||||
"Monaco",
|
||||
"Mongolia",
|
||||
"Montenegro",
|
||||
"Morocco",
|
||||
"Mozambique",
|
||||
"Myanmar",
|
||||
"Namibia",
|
||||
"Nauru",
|
||||
"Nepal",
|
||||
"Netherlands",
|
||||
"New Zealand",
|
||||
"Nicaragua",
|
||||
"Niger",
|
||||
"Nigeria",
|
||||
"North Korea",
|
||||
"North Macedonia",
|
||||
"Norway",
|
||||
"Oman",
|
||||
"Pakistan",
|
||||
"Palau",
|
||||
"Palestine",
|
||||
"Panama",
|
||||
"Papua New Guinea",
|
||||
"Paraguay",
|
||||
"Peru",
|
||||
"Philippines",
|
||||
"Poland",
|
||||
"Portugal",
|
||||
"Qatar",
|
||||
"Romania",
|
||||
"Russia",
|
||||
"Rwanda",
|
||||
"Saint Kitts and Nevis",
|
||||
"Saint Lucia",
|
||||
"Saint Vincent and the Grenadines",
|
||||
"Samoa",
|
||||
"San Marino",
|
||||
"Sao Tome and Principe",
|
||||
"Saudi Arabia",
|
||||
"Senegal",
|
||||
"Serbia",
|
||||
"Seychelles",
|
||||
"Sierra Leone",
|
||||
"Singapore",
|
||||
"Slovakia",
|
||||
"Slovenia",
|
||||
"Solomon Islands",
|
||||
"Somalia",
|
||||
"South Africa",
|
||||
"South Korea",
|
||||
"South Sudan",
|
||||
"Spain",
|
||||
"Sri Lanka",
|
||||
"Sudan",
|
||||
"Suriname",
|
||||
"Sweden",
|
||||
"Switzerland",
|
||||
"Syria",
|
||||
"Taiwan",
|
||||
"Tajikistan",
|
||||
"Tanzania",
|
||||
"Thailand",
|
||||
"Togo",
|
||||
"Tonga",
|
||||
"Trinidad and Tobago",
|
||||
"Tunisia",
|
||||
"Turkey",
|
||||
"Turkmenistan",
|
||||
"Tuvalu",
|
||||
"Uganda",
|
||||
"Ukraine",
|
||||
"United Arab Emirates",
|
||||
"United Kingdom",
|
||||
"United States",
|
||||
"Uruguay",
|
||||
"Uzbekistan",
|
||||
"Vanuatu",
|
||||
"Vatican City",
|
||||
"Venezuela",
|
||||
"Vietnam",
|
||||
"Yemen",
|
||||
"Zambia",
|
||||
"Zimbabwe",
|
||||
];
|
||||
const OTHER_COUNTRIES = ALL_COUNTRIES.filter(
|
||||
c => !MOST_USED_COUNTRIES.includes(c)
|
||||
(c) => !MOST_USED_COUNTRIES.includes(c),
|
||||
).sort();
|
||||
|
||||
if (mode === "edit" && isLoadingClient) {
|
||||
return (
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
|
||||
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardContent className="p-8">
|
||||
<FormSkeleton />
|
||||
</CardContent>
|
||||
@@ -139,18 +390,23 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm w-full my-8 px-0">
|
||||
<Card className="my-8 w-full border-0 bg-white/80 px-0 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Basic Information Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2 text-emerald-700">
|
||||
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
|
||||
<Building className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Business Information</h3>
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Business Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="name"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Business Name / Full Name *
|
||||
</Label>
|
||||
<Input
|
||||
@@ -159,22 +415,25 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
required
|
||||
placeholder="Enter business name or full name"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="email"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Email Address
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Mail className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400 dark:text-gray-500" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
placeholder="business@example.com"
|
||||
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,17 +442,22 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
|
||||
{/* Contact Information Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2 text-emerald-700">
|
||||
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
|
||||
<Phone className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Contact Information</h3>
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Contact Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="phone"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Phone Number
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Phone className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400 dark:text-gray-500" />
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
@@ -201,49 +465,66 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
onChange={(e) => handlePhoneChange(e.target.value)}
|
||||
placeholder="(555) 123-4567"
|
||||
maxLength={14}
|
||||
className="h-12 pl-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 pl-10 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">Format: (555) 123-4567</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Format: (555) 123-4567
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Address Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center space-x-2 text-emerald-700">
|
||||
<div className="flex items-center space-x-2 text-emerald-700 dark:text-emerald-400">
|
||||
<MapPin className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Address Information</h3>
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Address Information
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="addressLine1" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="addressLine1"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Address Line 1
|
||||
</Label>
|
||||
<Input
|
||||
id="addressLine1"
|
||||
value={formData.addressLine1}
|
||||
onChange={(e) => handleInputChange("addressLine1", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("addressLine1", e.target.value)
|
||||
}
|
||||
placeholder="123 Main Street"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="addressLine2" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="addressLine2"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Address Line 2
|
||||
</Label>
|
||||
<Input
|
||||
id="addressLine2"
|
||||
value={formData.addressLine2}
|
||||
onChange={(e) => handleInputChange("addressLine2", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("addressLine2", e.target.value)
|
||||
}
|
||||
placeholder="Suite 100"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="city" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="city"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
City
|
||||
</Label>
|
||||
<Input
|
||||
@@ -255,14 +536,17 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="state" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="state"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
State / Province
|
||||
</Label>
|
||||
<select
|
||||
id="state"
|
||||
value={formData.state}
|
||||
onChange={(e) => handleInputChange("state", e.target.value)}
|
||||
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
{US_STATES.map((state) => (
|
||||
<option key={state} value={state}>
|
||||
@@ -272,28 +556,36 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="postalCode" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="postalCode"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Postal Code
|
||||
</Label>
|
||||
<Input
|
||||
id="postalCode"
|
||||
value={formData.postalCode}
|
||||
onChange={(e) => handleInputChange("postalCode", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("postalCode", e.target.value)
|
||||
}
|
||||
placeholder="12345"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-12 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="country" className="text-sm font-medium text-gray-700">
|
||||
Country
|
||||
</Label>
|
||||
<select
|
||||
id="country"
|
||||
value={formData.country}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="country"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Country
|
||||
</Label>
|
||||
<select
|
||||
id="country"
|
||||
value={formData.country}
|
||||
onChange={(e) => handleInputChange("country", e.target.value)}
|
||||
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500"
|
||||
>
|
||||
className="h-12 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="">Select Country</option>
|
||||
<optgroup label="Most Used">
|
||||
{MOST_USED_COUNTRIES.map((country) => (
|
||||
@@ -309,16 +601,16 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex gap-3 pt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
@@ -327,22 +619,22 @@ export function ClientForm({ clientId, mode }: ClientFormProps) {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{mode === "create" ? "Create Client" : "Update Client"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.push("/dashboard/clients")}
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
className="border-gray-300 font-medium text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ import {
|
||||
AlertCircle,
|
||||
Settings,
|
||||
User,
|
||||
Mail
|
||||
Mail,
|
||||
} from "lucide-react";
|
||||
|
||||
export function DarkModeTest() {
|
||||
return (
|
||||
<div className="min-h-screen p-8 space-y-8">
|
||||
<div className="min-h-screen space-y-8 p-8">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-4">
|
||||
<div className="space-y-4 text-center">
|
||||
<h1 className="text-4xl font-bold text-gray-900 dark:text-white">
|
||||
Dark Mode Test Suite
|
||||
</h1>
|
||||
@@ -53,11 +53,17 @@ export function DarkModeTest() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Text Colors:</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Text Colors:
|
||||
</p>
|
||||
<div className="text-gray-900 dark:text-white">Primary Text</div>
|
||||
<div className="text-gray-700 dark:text-gray-300">Secondary Text</div>
|
||||
<div className="text-gray-700 dark:text-gray-300">
|
||||
Secondary Text
|
||||
</div>
|
||||
<div className="text-gray-500 dark:text-gray-400">Muted Text</div>
|
||||
<div className="text-green-600 dark:text-green-400">Success Text</div>
|
||||
<div className="text-green-600 dark:text-green-400">
|
||||
Success Text
|
||||
</div>
|
||||
<div className="text-red-600 dark:text-red-400">Error Text</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -71,10 +77,18 @@ export function DarkModeTest() {
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm">Default</Button>
|
||||
<Button variant="secondary" size="sm">Secondary</Button>
|
||||
<Button variant="outline" size="sm">Outline</Button>
|
||||
<Button variant="ghost" size="sm">Ghost</Button>
|
||||
<Button variant="destructive" size="sm">Destructive</Button>
|
||||
<Button variant="secondary" size="sm">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
Outline
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm">
|
||||
Ghost
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm">
|
||||
Destructive
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -100,7 +114,7 @@ export function DarkModeTest() {
|
||||
<Label htmlFor="test-select">Test Select</Label>
|
||||
<select
|
||||
id="test-select"
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30"
|
||||
className="border-input placeholder:text-muted-foreground focus-visible:ring-ring dark:bg-input/30 flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:ring-1 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="">Select an option</option>
|
||||
<option value="1">Option 1</option>
|
||||
@@ -125,19 +139,27 @@ export function DarkModeTest() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Success Status</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Success Status
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<X className="h-4 w-4 text-red-500" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Error Status</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Error Status
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Info className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Info Status</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Info Status
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
<span className="text-gray-700 dark:text-gray-300">Warning Status</span>
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
Warning Status
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -150,14 +172,20 @@ export function DarkModeTest() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-700 rounded-md">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">Light Background</p>
|
||||
<div className="rounded-md bg-gray-50 p-3 dark:bg-gray-700">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Light Background
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-100 dark:bg-gray-600 rounded-md">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">Medium Background</p>
|
||||
<div className="rounded-md bg-gray-100 p-3 dark:bg-gray-600">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Medium Background
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-md">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">Card Background</p>
|
||||
<div className="rounded-md border border-gray-200 bg-white p-3 dark:border-gray-600 dark:bg-gray-800">
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Card Background
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -172,19 +200,27 @@ export function DarkModeTest() {
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<User className="h-6 w-6 text-gray-700 dark:text-gray-300" />
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Default</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Default
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Settings className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Success</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Success
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<AlertCircle className="h-6 w-6 text-red-600 dark:text-red-400" />
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Error</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Error
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Info className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Info</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Info
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -199,16 +235,28 @@ export function DarkModeTest() {
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">Dark Mode Method:</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Media Query (@media (prefers-color-scheme: dark))</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Dark Mode Method:
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Media Query (@media (prefers-color-scheme: dark))
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">Tailwind Config:</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">darkMode: "media"</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Tailwind Config:
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
darkMode: "media"
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">CSS Variables:</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">oklch() color space</p>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
CSS Variables:
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
oklch() color space
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -217,10 +265,15 @@ export function DarkModeTest() {
|
||||
{/* Instructions */}
|
||||
<Card className="border-blue-200 dark:border-blue-800 dark:bg-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-blue-700 dark:text-blue-300">Testing Instructions</CardTitle>
|
||||
<CardTitle className="text-blue-700 dark:text-blue-300">
|
||||
Testing Instructions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-blue-600 dark:text-blue-400">
|
||||
<p>• Change your system theme between light and dark to test automatic switching</p>
|
||||
<p>
|
||||
• Change your system theme between light and dark to test automatic
|
||||
switching
|
||||
</p>
|
||||
<p>• All UI elements should adapt colors automatically</p>
|
||||
<p>• Text should remain readable in both modes</p>
|
||||
<p>• Icons and buttons should have appropriate contrast</p>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator } from "~/components/ui/breadcrumb";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "~/components/ui/breadcrumb";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
@@ -10,61 +17,69 @@ import { format } from "date-fns";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
|
||||
function isUUID(str: string) {
|
||||
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(str);
|
||||
return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
||||
str,
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardBreadcrumbs() {
|
||||
const pathname = usePathname();
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
const segments = pathname.split("/").filter(Boolean);
|
||||
|
||||
// Find clientId if present
|
||||
let clientId: string | undefined = undefined;
|
||||
if (segments[1] === "clients" && segments[2] && isUUID(segments[2])) {
|
||||
clientId = segments[2];
|
||||
}
|
||||
const { data: client, isLoading: clientLoading } = api.clients.getById.useQuery(
|
||||
{ id: clientId ?? "" },
|
||||
{ enabled: !!clientId }
|
||||
);
|
||||
const { data: client, isLoading: clientLoading } =
|
||||
api.clients.getById.useQuery(
|
||||
{ id: clientId ?? "" },
|
||||
{ enabled: !!clientId },
|
||||
);
|
||||
|
||||
// Find invoiceId if present
|
||||
let invoiceId: string | undefined = undefined;
|
||||
if (segments[1] === "invoices" && segments[2] && isUUID(segments[2])) {
|
||||
invoiceId = segments[2];
|
||||
}
|
||||
const { data: invoice, isLoading: invoiceLoading } = api.invoices.getById.useQuery(
|
||||
{ id: invoiceId ?? "" },
|
||||
{ enabled: !!invoiceId }
|
||||
);
|
||||
const { data: invoice, isLoading: invoiceLoading } =
|
||||
api.invoices.getById.useQuery(
|
||||
{ id: invoiceId ?? "" },
|
||||
{ enabled: !!invoiceId },
|
||||
);
|
||||
|
||||
// Generate breadcrumb items based on pathname
|
||||
const breadcrumbs = React.useMemo(() => {
|
||||
const items = [];
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = segments[i];
|
||||
const path = `/${segments.slice(0, i + 1).join('/')}`;
|
||||
if (segment === 'dashboard') continue;
|
||||
|
||||
const path = `/${segments.slice(0, i + 1).join("/")}`;
|
||||
if (segment === "dashboard") continue;
|
||||
|
||||
let label: string | React.ReactElement = segment ?? "";
|
||||
if (segment === 'clients') label = 'Clients';
|
||||
if (isUUID(segment ?? "") && clientLoading) label = <Skeleton className="h-5 w-24 inline-block align-middle" />;
|
||||
if (segment === "clients") label = "Clients";
|
||||
if (isUUID(segment ?? "") && clientLoading)
|
||||
label = <Skeleton className="inline-block h-5 w-24 align-middle" />;
|
||||
else if (isUUID(segment ?? "") && client) label = client.name ?? "";
|
||||
if (isUUID(segment ?? "") && invoiceLoading) label = <Skeleton className="h-5 w-24 inline-block align-middle" />;
|
||||
if (isUUID(segment ?? "") && invoiceLoading)
|
||||
label = <Skeleton className="inline-block h-5 w-24 align-middle" />;
|
||||
else if (isUUID(segment ?? "") && invoice) {
|
||||
const issueDate = new Date(invoice.issueDate);
|
||||
label = format(issueDate, "MMM dd, yyyy");
|
||||
}
|
||||
if (segment === 'invoices') label = 'Invoices';
|
||||
if (segment === 'new') label = 'New';
|
||||
if (segment === "invoices") label = "Invoices";
|
||||
if (segment === "new") label = "New";
|
||||
// Only show 'Edit' if not the last segment
|
||||
if (segment === 'edit' && i !== segments.length - 1) label = 'Edit';
|
||||
if (segment === "edit" && i !== segments.length - 1) label = "Edit";
|
||||
// Don't show 'edit' as the last breadcrumb, just show the client name
|
||||
if (segment === 'edit' && i === segments.length - 1 && client) continue;
|
||||
if (segment === 'import') label = 'Import';
|
||||
if (segment === "edit" && i === segments.length - 1 && client) continue;
|
||||
if (segment === "import") label = "Import";
|
||||
items.push({
|
||||
label,
|
||||
href: path,
|
||||
isLast: i === segments.length - 1 || (segment === 'edit' && i === segments.length - 1 && client),
|
||||
isLast:
|
||||
i === segments.length - 1 ||
|
||||
(segment === "edit" && i === segments.length - 1 && client),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
@@ -77,7 +92,12 @@ export function DashboardBreadcrumbs() {
|
||||
<BreadcrumbList className="flex-wrap">
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href="/dashboard" className="text-sm sm:text-base">Dashboard</Link>
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="text-sm sm:text-base dark:text-gray-300"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{breadcrumbs.map((crumb) => (
|
||||
@@ -87,10 +107,17 @@ export function DashboardBreadcrumbs() {
|
||||
</BreadcrumbSeparator>
|
||||
<BreadcrumbItem>
|
||||
{crumb.isLast ? (
|
||||
<BreadcrumbPage className="text-sm sm:text-base">{crumb.label}</BreadcrumbPage>
|
||||
<BreadcrumbPage className="text-sm sm:text-base dark:text-white">
|
||||
{crumb.label}
|
||||
</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link href={crumb.href} className="text-sm sm:text-base">{crumb.label}</Link>
|
||||
<Link
|
||||
href={crumb.href}
|
||||
className="text-sm sm:text-base dark:text-gray-300"
|
||||
>
|
||||
{crumb.label}
|
||||
</Link>
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
@@ -99,4 +126,4 @@ export function DashboardBreadcrumbs() {
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ import {
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import {
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Button } from "~/components/ui/button";
|
||||
@@ -47,15 +45,19 @@ interface EditableInvoiceItemsProps {
|
||||
onRemoveItem: (index: number) => void;
|
||||
}
|
||||
|
||||
function SortableItem({
|
||||
item,
|
||||
index,
|
||||
onItemChange,
|
||||
onRemove
|
||||
}: {
|
||||
item: InvoiceItem;
|
||||
index: number;
|
||||
onItemChange: (index: number, field: string, value: any) => void;
|
||||
function SortableItem({
|
||||
item,
|
||||
index,
|
||||
onItemChange,
|
||||
onRemove,
|
||||
}: {
|
||||
item: InvoiceItem;
|
||||
index: number;
|
||||
onItemChange: (
|
||||
index: number,
|
||||
field: string,
|
||||
value: string | number | Date,
|
||||
) => void;
|
||||
onRemove: (index: number) => void;
|
||||
}) {
|
||||
const {
|
||||
@@ -72,7 +74,7 @@ function SortableItem({
|
||||
transition,
|
||||
};
|
||||
|
||||
const handleItemChange = (field: string, value: any) => {
|
||||
const handleItemChange = (field: string, value: string | number | Date) => {
|
||||
onItemChange(index, field, value);
|
||||
};
|
||||
|
||||
@@ -80,17 +82,17 @@ function SortableItem({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={`grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg hover:border-emerald-300 transition-colors ${
|
||||
className={`grid grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4 transition-colors hover:border-emerald-300 dark:border-gray-700 dark:hover:border-emerald-500 ${
|
||||
isDragging ? "opacity-50 shadow-lg" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Drag Handle */}
|
||||
<div className="col-span-1 flex items-center justify-center h-10">
|
||||
<div className="col-span-1 flex h-10 items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing rounded hover:bg-gray-100 transition-colors"
|
||||
className="cursor-grab rounded p-2 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 active:cursor-grabbing dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-400"
|
||||
>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -102,10 +104,10 @@ function SortableItem({
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-between font-normal h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 text-sm"
|
||||
className="h-10 w-full justify-between border-gray-200 text-sm font-normal focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600"
|
||||
>
|
||||
{item.date ? format(item.date, "MMM dd") : "Date"}
|
||||
<CalendarIcon className="h-4 w-4 text-gray-400" />
|
||||
<CalendarIcon className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
@@ -114,23 +116,23 @@ function SortableItem({
|
||||
selected={item.date}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(selectedDate: Date | undefined) => {
|
||||
handleItemChange("date", selectedDate || new Date())
|
||||
handleItemChange("date", selectedDate ?? new Date());
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Description */}
|
||||
<div className="col-span-4">
|
||||
<Input
|
||||
value={item.description}
|
||||
onChange={e => handleItemChange("description", e.target.value)}
|
||||
onChange={(e) => handleItemChange("description", e.target.value)}
|
||||
placeholder="Work description"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Hours */}
|
||||
<div className="col-span-1">
|
||||
<Input
|
||||
@@ -138,12 +140,12 @@ function SortableItem({
|
||||
step="0.25"
|
||||
min="0"
|
||||
value={item.hours}
|
||||
onChange={e => handleItemChange("hours", e.target.value)}
|
||||
onChange={(e) => handleItemChange("hours", e.target.value)}
|
||||
placeholder="0"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Rate */}
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
@@ -151,19 +153,19 @@ function SortableItem({
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={item.rate}
|
||||
onChange={e => handleItemChange("rate", e.target.value)}
|
||||
onChange={(e) => handleItemChange("rate", e.target.value)}
|
||||
placeholder="0.00"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Amount */}
|
||||
<div className="col-span-1">
|
||||
<div className="h-10 flex items-center px-3 border border-gray-200 rounded-md bg-gray-50 text-gray-700 font-medium">
|
||||
<div className="flex h-10 items-center rounded-md border border-gray-200 bg-gray-50 px-3 font-medium text-gray-700 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
${item.amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Remove Button */}
|
||||
<div className="col-span-1">
|
||||
<Button
|
||||
@@ -171,7 +173,7 @@ function SortableItem({
|
||||
onClick={() => onRemove(index)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 w-10 p-0 border-red-200 text-red-700 hover:bg-red-50"
|
||||
className="h-10 w-10 border-red-200 p-0 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -180,7 +182,11 @@ function SortableItem({
|
||||
);
|
||||
}
|
||||
|
||||
export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: EditableInvoiceItemsProps) {
|
||||
export function EditableInvoiceItems({
|
||||
items,
|
||||
onItemsChange,
|
||||
onRemoveItem,
|
||||
}: EditableInvoiceItemsProps) {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -191,35 +197,48 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const 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 oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over?.id);
|
||||
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
onItemsChange(newItems);
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemChange = (index: number, field: string, value: any) => {
|
||||
const handleItemChange = (
|
||||
index: number,
|
||||
field: string,
|
||||
value: string | number | Date,
|
||||
) => {
|
||||
const newItems = [...items];
|
||||
if (field === "hours" || field === "rate") {
|
||||
if (newItems[index]) {
|
||||
newItems[index][field as "hours" | "rate"] = parseFloat(value) || 0;
|
||||
const numValue =
|
||||
typeof value === "string"
|
||||
? parseFloat(value)
|
||||
: typeof value === "number"
|
||||
? value
|
||||
: 0;
|
||||
newItems[index][field] = numValue || 0;
|
||||
newItems[index].amount = newItems[index].hours * newItems[index].rate;
|
||||
}
|
||||
} else if (field === "date") {
|
||||
if (newItems[index]) {
|
||||
newItems[index][field as "date"] = value;
|
||||
const dateValue =
|
||||
value instanceof Date ? value : new Date(String(value));
|
||||
newItems[index].date = dateValue;
|
||||
}
|
||||
} else {
|
||||
if (newItems[index]) {
|
||||
newItems[index][field as "description"] = value;
|
||||
const stringValue = typeof value === "string" ? value : String(value);
|
||||
newItems[index].description = stringValue;
|
||||
}
|
||||
}
|
||||
onItemsChange(newItems);
|
||||
@@ -229,28 +248,31 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
|
||||
if (!isClient) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{items.map((item, index) => (
|
||||
<div key={item.id} className="grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg animate-pulse">
|
||||
<div className="col-span-1 flex items-center justify-center h-10">
|
||||
<div className="w-4 h-4 bg-gray-300 rounded"></div>
|
||||
{items.map((item, _index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4"
|
||||
>
|
||||
<div className="col-span-1 flex h-10 items-center justify-center">
|
||||
<div className="h-4 w-4 rounded bg-gray-300"></div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="h-10 bg-gray-300 rounded"></div>
|
||||
<div className="h-10 rounded bg-gray-300"></div>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<div className="h-10 bg-gray-300 rounded"></div>
|
||||
<div className="h-10 rounded bg-gray-300"></div>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="h-10 bg-gray-300 rounded"></div>
|
||||
<div className="h-10 rounded bg-gray-300"></div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="h-10 bg-gray-300 rounded"></div>
|
||||
<div className="h-10 rounded bg-gray-300"></div>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="h-10 bg-gray-300 rounded"></div>
|
||||
<div className="h-10 rounded bg-gray-300"></div>
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<div className="h-10 w-10 bg-gray-300 rounded"></div>
|
||||
<div className="h-10 w-10 rounded bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -264,7 +286,10 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={items.map(item => item.id)} strategy={verticalListSortingStrategy}>
|
||||
<SortableContext
|
||||
items={items.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{items.map((item, index) => (
|
||||
<SortableItem
|
||||
@@ -279,4 +304,4 @@ export function EditableInvoiceItems({ items, onItemsChange, onRemoveItem }: Edi
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,21 +11,27 @@ import { DatePicker } from "~/components/ui/date-picker";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import { SearchableSelect } from "~/components/ui/select";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/components/ui/select";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Calendar,
|
||||
FileText,
|
||||
User,
|
||||
Plus,
|
||||
Trash2,
|
||||
DollarSign,
|
||||
Clock,
|
||||
import {
|
||||
Calendar,
|
||||
FileText,
|
||||
User,
|
||||
Plus,
|
||||
Trash2,
|
||||
DollarSign,
|
||||
Clock,
|
||||
Edit3,
|
||||
Save,
|
||||
X,
|
||||
AlertCircle,
|
||||
Building
|
||||
Building,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { format } from "date-fns";
|
||||
@@ -33,10 +39,27 @@ import { FormSkeleton } from "~/components/ui/skeleton";
|
||||
import { EditableInvoiceItems } from "~/components/editable-invoice-items";
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: "draft", label: "Draft", color: "bg-gray-100 text-gray-800" },
|
||||
{ value: "sent", label: "Sent", color: "bg-blue-100 text-blue-800" },
|
||||
{ value: "paid", label: "Paid", color: "bg-green-100 text-green-800" },
|
||||
{ value: "overdue", label: "Overdue", color: "bg-red-100 text-red-800" },
|
||||
{
|
||||
value: "draft",
|
||||
label: "Draft",
|
||||
color: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
|
||||
},
|
||||
{
|
||||
value: "sent",
|
||||
label: "Sent",
|
||||
color: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
},
|
||||
{
|
||||
value: "paid",
|
||||
label: "Paid",
|
||||
color:
|
||||
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
||||
},
|
||||
{
|
||||
value: "overdue",
|
||||
label: "Overdue",
|
||||
color: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface InvoiceFormProps {
|
||||
@@ -46,7 +69,7 @@ interface InvoiceFormProps {
|
||||
export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
const router = useRouter();
|
||||
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: "",
|
||||
clientId: "",
|
||||
issueDate: new Date(),
|
||||
@@ -55,21 +78,28 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
notes: "",
|
||||
taxRate: 0,
|
||||
items: [
|
||||
{ id: crypto.randomUUID(), date: new Date(), description: "", hours: 0, rate: 0, amount: 0 },
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
date: new Date(),
|
||||
description: "",
|
||||
hours: 0,
|
||||
rate: 0,
|
||||
amount: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [defaultRate, setDefaultRate] = useState(0);
|
||||
|
||||
// Fetch clients and businesses for dropdowns
|
||||
const { data: clients, isLoading: loadingClients } = api.clients.getAll.useQuery();
|
||||
const { data: businesses, isLoading: loadingBusinesses } = api.businesses.getAll.useQuery();
|
||||
const { data: clients, isLoading: loadingClients } =
|
||||
api.clients.getAll.useQuery();
|
||||
const { data: businesses, isLoading: loadingBusinesses } =
|
||||
api.businesses.getAll.useQuery();
|
||||
|
||||
// Fetch existing invoice data if editing
|
||||
const { data: existingInvoice, isLoading: loadingInvoice } = api.invoices.getById.useQuery(
|
||||
{ id: invoiceId! },
|
||||
{ enabled: !!invoiceId }
|
||||
);
|
||||
const { data: existingInvoice, isLoading: loadingInvoice } =
|
||||
api.invoices.getById.useQuery({ id: invoiceId! }, { enabled: !!invoiceId });
|
||||
|
||||
// Populate form with existing data when editing
|
||||
React.useEffect(() => {
|
||||
@@ -83,16 +113,25 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
status: existingInvoice.status as "draft" | "sent" | "paid" | "overdue",
|
||||
notes: existingInvoice.notes ?? "",
|
||||
taxRate: existingInvoice.taxRate,
|
||||
items: existingInvoice.items?.map(item => ({
|
||||
items: existingInvoice.items?.map((item) => ({
|
||||
id: crypto.randomUUID(),
|
||||
date: new Date(item.date),
|
||||
description: item.description,
|
||||
hours: item.hours,
|
||||
rate: item.rate,
|
||||
amount: item.amount,
|
||||
})) || [{ id: crypto.randomUUID(), date: new Date(), description: "", hours: 0, rate: 0, amount: 0 }],
|
||||
})) || [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
date: new Date(),
|
||||
description: "",
|
||||
hours: 0,
|
||||
rate: 0,
|
||||
amount: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
// Set default rate from first item
|
||||
if (existingInvoice.items?.[0]) {
|
||||
setDefaultRate(existingInvoice.items[0].rate);
|
||||
@@ -102,7 +141,10 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
|
||||
// Calculate totals
|
||||
const totals = React.useMemo(() => {
|
||||
const subtotal = formData.items.reduce((sum, item) => sum + (item.hours * item.rate), 0);
|
||||
const subtotal = formData.items.reduce(
|
||||
(sum, item) => sum + item.hours * item.rate,
|
||||
0,
|
||||
);
|
||||
const taxAmount = (subtotal * formData.taxRate) / 100;
|
||||
const total = subtotal + taxAmount;
|
||||
return {
|
||||
@@ -112,15 +154,20 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
};
|
||||
}, [formData.items, formData.taxRate]);
|
||||
|
||||
|
||||
|
||||
// Add new item
|
||||
const addItem = () => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
items: [
|
||||
...prev.items,
|
||||
{ id: crypto.randomUUID(), date: new Date(), description: "", hours: 0, rate: defaultRate, amount: 0 },
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
date: new Date(),
|
||||
description: "",
|
||||
hours: 0,
|
||||
rate: defaultRate,
|
||||
amount: 0,
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
@@ -139,7 +186,7 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
const applyDefaultRate = () => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
items: prev.items.map(item => ({
|
||||
items: prev.items.map((item) => ({
|
||||
...item,
|
||||
rate: defaultRate,
|
||||
amount: item.hours * defaultRate,
|
||||
@@ -171,29 +218,29 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
// Handle form submit
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
// Validate form
|
||||
if (!formData.businessId) {
|
||||
toast.error("Please select a business");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!formData.clientId) {
|
||||
toast.error("Please select a client");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.items.some(item => !item.description.trim())) {
|
||||
|
||||
if (formData.items.some((item) => !item.description.trim())) {
|
||||
toast.error("Please fill in all item descriptions");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.items.some(item => item.hours <= 0)) {
|
||||
|
||||
if (formData.items.some((item) => item.hours <= 0)) {
|
||||
toast.error("Please enter valid hours for all items");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.items.some(item => item.rate <= 0)) {
|
||||
|
||||
if (formData.items.some((item) => item.rate <= 0)) {
|
||||
toast.error("Please enter valid rates for all items");
|
||||
return;
|
||||
}
|
||||
@@ -231,16 +278,16 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
return (
|
||||
<div className="space-y-6 pb-20">
|
||||
{/* Invoice Details Card Skeleton */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<div className="h-6 bg-gray-300 rounded w-48 animate-pulse"></div>
|
||||
<div className="h-6 w-48 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6 xl:grid-cols-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="h-4 bg-gray-300 rounded w-24 animate-pulse"></div>
|
||||
<div className="h-10 bg-gray-300 rounded animate-pulse"></div>
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="h-10 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -248,27 +295,36 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</Card>
|
||||
|
||||
{/* Invoice Items Card Skeleton */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-6 bg-gray-300 rounded w-32 animate-pulse"></div>
|
||||
<div className="h-10 bg-gray-300 rounded w-24 animate-pulse"></div>
|
||||
<div className="h-6 w-32 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="h-10 w-24 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Items Table Header Skeleton */}
|
||||
<div className="grid grid-cols-12 gap-2 px-4 py-3 bg-gray-50 rounded-lg">
|
||||
<div className="grid grid-cols-12 gap-2 rounded-lg bg-gray-50 px-4 py-3 dark:bg-gray-700">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="h-4 bg-gray-300 rounded animate-pulse"></div>
|
||||
<div
|
||||
key={i}
|
||||
className="h-4 animate-pulse rounded bg-gray-300 dark:bg-gray-600"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Items Skeleton */}
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg animate-pulse">
|
||||
<div
|
||||
key={i}
|
||||
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4 dark:border-gray-700"
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, j) => (
|
||||
<div key={j} className="h-10 bg-gray-300 rounded"></div>
|
||||
<div
|
||||
key={j}
|
||||
className="h-10 rounded bg-gray-300 dark:bg-gray-600"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
@@ -278,12 +334,12 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
|
||||
{/* Form Controls Bar Skeleton */}
|
||||
<div className="mt-6">
|
||||
<div className="bg-white/90 rounded-2xl border border-gray-200 shadow-sm p-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-4 bg-gray-300 rounded w-32 animate-pulse"></div>
|
||||
<div className="h-4 w-32 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 bg-gray-300 rounded w-20 animate-pulse"></div>
|
||||
<div className="h-10 bg-gray-300 rounded w-32 animate-pulse"></div>
|
||||
<div className="h-10 w-20 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
<div className="h-10 w-32 animate-pulse rounded bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -292,24 +348,26 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const selectedClient = clients?.find(c => c.id === formData.clientId);
|
||||
const selectedBusiness = businesses?.find(b => b.id === formData.businessId);
|
||||
const selectedClient = clients?.find((c) => c.id === formData.clientId);
|
||||
const selectedBusiness = businesses?.find(
|
||||
(b) => b.id === formData.businessId,
|
||||
);
|
||||
|
||||
// Show loading state while fetching clients
|
||||
if (loadingClients) {
|
||||
return (
|
||||
<div className="space-y-6 pb-20">
|
||||
{/* Invoice Details Card Skeleton */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<CardHeader>
|
||||
<div className="h-6 bg-gray-300 rounded w-48 animate-pulse"></div>
|
||||
<div className="h-6 w-48 animate-pulse rounded bg-gray-300"></div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6 xl:grid-cols-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="h-4 bg-gray-300 rounded w-24 animate-pulse"></div>
|
||||
<div className="h-10 bg-gray-300 rounded animate-pulse"></div>
|
||||
<div className="h-4 w-24 animate-pulse rounded bg-gray-300"></div>
|
||||
<div className="h-10 animate-pulse rounded bg-gray-300"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -317,27 +375,33 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</Card>
|
||||
|
||||
{/* Invoice Items Card Skeleton */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-6 bg-gray-300 rounded w-32 animate-pulse"></div>
|
||||
<div className="h-10 bg-gray-300 rounded w-24 animate-pulse"></div>
|
||||
<div className="h-6 w-32 animate-pulse rounded bg-gray-300"></div>
|
||||
<div className="h-10 w-24 animate-pulse rounded bg-gray-300"></div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Items Table Header Skeleton */}
|
||||
<div className="grid grid-cols-12 gap-2 px-4 py-3 bg-gray-50 rounded-lg">
|
||||
<div className="grid grid-cols-12 gap-2 rounded-lg bg-gray-50 px-4 py-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="h-4 bg-gray-300 rounded animate-pulse"></div>
|
||||
<div
|
||||
key={i}
|
||||
className="h-4 animate-pulse rounded bg-gray-300"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Items Skeleton */}
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="grid grid-cols-12 gap-2 items-center p-4 border border-gray-200 rounded-lg animate-pulse">
|
||||
<div
|
||||
key={i}
|
||||
className="grid animate-pulse grid-cols-12 items-center gap-2 rounded-lg border border-gray-200 p-4"
|
||||
>
|
||||
{Array.from({ length: 8 }).map((_, j) => (
|
||||
<div key={j} className="h-10 bg-gray-300 rounded"></div>
|
||||
<div key={j} className="h-10 rounded bg-gray-300"></div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
@@ -347,12 +411,12 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
|
||||
{/* Form Controls Bar Skeleton */}
|
||||
<div className="mt-6">
|
||||
<div className="bg-white/90 rounded-2xl border border-gray-200 shadow-sm p-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-4 bg-gray-300 rounded w-32 animate-pulse"></div>
|
||||
<div className="h-4 w-32 animate-pulse rounded bg-gray-300"></div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 bg-gray-300 rounded w-20 animate-pulse"></div>
|
||||
<div className="h-10 bg-gray-300 rounded w-32 animate-pulse"></div>
|
||||
<div className="h-10 w-20 animate-pulse rounded bg-gray-300"></div>
|
||||
<div className="h-10 w-32 animate-pulse rounded bg-gray-300"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,65 +428,96 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
return (
|
||||
<form id="invoice-form" onSubmit={handleSubmit} className="space-y-6 pb-20">
|
||||
{/* Invoice Details Card */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700">
|
||||
<FileText className="h-5 w-5" />
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<FileText className="h-5 w-5" />
|
||||
Invoice Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invoiceNumber" className="text-sm font-medium text-gray-700">
|
||||
Invoice Number
|
||||
</Label>
|
||||
<Input
|
||||
id="invoiceNumber"
|
||||
value={formData.invoiceNumber}
|
||||
className="h-10 border-gray-200 bg-gray-50"
|
||||
placeholder="Auto-generated"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="businessId" className="text-sm font-medium text-gray-700">
|
||||
Business *
|
||||
</Label>
|
||||
<SearchableSelect
|
||||
value={formData.businessId}
|
||||
onValueChange={(value) => setFormData(f => ({ ...f, businessId: value }))}
|
||||
options={businesses?.map(business => ({ value: business.id, label: business.name })) ?? []}
|
||||
placeholder="Select a business"
|
||||
searchPlaceholder="Search businesses..."
|
||||
disabled={loadingBusinesses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientId" className="text-sm font-medium text-gray-700">
|
||||
Client *
|
||||
</Label>
|
||||
<SearchableSelect
|
||||
value={formData.clientId}
|
||||
onValueChange={(value) => setFormData(f => ({ ...f, clientId: value }))}
|
||||
options={clients?.map(client => ({ value: client.id, label: client.name })) ?? []}
|
||||
placeholder="Select a client"
|
||||
searchPlaceholder="Search clients..."
|
||||
disabled={loadingClients}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 lg:gap-6 xl:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="invoiceNumber"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Invoice Number
|
||||
</Label>
|
||||
<Input
|
||||
id="invoiceNumber"
|
||||
value={formData.invoiceNumber}
|
||||
className="h-10 border-gray-200 bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Auto-generated"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="status" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="businessId"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Business *
|
||||
</Label>
|
||||
<SearchableSelect
|
||||
value={formData.businessId}
|
||||
onValueChange={(value) =>
|
||||
setFormData((f) => ({ ...f, businessId: value }))
|
||||
}
|
||||
options={
|
||||
businesses?.map((business) => ({
|
||||
value: business.id,
|
||||
label: business.name,
|
||||
})) ?? []
|
||||
}
|
||||
placeholder="Select a business"
|
||||
searchPlaceholder="Search businesses..."
|
||||
disabled={loadingBusinesses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="clientId"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Client *
|
||||
</Label>
|
||||
<SearchableSelect
|
||||
value={formData.clientId}
|
||||
onValueChange={(value) =>
|
||||
setFormData((f) => ({ ...f, clientId: value }))
|
||||
}
|
||||
options={
|
||||
clients?.map((client) => ({
|
||||
value: client.id,
|
||||
label: client.name,
|
||||
})) ?? []
|
||||
}
|
||||
placeholder="Select a client"
|
||||
searchPlaceholder="Search clients..."
|
||||
disabled={loadingClients}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="status"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Status
|
||||
</Label>
|
||||
<Select
|
||||
value={formData.status}
|
||||
onValueChange={(value) => setFormData(f => ({ ...f, status: value as "draft" | "sent" | "paid" | "overdue" }))}
|
||||
onValueChange={(value) =>
|
||||
setFormData((f) => ({
|
||||
...f,
|
||||
status: value as "draft" | "sent" | "paid" | "overdue",
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-10 border-gray-200 bg-gray-50">
|
||||
<SelectTrigger className="h-10 border-gray-200 bg-gray-50 dark:border-gray-600 dark:bg-gray-700">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -434,90 +529,119 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issueDate" className="text-sm font-medium text-gray-700">
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="issueDate"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Issue Date *
|
||||
</Label>
|
||||
<DatePicker
|
||||
date={formData.issueDate}
|
||||
onDateChange={date => setFormData(f => ({ ...f, issueDate: date ?? new Date() }))}
|
||||
placeholder="Select issue date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<DatePicker
|
||||
date={formData.issueDate}
|
||||
onDateChange={(date) =>
|
||||
setFormData((f) => ({ ...f, issueDate: date ?? new Date() }))
|
||||
}
|
||||
placeholder="Select issue date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dueDate" className="text-sm font-medium text-gray-700">
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="dueDate"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Due Date *
|
||||
</Label>
|
||||
<DatePicker
|
||||
date={formData.dueDate}
|
||||
onDateChange={date => setFormData(f => ({ ...f, dueDate: date ?? new Date() }))}
|
||||
placeholder="Select due date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
<DatePicker
|
||||
date={formData.dueDate}
|
||||
onDateChange={(date) =>
|
||||
setFormData((f) => ({ ...f, dueDate: date ?? new Date() }))
|
||||
}
|
||||
placeholder="Select due date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="defaultRate" className="text-sm font-medium text-gray-700">
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="defaultRate"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Default Rate ($/hr)
|
||||
</Label>
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="defaultRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={defaultRate}
|
||||
onChange={e => setDefaultRate(parseFloat(e.target.value) || 0)}
|
||||
onChange={(e) =>
|
||||
setDefaultRate(parseFloat(e.target.value) || 0)
|
||||
}
|
||||
placeholder="0.00"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={applyDefaultRate}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-10 border-emerald-200 text-emerald-700 hover:bg-emerald-50"
|
||||
className="h-10 border-emerald-200 text-emerald-700 hover:bg-emerald-50 dark:border-emerald-800 dark:text-emerald-400 dark:hover:bg-emerald-900/20"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="taxRate" className="text-sm font-medium text-gray-700">
|
||||
Tax Rate (%)
|
||||
</Label>
|
||||
<Input
|
||||
id="taxRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="100"
|
||||
value={formData.taxRate}
|
||||
onChange={e => setFormData(f => ({ ...f, taxRate: parseFloat(e.target.value) || 0 }))}
|
||||
placeholder="0.00"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label
|
||||
htmlFor="taxRate"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Tax Rate (%)
|
||||
</Label>
|
||||
<Input
|
||||
id="taxRate"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="100"
|
||||
value={formData.taxRate}
|
||||
onChange={(e) =>
|
||||
setFormData((f) => ({
|
||||
...f,
|
||||
taxRate: parseFloat(e.target.value) || 0,
|
||||
}))
|
||||
}
|
||||
placeholder="0.00"
|
||||
className="h-10 border-gray-200 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedBusiness && (
|
||||
<div className="p-4 bg-emerald-50 rounded-lg border border-emerald-200">
|
||||
<div className="flex items-center gap-2 text-emerald-700 mb-2">
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-800 dark:bg-emerald-900/20">
|
||||
<div className="mb-2 flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<Building className="h-4 w-4" />
|
||||
<span className="font-medium">Business Information</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="font-medium">{selectedBusiness.name}</p>
|
||||
{selectedBusiness.email && <p>{selectedBusiness.email}</p>}
|
||||
{selectedBusiness.phone && <p>{selectedBusiness.phone}</p>}
|
||||
{selectedBusiness.addressLine1 && (
|
||||
<p>{selectedBusiness.addressLine1}</p>
|
||||
)}
|
||||
{(selectedBusiness.city ?? selectedBusiness.state ?? selectedBusiness.postalCode) && (
|
||||
{(selectedBusiness.city ??
|
||||
selectedBusiness.state ??
|
||||
selectedBusiness.postalCode) && (
|
||||
<p>
|
||||
{[selectedBusiness.city, selectedBusiness.state, selectedBusiness.postalCode]
|
||||
{[
|
||||
selectedBusiness.city,
|
||||
selectedBusiness.state,
|
||||
selectedBusiness.postalCode,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
</p>
|
||||
@@ -527,12 +651,12 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
)}
|
||||
|
||||
{selectedClient && (
|
||||
<div className="p-4 bg-emerald-50 rounded-lg border border-emerald-200">
|
||||
<div className="flex items-center gap-2 text-emerald-700 mb-2">
|
||||
<div className="rounded-lg border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-800 dark:bg-emerald-900/20">
|
||||
<div className="mb-2 flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<User className="h-4 w-4" />
|
||||
<span className="font-medium">Client Information</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
<p className="font-medium">{selectedClient.name}</p>
|
||||
{selectedClient.email && <p>{selectedClient.email}</p>}
|
||||
{selectedClient.phone && <p>{selectedClient.phone}</p>}
|
||||
@@ -541,25 +665,30 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="notes" className="text-sm font-medium text-gray-700">
|
||||
<Label
|
||||
htmlFor="notes"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Notes
|
||||
</Label>
|
||||
<textarea
|
||||
id="notes"
|
||||
value={formData.notes}
|
||||
onChange={e => setFormData(f => ({ ...f, notes: e.target.value }))}
|
||||
className="w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-gray-700 focus:border-emerald-500 focus:ring-emerald-500 min-h-[80px] resize-none"
|
||||
onChange={(e) =>
|
||||
setFormData((f) => ({ ...f, notes: e.target.value }))
|
||||
}
|
||||
className="min-h-[80px] w-full resize-none rounded-md border border-gray-200 bg-white px-3 py-2 text-gray-700 focus:border-emerald-500 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
placeholder="Additional notes, terms, or special instructions..."
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice Items Card */}
|
||||
<Card className="shadow-xl border-0 bg-white/80 backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700">
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<Clock className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
@@ -572,11 +701,11 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Item
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Items Table Header */}
|
||||
<div className="grid grid-cols-12 gap-2 px-4 py-3 bg-gray-50 rounded-lg font-medium text-sm text-gray-700 items-center">
|
||||
<div className="grid grid-cols-12 items-center gap-2 rounded-lg bg-gray-50 px-4 py-3 text-sm font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
||||
<div className="col-span-1 text-center">⋮⋮</div>
|
||||
<div className="col-span-2">Date</div>
|
||||
<div className="col-span-4">Description</div>
|
||||
@@ -584,55 +713,64 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
<div className="col-span-2">Rate ($)</div>
|
||||
<div className="col-span-1">Amount</div>
|
||||
<div className="col-span-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<EditableInvoiceItems
|
||||
items={formData.items}
|
||||
onItemsChange={(newItems) => setFormData(prev => ({ ...prev, items: newItems }))}
|
||||
onItemsChange={(newItems) =>
|
||||
setFormData((prev) => ({ ...prev, items: newItems }))
|
||||
}
|
||||
onRemoveItem={removeItem}
|
||||
/>
|
||||
|
||||
{/* Validation Messages */}
|
||||
{formData.items.some(item => !item.description.trim()) && (
|
||||
<div className="flex items-center gap-2 text-amber-600 text-sm">
|
||||
{formData.items.some((item) => !item.description.trim()) && (
|
||||
<div className="flex items-center gap-2 text-sm text-amber-600">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
Please fill in all item descriptions
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.items.some(item => item.hours <= 0) && (
|
||||
<div className="flex items-center gap-2 text-amber-600 text-sm">
|
||||
|
||||
{formData.items.some((item) => item.hours <= 0) && (
|
||||
<div className="flex items-center gap-2 text-sm text-amber-600">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
Please enter valid hours for all items
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.items.some(item => item.rate <= 0) && (
|
||||
<div className="flex items-center gap-2 text-amber-600 text-sm">
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.items.some((item) => item.rate <= 0) && (
|
||||
<div className="flex items-center gap-2 text-sm text-amber-600">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
Please enter valid rates for all items
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Totals */}
|
||||
<div className="flex justify-end">
|
||||
<div className="text-right space-y-2">
|
||||
<div className="space-y-2 text-right">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-gray-600">Subtotal: ${totals.subtotal.toFixed(2)}</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Subtotal: ${totals.subtotal.toFixed(2)}
|
||||
</div>
|
||||
{formData.taxRate > 0 && (
|
||||
<div className="text-sm text-gray-600">
|
||||
Tax ({formData.taxRate}%): ${totals.taxAmount.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-lg font-medium text-gray-700">Total Amount</div>
|
||||
<div className="text-3xl font-bold text-emerald-600">${totals.total.toFixed(2)}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{formData.items.length} item{formData.items.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
<div className="text-lg font-medium text-gray-700 dark:text-gray-300">
|
||||
Total Amount
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
|
||||
${totals.total.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{formData.items.length} item
|
||||
{formData.items.length !== 1 ? "s" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -640,34 +778,37 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
|
||||
{/* Form Controls Bar */}
|
||||
<div className="mt-6">
|
||||
<div className="bg-white/90 rounded-2xl border border-gray-200 shadow-sm p-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-emerald-500 rounded-full"></div>
|
||||
<div className="h-2 w-2 rounded-full bg-emerald-500"></div>
|
||||
<span>Ready to save</span>
|
||||
</div>
|
||||
{formData.items.length > 0 && (
|
||||
<span className="text-gray-400">•</span>
|
||||
<span className="text-gray-400 dark:text-gray-500">•</span>
|
||||
)}
|
||||
{formData.items.length > 0 && (
|
||||
<span>{formData.items.length} item{formData.items.length !== 1 ? 's' : ''}</span>
|
||||
<span>
|
||||
{formData.items.length} item
|
||||
{formData.items.length !== 1 ? "s" : ""}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => router.push("/dashboard/invoices")}
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50 font-medium"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
className="border-gray-300 font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-medium shadow-lg hover:shadow-xl transition-all duration-200"
|
||||
className="bg-gradient-to-r from-emerald-600 to-teal-600 font-medium text-white shadow-lg transition-all duration-200 hover:from-emerald-700 hover:to-teal-700 hover:shadow-xl"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
@@ -685,6 +826,6 @@ export function InvoiceForm({ invoiceId }: InvoiceFormProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,16 +42,25 @@ interface InvoiceViewProps {
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
draft: { label: "Draft", color: "bg-gray-100 text-gray-800", icon: FileText },
|
||||
sent: { label: "Sent", color: "bg-blue-100 text-blue-800", icon: Send },
|
||||
draft: {
|
||||
label: "Draft",
|
||||
color: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
|
||||
icon: FileText,
|
||||
},
|
||||
sent: {
|
||||
label: "Sent",
|
||||
color: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
icon: Send,
|
||||
},
|
||||
paid: {
|
||||
label: "Paid",
|
||||
color: "bg-green-100 text-green-800",
|
||||
color:
|
||||
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
||||
icon: DollarSign,
|
||||
},
|
||||
overdue: {
|
||||
label: "Overdue",
|
||||
color: "bg-red-100 text-red-800",
|
||||
color: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
||||
icon: AlertCircle,
|
||||
},
|
||||
} as const;
|
||||
@@ -144,10 +153,10 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
return (
|
||||
<div className="py-12 text-center">
|
||||
<FileText className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
||||
<h3 className="mb-2 text-lg font-medium text-gray-900">
|
||||
<h3 className="mb-2 text-lg font-medium text-gray-900 dark:text-white">
|
||||
Invoice not found
|
||||
</h3>
|
||||
<p className="mb-4 text-gray-500">
|
||||
<p className="mb-4 text-gray-500 dark:text-gray-400">
|
||||
The invoice you're looking for doesn't exist or has been
|
||||
deleted.
|
||||
</p>
|
||||
@@ -165,9 +174,9 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<div className="space-y-6">
|
||||
{/* Status Alert */}
|
||||
{isOverdue && (
|
||||
<Card className="border-red-200 bg-red-50">
|
||||
<Card className="border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 text-red-700">
|
||||
<div className="flex items-center gap-2 text-red-700 dark:text-red-400">
|
||||
<AlertCircle className="h-5 w-5" />
|
||||
<span className="font-medium">This invoice is overdue</span>
|
||||
</div>
|
||||
@@ -179,32 +188,38 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
{/* Main Content */}
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* Invoice Header Card */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardContent>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-emerald-100 p-2">
|
||||
<FileText className="h-6 w-6 text-emerald-600" />
|
||||
<div className="rounded-lg bg-emerald-100 p-2 dark:bg-emerald-900/30">
|
||||
<FileText className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{invoice.invoiceNumber}
|
||||
</h2>
|
||||
<p className="text-gray-600">Professional Invoice</p>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Professional Invoice
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Issue Date</span>
|
||||
<p className="font-medium text-gray-900">
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
Issue Date
|
||||
</span>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{formatDate(invoice.issueDate)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">Due Date</span>
|
||||
<p className="font-medium text-gray-900">
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
Due Date
|
||||
</span>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{formatDate(invoice.dueDate)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -221,7 +236,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
.label
|
||||
}
|
||||
</Badge>
|
||||
<div className="text-3xl font-bold text-emerald-600">
|
||||
<div className="text-3xl font-bold text-emerald-600 dark:text-emerald-400">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</div>
|
||||
<Button
|
||||
@@ -247,38 +262,38 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Client Information */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700">
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<User className="h-5 w-5" />
|
||||
Bill To
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{invoice.client?.name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 text-sm md:grid-cols-2">
|
||||
{invoice.client?.email && (
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Mail className="h-4 w-4 text-gray-400" />
|
||||
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
|
||||
<Mail className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
{invoice.client.email}
|
||||
</div>
|
||||
)}
|
||||
{invoice.client?.phone && (
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<Phone className="h-4 w-4 text-gray-400" />
|
||||
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-300">
|
||||
<Phone className="h-4 w-4 text-gray-400 dark:text-gray-500" />
|
||||
{invoice.client.phone}
|
||||
</div>
|
||||
)}
|
||||
{(invoice.client?.addressLine1 ??
|
||||
invoice.client?.city ??
|
||||
invoice.client?.state) && (
|
||||
<div className="flex items-start gap-2 text-gray-600 md:col-span-2">
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400" />
|
||||
<div className="flex items-start gap-2 text-gray-600 md:col-span-2 dark:text-gray-300">
|
||||
<MapPin className="mt-0.5 h-4 w-4 flex-shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<div>
|
||||
{invoice.client?.addressLine1 && (
|
||||
<div>{invoice.client.addressLine1}</div>
|
||||
@@ -310,31 +325,31 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Invoice Items */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700">
|
||||
<CardTitle className="flex items-center gap-2 text-emerald-700 dark:text-emerald-400">
|
||||
<Clock className="h-5 w-5" />
|
||||
Invoice Items
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700">
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Hours
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Rate
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700">
|
||||
<th className="px-4 py-3 text-right text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
Amount
|
||||
</th>
|
||||
</tr>
|
||||
@@ -343,21 +358,21 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
{invoice.items?.map((item, index) => (
|
||||
<tr
|
||||
key={item.id || index}
|
||||
className="border-t border-gray-100 hover:bg-gray-50"
|
||||
className="border-t border-gray-100 hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700"
|
||||
>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
|
||||
{formatDate(item.date)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-sm text-gray-900 dark:text-gray-300">
|
||||
{item.description}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
|
||||
{item.hours}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm text-gray-900 dark:text-gray-300">
|
||||
{formatCurrency(item.rate)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900">
|
||||
<td className="px-4 py-3 text-right text-sm font-medium text-gray-900 dark:text-gray-300">
|
||||
{formatCurrency(item.amount)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -370,12 +385,14 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
{/* Notes */}
|
||||
{invoice.notes && (
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-700">Notes</CardTitle>
|
||||
<CardTitle className="text-emerald-700 dark:text-emerald-400">
|
||||
Notes
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="whitespace-pre-wrap text-gray-700">
|
||||
<p className="whitespace-pre-wrap text-gray-700 dark:text-gray-300">
|
||||
{invoice.notes}
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -386,9 +403,11 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Status Actions */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-700">Status Actions</CardTitle>
|
||||
<CardTitle className="text-emerald-700 dark:text-emerald-400">
|
||||
Status Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{invoice.status === "draft" && (
|
||||
@@ -426,41 +445,47 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
{invoice.status === "paid" && (
|
||||
<div className="py-4 text-center">
|
||||
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600" />
|
||||
<p className="font-medium text-green-600">Invoice Paid</p>
|
||||
<DollarSign className="mx-auto mb-2 h-8 w-8 text-green-600 dark:text-green-400" />
|
||||
<p className="font-medium text-green-600 dark:text-green-400">
|
||||
Invoice Paid
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice Summary */}
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<Card className="border-0 bg-white/80 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-emerald-700">Summary</CardTitle>
|
||||
<CardTitle className="text-emerald-700 dark:text-emerald-400">
|
||||
Summary
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Subtotal</span>
|
||||
<span className="font-medium">
|
||||
<span className="text-gray-600 dark:text-gray-300">
|
||||
Subtotal
|
||||
</span>
|
||||
<span className="font-medium dark:text-white">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-600">Tax</span>
|
||||
<span className="font-medium">$0.00</span>
|
||||
<span className="text-gray-600 dark:text-gray-300">Tax</span>
|
||||
<span className="font-medium dark:text-white">$0.00</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total</span>
|
||||
<span className="text-emerald-600">
|
||||
<span className="dark:text-white">Total</span>
|
||||
<span className="text-emerald-600 dark:text-emerald-400">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-4 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
<div className="border-t border-gray-200 pt-4 text-center dark:border-gray-700">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{invoice.items?.length ?? 0} item
|
||||
{invoice.items?.length !== 1 ? "s" : ""}
|
||||
</p>
|
||||
@@ -469,15 +494,17 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="border-0 border-red-200 bg-white/80 shadow-xl backdrop-blur-sm">
|
||||
<Card className="border-0 border-red-200 bg-white/80 shadow-xl backdrop-blur-sm dark:border-red-800 dark:bg-gray-800/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-700">Danger Zone</CardTitle>
|
||||
<CardTitle className="text-red-700 dark:text-red-400">
|
||||
Danger Zone
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
variant="outline"
|
||||
className="w-full border-red-200 text-red-700 hover:bg-red-50"
|
||||
className="w-full border-red-200 text-red-700 hover:bg-red-50 dark:border-red-800 dark:text-red-400 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete Invoice
|
||||
@@ -489,12 +516,12 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm">
|
||||
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm dark:bg-gray-800/95">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-gray-800">
|
||||
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
|
||||
Delete Invoice
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-600">
|
||||
<DialogDescription className="text-gray-600 dark:text-gray-300">
|
||||
Are you sure you want to delete this invoice? This action cannot
|
||||
be undone and will permanently remove the invoice and all its
|
||||
data.
|
||||
@@ -504,7 +531,7 @@ export function InvoiceView({ invoiceId }: InvoiceViewProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, Search } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
@@ -30,15 +35,15 @@ function SelectTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
size?: "sm" | "default";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 h-10 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-emerald-500 focus-visible:ring-emerald-500 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground",
|
||||
className
|
||||
"data-[placeholder]:text-muted-foreground flex h-10 w-full items-center justify-between gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-emerald-500 focus-visible:ring-[3px] focus-visible:ring-emerald-500 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -47,7 +52,7 @@ function SelectTrigger({
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
@@ -64,7 +69,7 @@ function SelectContent({
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
@@ -74,7 +79,7 @@ function SelectContent({
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -82,7 +87,7 @@ function SelectContent({
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
@@ -95,7 +100,7 @@ function SelectLabel({
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
@@ -108,7 +113,7 @@ function SelectItem({
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -119,7 +124,7 @@ function SelectItem({
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
@@ -132,7 +137,7 @@ function SelectSeparator({
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
@@ -144,13 +149,13 @@ function SelectScrollUpButton({
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
@@ -162,13 +167,13 @@ function SelectScrollDownButton({
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Enhanced SelectContent with search functionality
|
||||
@@ -208,7 +213,7 @@ function SelectContentWithSearch({
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-hidden rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
onEscapeKeyDown={(e) => {
|
||||
@@ -226,11 +231,11 @@ function SelectContentWithSearch({
|
||||
{...props}
|
||||
>
|
||||
{onSearchChange && (
|
||||
<div className="flex items-center px-3 py-2 border-b">
|
||||
<div className="border-border flex items-center border-b px-3 py-2">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
className="flex h-8 w-full rounded-md bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 border-0 focus:ring-0 focus:outline-none"
|
||||
className="placeholder:text-muted-foreground text-foreground flex h-8 w-full rounded-md border-0 bg-transparent py-2 text-sm outline-none focus:ring-0 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
@@ -240,7 +245,11 @@ function SelectContentWithSearch({
|
||||
e.stopPropagation();
|
||||
}
|
||||
// Prevent arrow keys from moving focus away from search
|
||||
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
||||
if (
|
||||
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(
|
||||
e.key,
|
||||
)
|
||||
) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
@@ -255,7 +264,9 @@ function SelectContentWithSearch({
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{filteredOptions && filteredOptions.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground select-none">No results found</div>
|
||||
<div className="text-muted-foreground px-3 py-2 text-sm select-none">
|
||||
No results found
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
@@ -263,7 +274,7 @@ function SelectContentWithSearch({
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Searchable Select component
|
||||
@@ -284,21 +295,21 @@ function SearchableSelect({
|
||||
options,
|
||||
searchPlaceholder = "Search...",
|
||||
className,
|
||||
disabled
|
||||
disabled,
|
||||
}: SearchableSelectProps) {
|
||||
const [searchValue, setSearchValue] = React.useState("");
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
if (!searchValue) return options;
|
||||
return options.filter(option =>
|
||||
option.label.toLowerCase().includes(searchValue.toLowerCase())
|
||||
return options.filter((option) =>
|
||||
option.label.toLowerCase().includes(searchValue.toLowerCase()),
|
||||
);
|
||||
}, [options, searchValue]);
|
||||
|
||||
// Convert empty string to placeholder value for display
|
||||
const displayValue = value === "" ? "__placeholder__" : value;
|
||||
|
||||
|
||||
// Convert placeholder value back to empty string when selected
|
||||
const handleValueChange = (newValue: string) => {
|
||||
const actualValue = newValue === "__placeholder__" ? "" : newValue;
|
||||
@@ -309,9 +320,9 @@ function SearchableSelect({
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={displayValue}
|
||||
onValueChange={handleValueChange}
|
||||
<Select
|
||||
value={displayValue}
|
||||
onValueChange={handleValueChange}
|
||||
disabled={disabled}
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
@@ -353,4 +364,4 @@ export {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SearchableSelect,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "~/lib/utils"
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
@@ -7,37 +7,43 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className={cn("bg-muted animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Dashboard skeleton components
|
||||
export function DashboardStatsSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="mb-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"
|
||||
>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-16 mb-2" />
|
||||
<Skeleton className="mb-2 h-8 w-16" />
|
||||
<Skeleton className="h-3 w-32" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardCardsSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
<div className="mb-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<div key={i} className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80"
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</div>
|
||||
<Skeleton className="h-4 w-full mb-4" />
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<div className="flex gap-3">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-32" />
|
||||
@@ -45,20 +51,20 @@ export function DashboardCardsSkeleton() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardActivitySkeleton() {
|
||||
return (
|
||||
<div className="shadow-xl border-0 bg-white/80 backdrop-blur-sm rounded-xl p-6">
|
||||
<Skeleton className="h-6 w-32 mb-6" />
|
||||
<div className="text-center py-12">
|
||||
<Skeleton className="h-20 w-20 rounded-full mx-auto mb-4" />
|
||||
<Skeleton className="h-6 w-48 mx-auto mb-2" />
|
||||
<Skeleton className="h-4 w-64 mx-auto" />
|
||||
<div className="rounded-xl border-0 bg-white/80 p-6 shadow-xl backdrop-blur-sm dark:bg-gray-800/80">
|
||||
<Skeleton className="mb-6 h-6 w-32" />
|
||||
<div className="py-12 text-center">
|
||||
<Skeleton className="mx-auto mb-4 h-20 w-20 rounded-full" />
|
||||
<Skeleton className="mx-auto mb-2 h-6 w-48" />
|
||||
<Skeleton className="mx-auto h-4 w-64" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Table skeleton components
|
||||
@@ -66,17 +72,17 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search and filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<Skeleton className="h-10 w-64" />
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Table */}
|
||||
<div className="border rounded-lg">
|
||||
<div className="p-4 border-b">
|
||||
<div className="rounded-lg border">
|
||||
<div className="border-b p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<div className="flex gap-2">
|
||||
@@ -85,7 +91,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="p-4">
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
@@ -101,7 +107,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
@@ -112,7 +118,7 @@ export function TableSkeleton({ rows = 5 }: { rows?: number }) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Form skeleton components
|
||||
@@ -121,36 +127,36 @@ export function FormSkeleton() {
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Skeleton className="h-4 w-20 mb-2" />
|
||||
<Skeleton className="mb-2 h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="h-4 w-24 mb-2" />
|
||||
<Skeleton className="mb-2 h-4 w-24" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="h-4 w-16 mb-2" />
|
||||
<Skeleton className="mb-2 h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Skeleton className="h-4 w-20 mb-2" />
|
||||
<Skeleton className="mb-2 h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<Skeleton className="h-4 w-16 mb-2" />
|
||||
<Skeleton className="mb-2 h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Skeleton className="h-10 w-24" />
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Invoice view skeleton
|
||||
@@ -158,16 +164,16 @@ export function InvoiceViewSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<Skeleton className="h-10 w-32" />
|
||||
</div>
|
||||
|
||||
|
||||
{/* Client info */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
@@ -180,10 +186,10 @@ export function InvoiceViewSkeleton() {
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Items table */}
|
||||
<div className="border rounded-lg">
|
||||
<div className="p-4 border-b">
|
||||
<div className="rounded-lg border">
|
||||
<div className="border-b p-4">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</div>
|
||||
<div className="p-4">
|
||||
@@ -200,7 +206,7 @@ export function InvoiceViewSkeleton() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Total */}
|
||||
<div className="flex justify-end">
|
||||
<div className="space-y-2">
|
||||
@@ -209,7 +215,7 @@ export function InvoiceViewSkeleton() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
||||
@@ -104,10 +104,10 @@ interface Business {
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
draft: "bg-gray-100 text-gray-800",
|
||||
sent: "bg-blue-100 text-blue-800",
|
||||
paid: "bg-green-100 text-green-800",
|
||||
overdue: "bg-red-100 text-red-800",
|
||||
draft: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",
|
||||
sent: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
|
||||
paid: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
||||
overdue: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
|
||||
} as const;
|
||||
|
||||
const statusLabels = {
|
||||
@@ -503,7 +503,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -515,10 +515,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
)}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
|
||||
Email
|
||||
</TableHead>
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
|
||||
Phone
|
||||
</TableHead>
|
||||
<TableHead className="w-8 px-4 py-4"></TableHead>
|
||||
@@ -536,7 +536,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
onClick={() => handleSort("invoiceNumber")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -549,7 +549,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
onClick={() => handleSort("client.name")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -562,7 +562,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
onClick={() => handleSort("status")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -575,7 +575,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
onClick={() => handleSort("totalAmount")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -588,7 +588,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
onClick={() => handleSort("dueDate")}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -615,21 +615,21 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50"
|
||||
className="cursor-pointer px-4 py-4 text-base font-semibold text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
onClick={() => handleSort("name")}
|
||||
>
|
||||
Name
|
||||
</TableHead>
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
|
||||
Email
|
||||
</TableHead>
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
|
||||
Phone
|
||||
</TableHead>
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
|
||||
Website
|
||||
</TableHead>
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700">
|
||||
<TableHead className="px-4 py-4 text-base font-semibold text-gray-700 dark:text-gray-300">
|
||||
Default
|
||||
</TableHead>
|
||||
<TableHead className="w-8 px-4 py-4"></TableHead>
|
||||
@@ -691,7 +691,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={colSpan}
|
||||
className="py-12 text-center text-gray-500"
|
||||
className="py-12 text-center text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{resource === "clients" ? (
|
||||
@@ -701,8 +701,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
) : (
|
||||
<FileText className="mb-2 h-8 w-8 text-emerald-400" />
|
||||
)}
|
||||
<div className="text-lg font-semibold">No {resource} found</div>
|
||||
<div className="mb-2 text-gray-500">
|
||||
<div className="text-lg font-semibold dark:text-gray-300">
|
||||
No {resource} found
|
||||
</div>
|
||||
<div className="mb-2 text-gray-500 dark:text-gray-400">
|
||||
Get started by adding your first{" "}
|
||||
{getSingularResourceName(resource).toLowerCase()}.
|
||||
</div>
|
||||
@@ -728,7 +730,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
<TableRow
|
||||
key={client.id}
|
||||
data-selected={selected.includes(client.id)}
|
||||
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
|
||||
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
|
||||
onClick={(e) => {
|
||||
if (
|
||||
(e.target as HTMLElement).closest(
|
||||
@@ -750,7 +752,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
|
||||
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
|
||||
<Link
|
||||
href={`/dashboard/clients/${client.id}/edit`}
|
||||
className="hover:underline"
|
||||
@@ -758,10 +760,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
{client.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{client.email}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{client.phone}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
@@ -797,7 +799,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
<TableRow
|
||||
key={invoice.id}
|
||||
data-selected={selected.includes(invoice.id)}
|
||||
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
|
||||
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
|
||||
onClick={(e) => {
|
||||
if (
|
||||
(e.target as HTMLElement).closest(
|
||||
@@ -819,7 +821,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
|
||||
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
|
||||
<Link
|
||||
href={`/dashboard/invoices/${invoice.id}`}
|
||||
className="hover:underline"
|
||||
@@ -827,7 +829,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
{invoice.invoiceNumber}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{invoice.client?.name}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4">
|
||||
@@ -837,10 +839,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
{statusLabels[invoice.status]}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 font-medium text-gray-700">
|
||||
<TableCell className="px-4 py-4 font-medium text-gray-700 dark:text-gray-300">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{formatDate(invoice.dueDate)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
@@ -885,7 +887,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
<TableRow
|
||||
key={business.id}
|
||||
data-selected={selected.includes(business.id)}
|
||||
className="group cursor-pointer transition-colors hover:bg-emerald-50/60"
|
||||
className="group cursor-pointer transition-colors hover:bg-emerald-50/60 dark:hover:bg-emerald-900/20"
|
||||
onClick={(e) => {
|
||||
if (
|
||||
(e.target as HTMLElement).closest(
|
||||
@@ -907,7 +909,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
className="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700">
|
||||
<TableCell className="px-4 py-4 text-base font-medium text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
|
||||
<Link
|
||||
href={`/dashboard/businesses/${business.id}/edit`}
|
||||
className="hover:underline"
|
||||
@@ -915,18 +917,18 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
{business.name}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{business.email}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{business.phone}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{business.website}
|
||||
</TableCell>
|
||||
<TableCell className="px-4 py-4 text-gray-700">
|
||||
<TableCell className="px-4 py-4 text-gray-700 dark:text-gray-300">
|
||||
{business.isDefault ? (
|
||||
<span className="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-800">
|
||||
<span className="rounded-full bg-emerald-100 px-2 py-1 text-xs font-medium text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400">
|
||||
Default
|
||||
</span>
|
||||
) : (
|
||||
@@ -968,7 +970,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* Controls */}
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-white/90 p-4 shadow-sm">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
|
||||
{/* Left side - View controls and filters */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -996,7 +998,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
<DropdownMenuItem className="font-medium text-gray-700">
|
||||
<DropdownMenuItem className="font-medium text-gray-700 dark:text-gray-300">
|
||||
Filters
|
||||
</DropdownMenuItem>
|
||||
{resource === "invoices" && (
|
||||
@@ -1005,7 +1007,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
onClick={() => setStatusFilter("all")}
|
||||
className={
|
||||
statusFilter === "all"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
@@ -1015,7 +1017,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
onClick={() => setStatusFilter("draft")}
|
||||
className={
|
||||
statusFilter === "draft"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
@@ -1025,7 +1027,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
onClick={() => setStatusFilter("sent")}
|
||||
className={
|
||||
statusFilter === "sent"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
@@ -1035,7 +1037,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
onClick={() => setStatusFilter("paid")}
|
||||
className={
|
||||
statusFilter === "paid"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
@@ -1045,7 +1047,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
onClick={() => setStatusFilter("overdue")}
|
||||
className={
|
||||
statusFilter === "overdue"
|
||||
? "bg-emerald-50 text-emerald-700"
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
@@ -1065,7 +1067,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
placeholder={`Search ${resource}...`}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-48 sm:w-64"
|
||||
className="w-48 sm:w-64 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
<Button variant="outline" size="icon">
|
||||
<Search className="h-4 w-4" />
|
||||
@@ -1075,7 +1077,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
{/* Batch actions */}
|
||||
{selected.length > 0 && (
|
||||
<>
|
||||
<span className="hidden text-sm text-gray-500 sm:inline">
|
||||
<span className="hidden text-sm text-gray-500 sm:inline dark:text-gray-400">
|
||||
{selected.length} selected
|
||||
</span>
|
||||
{resource === "invoices" && (
|
||||
@@ -1124,7 +1126,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
</div>
|
||||
{/* Table View */}
|
||||
{view === "table" && (
|
||||
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white/90 shadow-xl">
|
||||
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white/90 shadow-xl dark:border-gray-700 dark:bg-gray-800/90">
|
||||
<Table className="w-full">
|
||||
<TableHeader>
|
||||
<TableRow>{renderTableHeaders()}</TableRow>
|
||||
@@ -1135,9 +1137,9 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
)}
|
||||
{/* Pagination Controls */}
|
||||
{view === "table" && totalPages > 1 && (
|
||||
<div className="mt-4 mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm">
|
||||
<div className="mt-4 mb-4 flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-sm dark:border-gray-700 dark:bg-gray-800/90">
|
||||
{/* Left side - Page info and items per page */}
|
||||
<div className="flex items-center gap-3 text-sm text-gray-600">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span className="hidden sm:inline">
|
||||
Showing {startIndex + 1} to{" "}
|
||||
{Math.min(endIndex, filteredAndSortedData.length)} of{" "}
|
||||
@@ -1154,7 +1156,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
setItemsPerPage(Number(e.target.value));
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className="h-8 w-20 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm focus:border-emerald-500 focus:ring-emerald-500 sm:w-28"
|
||||
className="h-8 w-20 rounded-md border border-gray-300 bg-white px-2 py-1 text-sm focus:border-emerald-500 focus:ring-emerald-500 sm:w-28 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value={5}>5</option>
|
||||
<option value={10}>10</option>
|
||||
@@ -1220,7 +1222,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
pageNum === currentPage + 2
|
||||
) {
|
||||
return (
|
||||
<span key={pageNum} className="px-1 text-gray-400 sm:px-2">
|
||||
<span
|
||||
key={pageNum}
|
||||
className="px-1 text-gray-400 sm:px-2 dark:text-gray-500"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
);
|
||||
@@ -1251,7 +1256,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
Array.from({ length: 6 }).map((_, index) => (
|
||||
<div
|
||||
key={`skeleton-card-${index}`}
|
||||
className="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl"
|
||||
className="flex flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl dark:border-gray-700 dark:bg-gray-800/90"
|
||||
>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="h-4 w-40" />
|
||||
@@ -1259,7 +1264,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
</div>
|
||||
))
|
||||
) : filteredAndSortedData.length === 0 ? (
|
||||
<div className="col-span-full flex flex-col items-center py-16 text-gray-500">
|
||||
<div className="col-span-full flex flex-col items-center py-16 text-gray-500 dark:text-gray-400">
|
||||
{resource === "clients" ? (
|
||||
<UserPlus className="mb-2 h-8 w-8 text-emerald-400" />
|
||||
) : resource === "businesses" ? (
|
||||
@@ -1267,8 +1272,10 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
) : (
|
||||
<FileText className="mb-2 h-8 w-8 text-emerald-400" />
|
||||
)}
|
||||
<div className="text-lg font-semibold">No {resource} found</div>
|
||||
<div className="mb-2 text-gray-500">
|
||||
<div className="text-lg font-semibold dark:text-gray-300">
|
||||
No {resource} found
|
||||
</div>
|
||||
<div className="mb-2 text-gray-500 dark:text-gray-400">
|
||||
Get started by adding your first{" "}
|
||||
{getSingularResourceName(resource).toLowerCase()}.
|
||||
</div>
|
||||
@@ -1289,13 +1296,17 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
return (
|
||||
<div
|
||||
key={client.id}
|
||||
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
|
||||
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
|
||||
>
|
||||
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
|
||||
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
|
||||
{client.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">{client.email}</div>
|
||||
<div className="text-sm text-gray-700">{client.phone}</div>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{client.email}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{client.phone}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (resource === "invoices") {
|
||||
@@ -1303,15 +1314,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
return (
|
||||
<div
|
||||
key={invoice.id}
|
||||
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
|
||||
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
|
||||
>
|
||||
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
|
||||
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
|
||||
{invoice.invoiceNumber}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{invoice.client?.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{formatCurrency(invoice.totalAmount)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1321,15 +1332,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
return (
|
||||
<div
|
||||
key={business.id}
|
||||
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60"
|
||||
className="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-200 bg-white/90 p-4 shadow-xl transition-colors hover:bg-emerald-50/60 dark:border-gray-700 dark:bg-gray-800/90 dark:hover:bg-emerald-900/20"
|
||||
>
|
||||
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700">
|
||||
<div className="text-lg font-semibold text-gray-900 group-hover:text-emerald-700 dark:text-white dark:group-hover:text-emerald-400">
|
||||
{business.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{business.email}
|
||||
</div>
|
||||
<div className="text-sm text-gray-700">
|
||||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{business.phone}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1341,15 +1352,15 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
)}
|
||||
{/* Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm">
|
||||
<DialogContent className="border-0 bg-white/95 shadow-2xl backdrop-blur-sm dark:bg-gray-800/95">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-gray-800">
|
||||
<DialogTitle className="text-xl font-bold text-gray-800 dark:text-white">
|
||||
Delete{" "}
|
||||
{resource.slice(0, -1).charAt(0).toUpperCase() +
|
||||
resource.slice(0, -1).slice(1)}
|
||||
{itemToDelete === "batch" ? "s" : ""}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-600">
|
||||
<DialogDescription className="text-gray-600 dark:text-gray-300">
|
||||
{itemToDelete === "batch"
|
||||
? `Are you sure you want to delete the selected ${resource}? This action cannot be undone.`
|
||||
: `Are you sure you want to delete this ${resource.slice(0, -1)}? This action cannot be undone.`}
|
||||
@@ -1359,7 +1370,7 @@ export function UniversalTable({ resource }: UniversalTableProps) {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setDeleteDialogOpen(false)}
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user