diff --git a/src/app/dashboard/contacts/page.tsx b/src/app/dashboard/contacts/page.tsx new file mode 100644 index 0000000..155d76c --- /dev/null +++ b/src/app/dashboard/contacts/page.tsx @@ -0,0 +1,298 @@ +"use client" + +import * as React from "react" +import { IconPlus } from "@tabler/icons-react" +import { Plus } from "lucide-react" +import { useSearchParams, useRouter } from "next/navigation" +import { toast } from "sonner" +import { useRegisterPageActions } from "@/hooks/use-register-page-actions" + +import { + getCustomers, + createCustomer, + updateCustomer, + deleteCustomer, +} from "@/app/actions/customers" +import { + getVendors, + createVendor, + updateVendor, + deleteVendor, +} from "@/app/actions/vendors" +import type { Customer, Vendor } from "@/db/schema" +import { Button } from "@/components/ui/button" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { Skeleton } from "@/components/ui/skeleton" +import { CustomersTable } from "@/components/financials/customers-table" +import { CustomerDialog } from "@/components/financials/customer-dialog" +import { VendorsTable } from "@/components/financials/vendors-table" +import { VendorDialog } from "@/components/financials/vendor-dialog" + +type Tab = "customers" | "vendors" + +export default function ContactsPage() { + return ( + }> + + + ) +} + +function ContactsSkeleton() { + return ( +
+
+ + +
+ + +
+ ) +} + +function ContactsContent() { + const searchParams = useSearchParams() + const router = useRouter() + const initialTab = (searchParams.get("tab") as Tab) || "customers" + + const [tab, setTab] = React.useState(initialTab) + const [loading, setLoading] = React.useState(true) + + const [customersList, setCustomersList] = React.useState([]) + const [vendorsList, setVendorsList] = React.useState([]) + + const [customerDialogOpen, setCustomerDialogOpen] = React.useState(false) + const [editingCustomer, setEditingCustomer] = + React.useState(null) + + const [vendorDialogOpen, setVendorDialogOpen] = React.useState(false) + const [editingVendor, setEditingVendor] = + React.useState(null) + + const loadAll = async () => { + try { + const [customers, vendors] = await Promise.all([ + getCustomers(), + getVendors(), + ]) + setCustomersList(customers) + setVendorsList(vendors) + } catch { + toast.error("Failed to load contacts") + } finally { + setLoading(false) + } + } + + React.useEffect(() => { + loadAll() + }, []) + + const openCustomer = React.useCallback(() => { + setEditingCustomer(null) + setCustomerDialogOpen(true) + }, []) + + const openVendor = React.useCallback(() => { + setEditingVendor(null) + setVendorDialogOpen(true) + }, []) + + const TAB_ACTIONS: Record< + Tab, + { id: string; label: string; onSelect: () => void } + > = React.useMemo( + () => ({ + customers: { + id: "add-customer", + label: "Add Customer", + onSelect: openCustomer, + }, + vendors: { + id: "add-vendor", + label: "Add Vendor", + onSelect: openVendor, + }, + }), + [openCustomer, openVendor] + ) + + const pageActions = React.useMemo(() => { + const action = TAB_ACTIONS[tab] + return [{ ...action, icon: Plus }] + }, [tab, TAB_ACTIONS]) + + useRegisterPageActions(pageActions) + + const handleTabChange = (value: string) => { + setTab(value as Tab) + router.replace(`/dashboard/contacts?tab=${value}`, { scroll: false }) + } + + const handleCustomerSubmit = async (data: { + name: string + company: string + email: string + phone: string + address: string + notes: string + }) => { + if (editingCustomer) { + const result = await updateCustomer(editingCustomer.id, data) + if (result.success) { + toast.success("Customer updated") + } else { + toast.error(result.error || "Failed") + return + } + } else { + const result = await createCustomer(data) + if (result.success) { + toast.success("Customer created") + } else { + toast.error(result.error || "Failed") + return + } + } + setCustomerDialogOpen(false) + await loadAll() + } + + const handleDeleteCustomer = async (id: string) => { + const result = await deleteCustomer(id) + if (result.success) { + toast.success("Customer deleted") + await loadAll() + } else { + toast.error(result.error || "Failed") + } + } + + const handleVendorSubmit = async (data: { + name: string + category: string + email: string + phone: string + address: string + }) => { + if (editingVendor) { + const result = await updateVendor(editingVendor.id, data) + if (result.success) { + toast.success("Vendor updated") + } else { + toast.error(result.error || "Failed") + return + } + } else { + const result = await createVendor(data) + if (result.success) { + toast.success("Vendor created") + } else { + toast.error(result.error || "Failed") + return + } + } + setVendorDialogOpen(false) + await loadAll() + } + + const handleDeleteVendor = async (id: string) => { + const result = await deleteVendor(id) + if (result.success) { + toast.success("Vendor deleted") + await loadAll() + } else { + toast.error(result.error || "Failed") + } + } + + if (loading) { + return + } + + const addLabel = tab === "customers" ? "Add Customer" : "Add Vendor" + const addHandler = tab === "customers" ? openCustomer : openVendor + + return ( + <> +
+ {/* single toolbar: tabs left, add button right */} + +
+ + + Customers + + {customersList.length} + + + + Vendors + + {vendorsList.length} + + + + + +
+ + + { + setEditingCustomer(customer) + setCustomerDialogOpen(true) + }} + onDelete={handleDeleteCustomer} + /> + + + + { + setEditingVendor(vendor) + setVendorDialogOpen(true) + }} + onDelete={handleDeleteVendor} + /> + +
+
+ + + + + + ) +} diff --git a/src/app/dashboard/customers/page.tsx b/src/app/dashboard/customers/page.tsx index 4e5ca17..18c4067 100755 --- a/src/app/dashboard/customers/page.tsx +++ b/src/app/dashboard/customers/page.tsx @@ -1,163 +1,5 @@ -"use client" +import { redirect } from "next/navigation" -import * as React from "react" -import { IconPlus } from "@tabler/icons-react" -import { Plus } from "lucide-react" -import { toast } from "sonner" - -import { - getCustomers, - createCustomer, - updateCustomer, - deleteCustomer, -} from "@/app/actions/customers" -import type { Customer } from "@/db/schema" -import { Button } from "@/components/ui/button" -import { CustomersTable } from "@/components/financials/customers-table" -import { CustomerDialog } from "@/components/financials/customer-dialog" -import { useRegisterPageActions } from "@/hooks/use-register-page-actions" - -export default function CustomersPage() { - const [customers, setCustomers] = React.useState([]) - const [loading, setLoading] = React.useState(true) - const [dialogOpen, setDialogOpen] = React.useState(false) - const [editing, setEditing] = React.useState(null) - - const openCreate = React.useCallback(() => { - setEditing(null) - setDialogOpen(true) - }, []) - - const pageActions = React.useMemo( - () => [ - { - id: "add-customer", - label: "Add Customer", - icon: Plus, - onSelect: openCreate, - }, - ], - [openCreate] - ) - useRegisterPageActions(pageActions) - - const load = async () => { - try { - const data = await getCustomers() - setCustomers(data) - } catch { - toast.error("Failed to load customers") - } finally { - setLoading(false) - } - } - - React.useEffect(() => { load() }, []) - - const handleEdit = (customer: Customer) => { - setEditing(customer) - setDialogOpen(true) - } - - const handleDelete = async (id: string) => { - const result = await deleteCustomer(id) - if (result.success) { - toast.success("Customer deleted") - await load() - } else { - toast.error(result.error || "Failed to delete customer") - } - } - - const handleSubmit = async (data: { - name: string - company: string - email: string - phone: string - address: string - notes: string - }) => { - if (editing) { - const result = await updateCustomer(editing.id, data) - if (result.success) { - toast.success("Customer updated") - } else { - toast.error(result.error || "Failed to update customer") - return - } - } else { - const result = await createCustomer(data) - if (result.success) { - toast.success("Customer created") - } else { - toast.error(result.error || "Failed to create customer") - return - } - } - setDialogOpen(false) - await load() - } - - if (loading) { - return ( -
-
-
-

- Customers -

-

- Manage customer accounts -

-
-
-
- Loading... -
-
- ) - } - - return ( - <> -
-
-
-

- Customers -

-

- Manage customer accounts -

-
- -
- - {customers.length === 0 ? ( -
-

No customers yet

-

- Add your first customer to start tracking contacts and invoices. -

-
- ) : ( - - )} -
- - - - ) +export default function CustomersRedirect(): never { + redirect("/dashboard/contacts?tab=customers") } diff --git a/src/app/dashboard/people/page.tsx b/src/app/dashboard/people/page.tsx index 0deb0bf..8a9d038 100755 --- a/src/app/dashboard/people/page.tsx +++ b/src/app/dashboard/people/page.tsx @@ -1,148 +1,20 @@ "use client" import * as React from "react" -import { IconUserPlus } from "@tabler/icons-react" -import { UserPlus } from "lucide-react" -import { toast } from "sonner" - -import { getUsers, deactivateUser, type UserWithRelations } from "@/app/actions/users" -import { Button } from "@/components/ui/button" -import { PeopleTable } from "@/components/people-table" -import { UserDrawer } from "@/components/people/user-drawer" -import { InviteDialog } from "@/components/people/invite-dialog" -import { useRegisterPageActions } from "@/hooks/use-register-page-actions" - +import { useRouter } from "next/navigation" export default function PeoplePage() { - const [users, setUsers] = React.useState([]) - const [loading, setLoading] = React.useState(true) - const [selectedUser, setSelectedUser] = React.useState(null) - const [drawerOpen, setDrawerOpen] = React.useState(false) - const [inviteDialogOpen, setInviteDialogOpen] = React.useState(false) - - const pageActions = React.useMemo( - () => [ - { - id: "invite-user", - label: "Invite User", - icon: UserPlus, - onSelect: () => setInviteDialogOpen(true), - }, - ], - [] - ) - useRegisterPageActions(pageActions) + const router = useRouter() React.useEffect(() => { - loadUsers() - }, []) - - const loadUsers = async () => { - try { - const data = await getUsers() - setUsers(data) - } catch (error) { - console.error("Failed to load users:", error) - toast.error("Failed to load users") - } finally { - setLoading(false) - } - } - - const handleEditUser = (user: UserWithRelations) => { - setSelectedUser(user) - setDrawerOpen(true) - } - - const handleDeactivateUser = async (userId: string) => { - try { - const result = await deactivateUser(userId) - if (result.success) { - toast.success("User deactivated") - await loadUsers() - } else { - toast.error(result.error || "Failed to deactivate user") - } - } catch (error) { - console.error("Failed to deactivate user:", error) - toast.error("Failed to deactivate user") - } - } - - const handleUserUpdated = async () => { - await loadUsers() - } - - const handleUserInvited = async () => { - await loadUsers() - } - - if (loading) { - return ( -
-
-
-

People

-

- Manage team members and client users -

-
-
-
- Loading... -
-
- ) - } + router.replace("/dashboard") + }, [router]) return ( - <> -
-
-
-

People

-

- Manage team members and client users -

-
- -
- - {users.length === 0 ? ( -
-

No users found

-

- Invite users to get started -

-
- ) : ( - - )} -
- - - - - - +
+

+ Team management has moved to Settings. +

+
) } diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx new file mode 100644 index 0000000..01e46f0 --- /dev/null +++ b/src/app/dashboard/settings/page.tsx @@ -0,0 +1,193 @@ +"use client" + +import * as React from "react" +import { + IconAdjustments, + IconBrain, + IconPalette, + IconPlug, + IconPuzzle, + IconRobot, + IconTerminal2, + IconUsers, +} from "@tabler/icons-react" + +import { useIsMobile } from "@/hooks/use-mobile" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" + +import { PreferencesTab } from "@/components/settings/preferences-tab" +import { AppearanceTab } from "@/components/settings/appearance-tab" +import { TeamTab } from "@/components/settings/team-tab" +import { AIModelTab } from "@/components/settings/ai-model-tab" +import { SkillsTab } from "@/components/settings/skills-tab" +import { ClaudeCodeTab } from "@/components/settings/claude-code-tab" +import { NetSuiteConnectionStatus } from "@/components/netsuite/connection-status" +import { SyncControls } from "@/components/netsuite/sync-controls" +import { GoogleDriveConnectionStatus } from "@/components/google/connection-status" + +const SETTINGS_TABS = [ + { value: "preferences", label: "Preferences", icon: IconAdjustments }, + { value: "appearance", label: "Theme", icon: IconPalette }, + { value: "team", label: "Team", icon: IconUsers }, + { value: "ai-model", label: "AI Model", icon: IconBrain }, + { value: "agent", label: "Agent", icon: IconRobot }, + { value: "skills", label: "Skills", icon: IconPuzzle }, + { value: "integrations", label: "Integrations", icon: IconPlug }, + { value: "claude-code", label: "Code Bridge", icon: IconTerminal2 }, +] as const + +type SectionValue = (typeof SETTINGS_TABS)[number]["value"] + +// wide sections get unconstrained width for tables/complex layouts +const WIDE_SECTIONS = new Set([ + "appearance", "team", "ai-model", "claude-code", +]) + +function AgentSection() { + const [signetId, setSignetId] = React.useState("") + + return ( +
+
+ + setSignetId(e.target.value)} + placeholder="0x..." + className="h-9 max-w-sm font-mono" + type="password" + /> +
+ + +
+ ) +} + +function IntegrationsSection() { + return ( +
+ + + + +
+ ) +} + +export default function SettingsPage() { + const [activeSection, setActiveSection] = + React.useState("preferences") + const isMobile = useIsMobile() + + const isWide = WIDE_SECTIONS.has(activeSection) + + const renderContent = (): React.ReactNode => { + switch (activeSection) { + case "preferences": + return + case "appearance": + return + case "team": + return + case "ai-model": + return + case "agent": + return + case "skills": + return + case "integrations": + return + case "claude-code": + return + default: + return null + } + } + + return ( +
+ {/* tab bar — pinned to top, full bleed */} +
+
+ {isMobile ? ( +
+ +
+ ) : ( + + )} +
+
+ + {/* scrollable content area */} +
+
+ {renderContent()} +
+
+
+ ) +} diff --git a/src/app/dashboard/vendors/page.tsx b/src/app/dashboard/vendors/page.tsx index 344ceb1..a2f6245 100755 --- a/src/app/dashboard/vendors/page.tsx +++ b/src/app/dashboard/vendors/page.tsx @@ -1,162 +1,5 @@ -"use client" +import { redirect } from "next/navigation" -import * as React from "react" -import { IconPlus } from "@tabler/icons-react" -import { Plus } from "lucide-react" -import { toast } from "sonner" - -import { - getVendors, - createVendor, - updateVendor, - deleteVendor, -} from "@/app/actions/vendors" -import type { Vendor } from "@/db/schema" -import { Button } from "@/components/ui/button" -import { VendorsTable } from "@/components/financials/vendors-table" -import { VendorDialog } from "@/components/financials/vendor-dialog" -import { useRegisterPageActions } from "@/hooks/use-register-page-actions" - -export default function VendorsPage() { - const [vendors, setVendors] = React.useState([]) - const [loading, setLoading] = React.useState(true) - const [dialogOpen, setDialogOpen] = React.useState(false) - const [editing, setEditing] = React.useState(null) - - const openCreate = React.useCallback(() => { - setEditing(null) - setDialogOpen(true) - }, []) - - const pageActions = React.useMemo( - () => [ - { - id: "add-vendor", - label: "Add Vendor", - icon: Plus, - onSelect: openCreate, - }, - ], - [openCreate] - ) - useRegisterPageActions(pageActions) - - const load = async () => { - try { - const data = await getVendors() - setVendors(data) - } catch { - toast.error("Failed to load vendors") - } finally { - setLoading(false) - } - } - - React.useEffect(() => { load() }, []) - - const handleEdit = (vendor: Vendor) => { - setEditing(vendor) - setDialogOpen(true) - } - - const handleDelete = async (id: string) => { - const result = await deleteVendor(id) - if (result.success) { - toast.success("Vendor deleted") - await load() - } else { - toast.error(result.error || "Failed to delete vendor") - } - } - - const handleSubmit = async (data: { - name: string - category: string - email: string - phone: string - address: string - }) => { - if (editing) { - const result = await updateVendor(editing.id, data) - if (result.success) { - toast.success("Vendor updated") - } else { - toast.error(result.error || "Failed to update vendor") - return - } - } else { - const result = await createVendor(data) - if (result.success) { - toast.success("Vendor created") - } else { - toast.error(result.error || "Failed to create vendor") - return - } - } - setDialogOpen(false) - await load() - } - - if (loading) { - return ( -
-
-
-

- Vendors -

-

- Manage vendor relationships -

-
-
-
- Loading... -
-
- ) - } - - return ( - <> -
-
-
-

- Vendors -

-

- Manage vendor relationships -

-
- -
- - {vendors.length === 0 ? ( -
-

No vendors yet

-

- Add your first vendor to manage subcontractors, suppliers, and bills. -

-
- ) : ( - - )} -
- - - - ) +export default function VendorsRedirect(): never { + redirect("/dashboard/contacts?tab=vendors") } diff --git a/src/app/page.tsx b/src/app/page.tsx index b1fa401..6cc7471 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,13 +1,155 @@ import Link from "next/link"; -import { IconArrowRight, IconBrandGithub } from "@tabler/icons-react"; +import { + IconArrowRight, + IconBrandGithub, + IconBrandDocker, + IconBrandApple, + IconBrandWindows, + IconBrandAndroid, + IconDeviceDesktop, + IconCloud, + IconSparkles, + IconPuzzle, + IconRocket, + IconShieldLock, +} from "@tabler/icons-react"; -export default function Home() { +const features = [ + { + num: "01", + title: "AI Agent Built In", + description: + "Every workspace ships with an intelligent assistant " + + "that understands your domain and takes action " + + "through tools you define.", + icon: IconSparkles, + }, + { + num: "02", + title: "Modular by Design", + description: + "Scheduling, financials, file management, " + + "messaging \u2014 drop in what you need, leave " + + "out what you don\u2019t.", + icon: IconPuzzle, + }, + { + num: "03", + title: "Deploy Anywhere", + description: + "Self-host with Docker, ship to desktop and " + + "mobile, or deploy to the edge with Cloudflare. " + + "Your infrastructure, your call.", + icon: IconRocket, + }, + { + num: "04", + title: "Enterprise Auth", + description: + "SSO, directory sync, and role-based access " + + "control out of the box via WorkOS.", + icon: IconShieldLock, + }, +] as const; + +const platforms = [ + { icon: IconBrandDocker, label: "Docker" }, + { icon: IconBrandApple, label: "macOS" }, + { icon: IconBrandWindows, label: "Windows" }, + { icon: IconDeviceDesktop, label: "Linux" }, + { icon: IconBrandApple, label: "iOS" }, + { icon: IconBrandAndroid, label: "Android" }, + { icon: IconCloud, label: "Edge" }, +] as const; + +const modules = [ + "Scheduling", + "Financials", + "File Management", + "Messaging", + "NetSuite Sync", + "Google Drive", + "Themes", + "Plugins", +] as const; + +export default function Home(): React.JSX.Element { return ( -
-