import React, { useCallback, useMemo, useState } from 'react'; import { ChevronDown, ChevronUp, GripVertical, Loader2, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'; function newLineRow() { return { id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, product_service: '', description: '', qty: '1', rate: '0', billing_interval: 'monthly', currency: 'USD', }; } function parseNum(s) { const n = parseFloat(String(s ?? '').replace(/,/g, '').trim()); return Number.isFinite(n) ? n : NaN; } function fmtMoneyAmount(n) { if (!Number.isFinite(n)) return '—'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(n); } /** * Modal to capture PO / customer / line items before marking a deal Won (invoicing). * @param {boolean | undefined} gmailInvitesReady — from /api/auth/me; false blocks submit until user reconnects Gmail. */ export default function WonBillingModal({ dealName, busy, onCancel, onSubmit, gmailInvitesReady }) { const [poNumber, setPoNumber] = useState(''); const [customerLegalName, setCustomerLegalName] = useState(''); const [customerAddress, setCustomerAddress] = useState(''); const [contactPersonName, setContactPersonName] = useState(''); const [channelPartnerName, setChannelPartnerName] = useState(''); const [noteToCustomer, setNoteToCustomer] = useState(''); const [noteToAccounts, setNoteToAccounts] = useState(''); const [lines, setLines] = useState(() => [newLineRow()]); const [localError, setLocalError] = useState(''); const lineAmounts = useMemo(() => { return lines.map((row) => { const q = parseNum(row.qty); const r = parseNum(row.rate); if (!Number.isFinite(q) || !Number.isFinite(r)) return NaN; return Math.round(q * r * 100) / 100; }); }, [lines]); const subtotal = useMemo(() => { return lineAmounts.reduce((a, v) => a + (Number.isFinite(v) ? v : 0), 0); }, [lineAmounts]); const updateLine = useCallback((id, patch) => { setLines((prev) => prev.map((row) => (row.id === id ? { ...row, ...patch } : row))); }, []); const removeLine = useCallback((id) => { setLines((prev) => (prev.length <= 1 ? prev : prev.filter((r) => r.id !== id))); }, []); const addLine = useCallback(() => { setLines((prev) => [...prev, newLineRow()]); }, []); const clearAllLines = useCallback(() => { setLines([newLineRow()]); }, []); const moveLine = useCallback((index, dir) => { setLines((prev) => { const j = index + dir; if (j < 0 || j >= prev.length) return prev; const next = [...prev]; const t = next[index]; next[index] = next[j]; next[j] = t; return next; }); }, []); const handleSubmit = () => { setLocalError(''); if (gmailInvitesReady === false) { setLocalError('Connect Gmail below before marking this deal won.'); return; } if (!poNumber.trim()) { setLocalError('PO number is required.'); return; } if (!customerLegalName.trim()) { setLocalError('Customer legal name is required.'); return; } if (!customerAddress.trim()) { setLocalError('Customer address is required.'); return; } if (!contactPersonName.trim()) { setLocalError('Contact person name is required.'); return; } if (!noteToAccounts.trim()) { setLocalError('Note to our accounts is required.'); return; } const items = []; for (const row of lines) { const ps = row.product_service.trim(); if (!ps) { setLocalError('Each line must have a product or service name.'); return; } if (!row.description.trim()) { setLocalError('Each line must have a description.'); return; } if (!row.billing_interval) { setLocalError('Each line must have a billing interval.'); return; } if (!row.currency) { setLocalError('Each line must have a currency.'); return; } const q = parseNum(row.qty); const r = parseNum(row.rate); if (!Number.isFinite(q) || q <= 0) { setLocalError('Each line needs a valid quantity greater than 0.'); return; } if (!Number.isFinite(r) || r < 0) { setLocalError('Each line needs a valid rate (0 or greater).'); return; } items.push({ product_service: ps, description: row.description.trim(), qty: q, rate: r, billing_interval: row.billing_interval || 'monthly', currency: row.currency || 'USD', }); } onSubmit({ po_number: poNumber.trim(), customer_legal_name: customerLegalName.trim(), customer_address: customerAddress.trim(), contact_person_name: contactPersonName.trim(), channel_partner_name: channelPartnerName.trim() || null, note_to_customer: noteToCustomer.trim(), note_to_accounts: noteToAccounts.trim(), line_items: items, }); }; return (
{dealName ? ( <> {dealName} — complete the details below for accurate invoicing. > ) : ( 'Complete the details below for accurate invoicing.' )}
Workspace admins get an email when you mark a deal won. Sign in with Google again (consent screen) or use{' '} Reconnect Google for invites , then return here and submit again.
) : null}| # | Product / service | Description * | Billing interval * | Currency * | Qty | Rate | Amount | ||
|---|---|---|---|---|---|---|---|---|---|
|
|
{idx + 1} | updateLine(row.id, { product_service: e.target.value }) } className="h-8 bg-white text-xs" placeholder="Product or service" /> | updateLine(row.id, { description: e.target.value }) } className="h-8 bg-white text-xs" placeholder="Description" /> | updateLine(row.id, { qty: e.target.value })} className="h-8 bg-white text-right text-xs tabular-nums" inputMode="decimal" /> | updateLine(row.id, { rate: e.target.value })} className="h-8 bg-white text-right text-xs tabular-nums" inputMode="decimal" /> | {fmtMoneyAmount(amt)} |
{localError}
) : null}