compassmock/src/components/financials/payment-dialog.tsx
Nicholai fbd31b58ae
feat(netsuite): add NetSuite integration and financials (#29)
* feat(schema): add auth, people, and financial tables

Add users, organizations, teams, groups, and project
members tables. Extend customers/vendors with netsuite
fields. Add netsuite schema for invoices, bills,
payments, and credit memos. Include all migrations,
seeds, new UI primitives, and config updates.

* feat(auth): add WorkOS authentication system

Add login, signup, password reset, email verification,
and invitation flows via WorkOS AuthKit. Includes auth
middleware, permission helpers, dev mode fallbacks,
and auth page components.

* feat(people): add people management system

Add user, team, group, and organization management
with CRUD actions, dashboard pages, invite dialog,
user drawer, and role-based filtering. Includes
WorkOS invitation integration.

* feat(netsuite): add NetSuite integration and financials

Add bidirectional NetSuite REST API integration with
OAuth 2.0, rate limiting, sync engine, and conflict
resolution. Includes invoices, vendor bills, payments,
credit memos CRUD, customer/vendor management pages,
and financial dashboard with tabbed views.

* ci: retrigger build

* fix: add mobile-list-card dependency for people-table

---------

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
2026-02-04 16:36:19 -07:00

269 lines
7.8 KiB
TypeScript
Executable File

"use client"
import * as React from "react"
import { Button } from "@/components/ui/button"
import {
ResponsiveDialog,
ResponsiveDialogBody,
ResponsiveDialogFooter,
} from "@/components/ui/responsive-dialog"
import { DatePicker } from "@/components/ui/date-picker"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import type { Payment } from "@/db/schema-netsuite"
import type { Customer, Vendor, Project } from "@/db/schema"
const PAYMENT_METHODS = [
"check",
"ach",
"wire",
"credit_card",
"cash",
"other",
] as const
interface PaymentDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
initialData?: Payment | null
customers: Customer[]
vendors: Vendor[]
projects: Project[]
onSubmit: (data: {
paymentType: string
customerId: string | null
vendorId: string | null
projectId: string | null
amount: number
paymentDate: string
paymentMethod: string
referenceNumber: string
memo: string
}) => void
}
export function PaymentDialog({
open,
onOpenChange,
initialData,
customers,
vendors,
projects,
onSubmit,
}: PaymentDialogProps) {
const [paymentType, setPaymentType] = React.useState("received")
const [customerId, setCustomerId] = React.useState("")
const [vendorId, setVendorId] = React.useState("")
const [projectId, setProjectId] = React.useState("")
const [amount, setAmount] = React.useState(0)
const [paymentDate, setPaymentDate] = React.useState("")
const [paymentMethod, setPaymentMethod] = React.useState("")
const [referenceNumber, setReferenceNumber] = React.useState("")
const [memo, setMemo] = React.useState("")
React.useEffect(() => {
if (initialData) {
setPaymentType(initialData.paymentType)
setCustomerId(initialData.customerId ?? "")
setVendorId(initialData.vendorId ?? "")
setProjectId(initialData.projectId ?? "")
setAmount(initialData.amount)
setPaymentDate(initialData.paymentDate)
setPaymentMethod(initialData.paymentMethod ?? "")
setReferenceNumber(initialData.referenceNumber ?? "")
setMemo(initialData.memo ?? "")
} else {
setPaymentType("received")
setCustomerId("")
setVendorId("")
setProjectId("")
setAmount(0)
setPaymentDate(new Date().toISOString().split("T")[0])
setPaymentMethod("")
setReferenceNumber("")
setMemo("")
}
}, [initialData, open])
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!amount || !paymentDate) return
onSubmit({
paymentType,
customerId: paymentType === "received" ? (customerId || null) : null,
vendorId: paymentType === "sent" ? (vendorId || null) : null,
projectId: projectId || null,
amount,
paymentDate,
paymentMethod,
referenceNumber,
memo,
})
}
const page1 = (
<>
<div className="space-y-1.5">
<Label className="text-xs">Type *</Label>
<Select value={paymentType} onValueChange={setPaymentType}>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="received">Received</SelectItem>
<SelectItem value="sent">Sent</SelectItem>
</SelectContent>
</Select>
</div>
{paymentType === "received" && (
<div className="space-y-1.5">
<Label className="text-xs">Customer</Label>
<Select value={customerId || "none"} onValueChange={(v) => setCustomerId(v === "none" ? "" : v)}>
<SelectTrigger className="h-9">
<SelectValue placeholder="Select customer" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{customers.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{paymentType === "sent" && (
<div className="space-y-1.5">
<Label className="text-xs">Vendor</Label>
<Select value={vendorId || "none"} onValueChange={(v) => setVendorId(v === "none" ? "" : v)}>
<SelectTrigger className="h-9">
<SelectValue placeholder="Select vendor" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{vendors.map((v) => (
<SelectItem key={v.id} value={v.id}>
{v.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-1.5">
<Label className="text-xs">Project</Label>
<Select value={projectId || "none"} onValueChange={(v) => setProjectId(v === "none" ? "" : v)}>
<SelectTrigger className="h-9">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None</SelectItem>
{projects.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</>
)
const page2 = (
<>
<div className="space-y-1.5">
<Label className="text-xs">Amount *</Label>
<Input
type="number"
className="h-9"
min={0}
step="any"
value={amount || ""}
onChange={(e) => setAmount(parseFloat(e.target.value) || 0)}
required
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Date *</Label>
<DatePicker
value={paymentDate}
onChange={setPaymentDate}
placeholder="Select date"
/>
</div>
</>
)
const page3 = (
<>
<div className="space-y-1.5">
<Label className="text-xs">Method</Label>
<Select value={paymentMethod} onValueChange={setPaymentMethod}>
<SelectTrigger className="h-9">
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
{PAYMENT_METHODS.map((m) => (
<SelectItem key={m} value={m}>
{m.replace("_", " ")}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Reference #</Label>
<Input
className="h-9"
value={referenceNumber}
onChange={(e) => setReferenceNumber(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Memo</Label>
<Textarea
value={memo}
onChange={(e) => setMemo(e.target.value)}
rows={2}
className="text-sm"
/>
</div>
</>
)
return (
<ResponsiveDialog
open={open}
onOpenChange={onOpenChange}
title={initialData ? "Edit Payment" : "New Payment"}
>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 min-h-0">
<ResponsiveDialogBody pages={[page1, page2, page3]} />
<ResponsiveDialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
className="h-9"
>
Cancel
</Button>
<Button type="submit" className="h-9">
{initialData ? "Save Changes" : "Create Payment"}
</Button>
</ResponsiveDialogFooter>
</form>
</ResponsiveDialog>
)
}