Nicholai a0f7852845
feat(agent): add AI chat panel and dashboard updates (#34)
* feat(agent): add AI chat panel and dashboard updates

Add ElizaOS-powered agent chat panel with streaming,
voice input, markdown rendering, and page-aware context.
Update dashboard layout with context menu and refactored
pages. Add agent memory schema, new UI components,
and fix lint errors across AI-related files.

* fix(auth): use Host header for SSO redirect URI

nextUrl.origin returns http://localhost:3000 on CF Workers,
breaking OAuth callbacks. Use Host header to derive the
correct production origin for WorkOS redirect URI.

* fix(auth): add Toaster to auth layout, fix error codes

Auth pages had no Toaster component so toast.error() calls
were invisible. Also return 401 for auth errors instead of
generic 500 from the login API.

---------

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
2026-02-05 15:56:06 -07:00

164 lines
4.4 KiB
TypeScript
Executable File

"use client"
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<Customer[]>([])
const [loading, setLoading] = React.useState(true)
const [dialogOpen, setDialogOpen] = React.useState(false)
const [editing, setEditing] = React.useState<Customer | null>(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 (
<div className="flex-1 space-y-4 p-4 sm:p-6 md:p-8 pt-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
Customers
</h2>
<p className="text-sm sm:text-base text-muted-foreground">
Manage customer accounts
</p>
</div>
</div>
<div className="rounded-md border p-8 text-center text-muted-foreground">
Loading...
</div>
</div>
)
}
return (
<>
<div className="flex-1 space-y-4 p-4 sm:p-6 md:p-8 pt-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight">
Customers
</h2>
<p className="text-sm sm:text-base text-muted-foreground">
Manage customer accounts
</p>
</div>
<Button onClick={openCreate} className="w-full sm:w-auto">
<IconPlus className="mr-2 size-4" />
Add Customer
</Button>
</div>
{customers.length === 0 ? (
<div className="rounded-md border border-dashed p-8 text-center">
<p className="text-muted-foreground">No customers yet</p>
<p className="text-sm text-muted-foreground/70 mt-1">
Add your first customer to start tracking contacts and invoices.
</p>
</div>
) : (
<CustomersTable
customers={customers}
onEdit={handleEdit}
onDelete={handleDelete}
/>
)}
</div>
<CustomerDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
initialData={editing}
onSubmit={handleSubmit}
/>
</>
)
}