@burdenoff/microfe-housingvista 2026.930.2 → 2026.1001.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"OffersPage.js","names":[],"sources":["../../../../src/housingvista/pages/offers/OffersPage.tsx"],"sourcesContent":["/**\n * Offers — the buyer/owner surface for property offers. Buyers track offers they\n * submitted (status timeline submitted→countered→accepted/rejected/withdrawn)\n * and can withdraw; owners can respond (accept / counter / reject). Server-side\n * status filter + pagination, with live per-status tab counts driven off a real\n * unfiltered read (no fabricated numbers).\n * @module housingvista\n */\n\nimport { useState, type FormEvent } from 'react';\nimport { useNavigate } from 'react-router';\nimport { Tag, Check, X, Repeat, Undo2, MapPin, ArrowRight, Home } from 'lucide-react';\nimport {\n Button,\n Pagination,\n EmphasisPanel,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout, StatCard, StatusBadge, EmptyState } from '../../components';\nimport { useOffers, useRespondOffer, useWithdrawOffer, type OfferRow } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { notify } from '../../lib/toast';\nimport { cn, formatCurrency, formatDate } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nconst statusTabs: { value: string; label: string }[] = [\n { value: 'all', label: 'All' },\n { value: 'submitted', label: 'Submitted' },\n { value: 'countered', label: 'Countered' },\n { value: 'accepted', label: 'Accepted' },\n { value: 'rejected', label: 'Rejected' },\n { value: 'withdrawn', label: 'Withdrawn' },\n { value: 'expired', label: 'Expired' },\n];\n\nfunction statusVariant(status: OfferRow['status']): 'success' | 'warning' | 'error' | 'info' | 'muted' {\n switch (status) {\n case 'accepted':\n return 'success';\n case 'countered':\n return 'warning';\n case 'rejected':\n return 'error';\n case 'submitted':\n return 'info';\n default:\n return 'muted';\n }\n}\n\n// ---- Status timeline (submitted → countered → accepted / rejected / withdrawn) ----\n\ntype NodeTone = 'success' | 'warning' | 'error' | 'info' | 'muted';\ninterface TimelineNode {\n label: string;\n date: string | null;\n tone: NodeTone;\n filled: boolean;\n}\n\nconst toneFill: Record<NodeTone, string> = {\n success: 'bg-status-success',\n warning: 'bg-status-warning',\n error: 'bg-status-error',\n info: 'bg-status-info',\n muted: 'bg-text-secondary',\n};\n\nfunction buildTimeline(offer: OfferRow): TimelineNode[] {\n const submitted: TimelineNode = { label: 'Submitted', date: offer.createdAt, tone: 'info', filled: true };\n const respondedAt = offer.respondedAt;\n switch (offer.status) {\n case 'accepted':\n return [\n { ...submitted, tone: 'success' },\n ...(offer.counterAmount != null\n ? [{ label: 'Countered', date: null, tone: 'success' as NodeTone, filled: true }]\n : []),\n { label: 'Accepted', date: respondedAt, tone: 'success', filled: true },\n ];\n case 'countered':\n return [\n { ...submitted, tone: 'warning' },\n { label: 'Countered', date: respondedAt, tone: 'warning', filled: true },\n { label: 'Accepted', date: null, tone: 'muted', filled: false },\n ];\n case 'rejected':\n return [{ ...submitted }, { label: 'Rejected', date: respondedAt, tone: 'error', filled: true }];\n case 'withdrawn':\n return [{ ...submitted }, { label: 'Withdrawn', date: offer.updatedAt, tone: 'muted', filled: true }];\n case 'expired':\n return [{ ...submitted }, { label: 'Expired', date: offer.expiresAt, tone: 'muted', filled: true }];\n default:\n // submitted\n return [{ ...submitted }, { label: 'Accepted', date: null, tone: 'muted', filled: false }];\n }\n}\n\nfunction OfferTimeline({ offer }: { offer: OfferRow }) {\n const nodes = buildTimeline(offer);\n return (\n <ol className=\"flex items-start\" aria-label=\"Offer status timeline\">\n {nodes.map((node, index) => {\n const isLast = index === nodes.length - 1;\n const nextFilled = !isLast && nodes[index + 1].filled;\n return (\n <li key={node.label} className={cn('flex flex-col items-center', !isLast && 'flex-1')}>\n <div className=\"flex w-full items-center\">\n <span\n className={cn(\n 'flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full',\n node.filled ? toneFill[node.tone] : 'border-2 border-border-subtle bg-bg-surface'\n )}\n />\n {!isLast && (\n <span\n className={cn(\n 'mx-1 h-0.5 flex-1 rounded-full',\n node.filled && nextFilled ? toneFill[node.tone] : 'bg-border-subtle'\n )}\n />\n )}\n </div>\n <div className={cn('mt-1.5 text-center', isLast ? 'items-end' : '')}>\n <span className=\"block text-xs font-medium text-text-primary\">{node.label}</span>\n {node.date && <span className=\"block text-[0.65rem] text-text-secondary\">{formatDate(node.date)}</span>}\n </div>\n </li>\n );\n })}\n </ol>\n );\n}\n\nconst inputClass =\n 'h-10 w-full rounded-md border border-border-subtle bg-bg-surface px-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n\nexport function OffersPage() {\n useTrackPageView('housingvista.offer.viewed');\n const navigate = useNavigate();\n const pagination = useServerPagination(10);\n const [activeTab, setActiveTab] = useState('all');\n const { data: offers, loading } = useOffers({\n filter: activeTab === 'all' ? undefined : { status: activeTab },\n pagination: pagination.pagination,\n });\n // Unfiltered read powers the live tab counts + summary stats (real data).\n const { data: allOffers } = useOffers({ pagination: { limit: 100, offset: 0 } });\n const { respondOffer, loading: responding } = useRespondOffer();\n const { withdrawOffer, loading: withdrawing } = useWithdrawOffer();\n\n const [respondTarget, setRespondTarget] = useState<OfferRow | null>(null);\n const [action, setAction] = useState<'accept' | 'reject' | 'counter'>('accept');\n const [counterAmount, setCounterAmount] = useState('');\n const [notes, setNotes] = useState('');\n\n const openRespond = (offer: OfferRow) => {\n setRespondTarget(offer);\n setAction('accept');\n setCounterAmount(String(offer.amount));\n setNotes('');\n };\n\n const handleRespond = async (event: FormEvent) => {\n event.preventDefault();\n if (!respondTarget) return;\n if (action === 'counter') {\n const amount = Number(counterAmount);\n if (!Number.isFinite(amount) || amount <= 0) {\n notify.warning('Enter a valid counter amount');\n return;\n }\n }\n try {\n await respondOffer(respondTarget.id, {\n action,\n counterAmount: action === 'counter' ? Number(counterAmount) : undefined,\n notes: notes.trim() || undefined,\n });\n notify.success(\n 'Response sent',\n `Offer ${action === 'accept' ? 'accepted' : action === 'reject' ? 'rejected' : 'countered'}.`\n );\n setRespondTarget(null);\n } catch (err) {\n notify.error('Could not respond to offer', (err as Error).message);\n }\n };\n\n const handleWithdraw = async (offer: OfferRow) => {\n try {\n await withdrawOffer(offer.id);\n notify.success('Offer withdrawn');\n } catch (err) {\n notify.error('Could not withdraw offer', (err as Error).message);\n }\n };\n\n const countFor = (value: string): number =>\n value === 'all' ? allOffers.length : allOffers.filter((o) => o.status === value).length;\n\n const activeCount = allOffers.filter((o) => o.status === 'submitted' || o.status === 'countered').length;\n const acceptedCount = allOffers.filter((o) => o.status === 'accepted').length;\n\n return (\n <PageLayout\n title=\"Offers\"\n description=\"Track the offers you have made and respond to offers on your listings\"\n purpose=\"Every offer in one place: watch its status move from submitted to countered, accepted, or rejected — withdraw one you no longer want, or respond to offers coming in on properties you own.\"\n >\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <StatCard label=\"Total offers\" value={allOffers.length} icon={<Tag className=\"h-4 w-4\" />} />\n <StatCard label=\"Awaiting action\" value={activeCount} icon={<Repeat className=\"h-4 w-4\" />} />\n <StatCard label=\"Accepted\" value={acceptedCount} icon={<Check className=\"h-4 w-4\" />} />\n </div>\n </EmphasisPanel>\n\n <div className=\"-mx-1 flex flex-nowrap items-center gap-2 overflow-x-auto px-1 pb-1 sm:flex-wrap sm:overflow-visible\">\n {statusTabs.map((tab) => {\n const count = countFor(tab.value);\n const isActive = activeTab === tab.value;\n return (\n <button\n key={tab.value}\n onClick={() => {\n setActiveTab(tab.value);\n pagination.setPage(1);\n }}\n className={cn(\n 'inline-flex shrink-0 items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n isActive\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {tab.label}\n <span\n className={cn(\n 'inline-flex min-w-5 items-center justify-center rounded-full px-1.5 text-[0.65rem] font-semibold tabular-nums',\n isActive ? 'bg-action-primary-text/20 text-action-primary-text' : 'bg-bg-surface text-text-secondary'\n )}\n >\n {count}\n </span>\n </button>\n );\n })}\n </div>\n\n {offers.length === 0 && !loading ? (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Tag className=\"h-12 w-12\" />}\n title=\"No offers here\"\n description=\"Offers you submit on properties — and offers made on your listings — will appear here.\"\n action={<Button onClick={() => navigate('../properties')}>Browse properties</Button>}\n />\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 xl:grid-cols-2\">\n {offers.map((offer) => {\n const image = offer.property?.images.find((img) => img.isPrimary) ?? offer.property?.images[0];\n const canAct = offer.status === 'submitted' || offer.status === 'countered';\n return (\n <GlassCard\n key={offer.id}\n treatment={canAct ? 'insight' : 'plain'}\n glow={canAct}\n className=\"flex flex-col gap-4 p-5\"\n >\n <div className=\"flex items-start gap-3\">\n <button\n type=\"button\"\n onClick={() => offer.property && navigate(`../properties/${offer.property.id}`)}\n className=\"shrink-0\"\n aria-label={`Open ${offer.property?.title ?? 'property'}`}\n >\n {image ? (\n <img src={image.url} alt=\"\" className=\"h-16 w-16 rounded-lg object-cover\" />\n ) : (\n <div className=\"flex h-16 w-16 items-center justify-center rounded-lg bg-bg-sunken\">\n <Home className=\"h-6 w-6 text-text-secondary\" />\n </div>\n )}\n </button>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <button\n type=\"button\"\n onClick={() => offer.property && navigate(`../properties/${offer.property.id}`)}\n className=\"text-left\"\n >\n <h3 className=\"font-semibold text-text-primary hover:text-primary\">\n {offer.property?.title ?? 'Property'}\n </h3>\n {offer.property && (\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {offer.property.location.city}, {offer.property.location.state}\n </p>\n )}\n </button>\n <StatusBadge label={offer.status} variant={statusVariant(offer.status)} dot />\n </div>\n <div className=\"mt-2 flex items-baseline gap-3\">\n <span className=\"text-xl font-bold tabular-nums text-text-primary\">\n {formatCurrency(offer.amount, offer.currency)}\n </span>\n {offer.counterAmount != null && (\n <span className=\"text-sm text-text-secondary\">\n Counter{' '}\n <span className=\"font-semibold text-status-warning-text\">\n {formatCurrency(offer.counterAmount, offer.currency)}\n </span>\n </span>\n )}\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">Submitted {formatDate(offer.createdAt)}</p>\n {(offer.status === 'submitted' || offer.status === 'countered') && offer.expiresAt && (\n <p className=\"mt-0.5 text-xs text-text-secondary\">Expires {formatDate(offer.expiresAt)}</p>\n )}\n </div>\n </div>\n\n <div className=\"rounded-lg bg-bg-sunken/50 p-3\">\n <OfferTimeline offer={offer} />\n </div>\n\n {canAct && (\n <div className=\"flex flex-wrap gap-2\">\n <Button size=\"sm\" onClick={() => openRespond(offer)}>\n <Check className=\"h-4 w-4\" />\n <span className=\"ml-1\">Respond</span>\n </Button>\n <Button variant=\"outline\" size=\"sm\" disabled={withdrawing} onClick={() => handleWithdraw(offer)}>\n <Undo2 className=\"h-4 w-4\" />\n <span className=\"ml-1\">Withdraw</span>\n </Button>\n {offer.property && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigate(`../properties/${offer.property?.id}`)}\n >\n <span>View property</span>\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n )}\n </div>\n )}\n </GlassCard>\n );\n })}\n </div>\n\n <Pagination\n page={pagination.page}\n pageSize={pagination.pageSize}\n hasNextPage={offers.length >= pagination.pageSize}\n hasPreviousPage={pagination.page > 1}\n onPageChange={pagination.setPage}\n onPageSizeChange={pagination.setPageSize}\n />\n </div>\n )}\n\n {/* Respond to offer */}\n <ResponsiveDialog open={respondTarget !== null} onOpenChange={(open) => !open && setRespondTarget(null)}>\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <form onSubmit={handleRespond}>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Respond to offer</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {respondTarget?.property?.title\n ? `Offer of ${formatCurrency(respondTarget.amount, respondTarget.currency)} on ${respondTarget.property.title}.`\n : 'Accept, counter, or reject this offer.'}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3 py-2\">\n <div className=\"flex flex-wrap gap-2\">\n {(['accept', 'counter', 'reject'] as const).map((value) => (\n <button\n key={value}\n type=\"button\"\n onClick={() => setAction(value)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-sm font-medium capitalize transition-colors',\n action === value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {value === 'accept' && <Check className=\"h-4 w-4\" />}\n {value === 'counter' && <Repeat className=\"h-4 w-4\" />}\n {value === 'reject' && <X className=\"h-4 w-4\" />}\n {value}\n </button>\n ))}\n </div>\n {action === 'counter' && (\n <div>\n <label className=\"mb-1.5 block text-sm font-medium text-text-primary\">Counter amount *</label>\n <input\n type=\"number\"\n min={1}\n step=\"any\"\n value={counterAmount}\n onChange={(event) => setCounterAmount(event.target.value)}\n className={inputClass}\n />\n </div>\n )}\n <div>\n <label className=\"mb-1.5 block text-sm font-medium text-text-primary\">Note (optional)</label>\n <textarea\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n rows={2}\n className=\"w-full resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setRespondTarget(null)}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={responding}>\n {responding ? 'Sending…' : 'Send response'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"OffersPage.js","names":[],"sources":["../../../../src/housingvista/pages/offers/OffersPage.tsx"],"sourcesContent":["/**\n * Offers — the buyer/owner surface for property offers. Buyers track offers they\n * submitted (status timeline submitted→countered→accepted/rejected/withdrawn)\n * and can withdraw; owners can respond (accept / counter / reject). Server-side\n * status filter + pagination, with live per-status tab counts driven off a real\n * unfiltered read (no fabricated numbers).\n * @module housingvista\n */\n\nimport { useState, type FormEvent } from 'react';\nimport { useNavigate } from 'react-router';\nimport { Tag, Check, X, Repeat, Undo2, MapPin, ArrowRight, Home, MessageSquare } from 'lucide-react';\nimport {\n Button,\n Pagination,\n EmphasisPanel,\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout, StatCard, StatusBadge, EmptyState } from '../../components';\nimport { ContactOwnerDialog } from '../../components';\nimport { useOffers, useRespondOffer, useWithdrawOffer, type OfferRow } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { notify } from '../../lib/toast';\nimport { cn, formatCurrency, formatDate } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nconst statusTabs: { value: string; label: string }[] = [\n { value: 'all', label: 'All' },\n { value: 'submitted', label: 'Submitted' },\n { value: 'countered', label: 'Countered' },\n { value: 'accepted', label: 'Accepted' },\n { value: 'rejected', label: 'Rejected' },\n { value: 'withdrawn', label: 'Withdrawn' },\n { value: 'expired', label: 'Expired' },\n];\n\nfunction statusVariant(status: OfferRow['status']): 'success' | 'warning' | 'error' | 'info' | 'muted' {\n switch (status) {\n case 'accepted':\n return 'success';\n case 'countered':\n return 'warning';\n case 'rejected':\n return 'error';\n case 'submitted':\n return 'info';\n default:\n return 'muted';\n }\n}\n\n// ---- Status timeline (submitted → countered → accepted / rejected / withdrawn) ----\n\ntype NodeTone = 'success' | 'warning' | 'error' | 'info' | 'muted';\ninterface TimelineNode {\n label: string;\n date: string | null;\n tone: NodeTone;\n filled: boolean;\n}\n\nconst toneFill: Record<NodeTone, string> = {\n success: 'bg-status-success',\n warning: 'bg-status-warning',\n error: 'bg-status-error',\n info: 'bg-status-info',\n muted: 'bg-text-secondary',\n};\n\nfunction buildTimeline(offer: OfferRow): TimelineNode[] {\n const submitted: TimelineNode = { label: 'Submitted', date: offer.createdAt, tone: 'info', filled: true };\n const respondedAt = offer.respondedAt;\n switch (offer.status) {\n case 'accepted':\n return [\n { ...submitted, tone: 'success' },\n ...(offer.counterAmount != null\n ? [{ label: 'Countered', date: null, tone: 'success' as NodeTone, filled: true }]\n : []),\n { label: 'Accepted', date: respondedAt, tone: 'success', filled: true },\n ];\n case 'countered':\n return [\n { ...submitted, tone: 'warning' },\n { label: 'Countered', date: respondedAt, tone: 'warning', filled: true },\n { label: 'Accepted', date: null, tone: 'muted', filled: false },\n ];\n case 'rejected':\n return [{ ...submitted }, { label: 'Rejected', date: respondedAt, tone: 'error', filled: true }];\n case 'withdrawn':\n return [{ ...submitted }, { label: 'Withdrawn', date: offer.updatedAt, tone: 'muted', filled: true }];\n case 'expired':\n return [{ ...submitted }, { label: 'Expired', date: offer.expiresAt, tone: 'muted', filled: true }];\n default:\n // submitted\n return [{ ...submitted }, { label: 'Accepted', date: null, tone: 'muted', filled: false }];\n }\n}\n\nfunction OfferTimeline({ offer }: { offer: OfferRow }) {\n const nodes = buildTimeline(offer);\n return (\n <ol className=\"flex items-start\" aria-label=\"Offer status timeline\">\n {nodes.map((node, index) => {\n const isLast = index === nodes.length - 1;\n const nextFilled = !isLast && nodes[index + 1].filled;\n return (\n <li key={node.label} className={cn('flex flex-col items-center', !isLast && 'flex-1')}>\n <div className=\"flex w-full items-center\">\n <span\n className={cn(\n 'flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full',\n node.filled ? toneFill[node.tone] : 'border-2 border-border-subtle bg-bg-surface'\n )}\n />\n {!isLast && (\n <span\n className={cn(\n 'mx-1 h-0.5 flex-1 rounded-full',\n node.filled && nextFilled ? toneFill[node.tone] : 'bg-border-subtle'\n )}\n />\n )}\n </div>\n <div className={cn('mt-1.5 text-center', isLast ? 'items-end' : '')}>\n <span className=\"block text-xs font-medium text-text-primary\">{node.label}</span>\n {node.date && <span className=\"block text-[0.65rem] text-text-secondary\">{formatDate(node.date)}</span>}\n </div>\n </li>\n );\n })}\n </ol>\n );\n}\n\nconst inputClass =\n 'h-10 w-full rounded-md border border-border-subtle bg-bg-surface px-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\n\nexport function OffersPage() {\n useTrackPageView('housingvista.offer.viewed');\n const navigate = useNavigate();\n const pagination = useServerPagination(10);\n const [activeTab, setActiveTab] = useState('all');\n const { data: offers, loading } = useOffers({\n filter: activeTab === 'all' ? undefined : { status: activeTab },\n pagination: pagination.pagination,\n });\n // Unfiltered read powers the live tab counts + summary stats (real data).\n const { data: allOffers } = useOffers({ pagination: { limit: 100, offset: 0 } });\n const { respondOffer, loading: responding } = useRespondOffer();\n const { withdrawOffer, loading: withdrawing } = useWithdrawOffer();\n\n const [respondTarget, setRespondTarget] = useState<OfferRow | null>(null);\n const [messageTarget, setMessageTarget] = useState<OfferRow | null>(null);\n const [action, setAction] = useState<'accept' | 'reject' | 'counter'>('accept');\n const [counterAmount, setCounterAmount] = useState('');\n const [notes, setNotes] = useState('');\n\n const openRespond = (offer: OfferRow) => {\n setRespondTarget(offer);\n setAction('accept');\n setCounterAmount(String(offer.amount));\n setNotes('');\n };\n\n const handleRespond = async (event: FormEvent) => {\n event.preventDefault();\n if (!respondTarget) return;\n if (action === 'counter') {\n const amount = Number(counterAmount);\n if (!Number.isFinite(amount) || amount <= 0) {\n notify.warning('Enter a valid counter amount');\n return;\n }\n }\n try {\n await respondOffer(respondTarget.id, {\n action,\n counterAmount: action === 'counter' ? Number(counterAmount) : undefined,\n notes: notes.trim() || undefined,\n });\n notify.success(\n 'Response sent',\n `Offer ${action === 'accept' ? 'accepted' : action === 'reject' ? 'rejected' : 'countered'}.`\n );\n setRespondTarget(null);\n } catch (err) {\n notify.error('Could not respond to offer', (err as Error).message);\n }\n };\n\n const handleWithdraw = async (offer: OfferRow) => {\n try {\n await withdrawOffer(offer.id);\n notify.success('Offer withdrawn');\n } catch (err) {\n notify.error('Could not withdraw offer', (err as Error).message);\n }\n };\n\n const countFor = (value: string): number =>\n value === 'all' ? allOffers.length : allOffers.filter((o) => o.status === value).length;\n\n const activeCount = allOffers.filter((o) => o.status === 'submitted' || o.status === 'countered').length;\n const acceptedCount = allOffers.filter((o) => o.status === 'accepted').length;\n\n return (\n <PageLayout\n title=\"Offers\"\n description=\"Track the offers you have made and respond to offers on your listings\"\n purpose=\"Every offer in one place: watch its status move from submitted to countered, accepted, or rejected — withdraw one you no longer want, or respond to offers coming in on properties you own.\"\n >\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <StatCard label=\"Total offers\" value={allOffers.length} icon={<Tag className=\"h-4 w-4\" />} />\n <StatCard label=\"Awaiting action\" value={activeCount} icon={<Repeat className=\"h-4 w-4\" />} />\n <StatCard label=\"Accepted\" value={acceptedCount} icon={<Check className=\"h-4 w-4\" />} />\n </div>\n </EmphasisPanel>\n\n <div className=\"-mx-1 flex flex-nowrap items-center gap-2 overflow-x-auto px-1 pb-1 sm:flex-wrap sm:overflow-visible\">\n {statusTabs.map((tab) => {\n const count = countFor(tab.value);\n const isActive = activeTab === tab.value;\n return (\n <button\n key={tab.value}\n onClick={() => {\n setActiveTab(tab.value);\n pagination.setPage(1);\n }}\n className={cn(\n 'inline-flex shrink-0 items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium transition-colors',\n isActive\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {tab.label}\n <span\n className={cn(\n 'inline-flex min-w-5 items-center justify-center rounded-full px-1.5 text-[0.65rem] font-semibold tabular-nums',\n isActive ? 'bg-action-primary-text/20 text-action-primary-text' : 'bg-bg-surface text-text-secondary'\n )}\n >\n {count}\n </span>\n </button>\n );\n })}\n </div>\n\n {offers.length === 0 && !loading ? (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Tag className=\"h-12 w-12\" />}\n title=\"No offers here\"\n description=\"Offers you submit on properties — and offers made on your listings — will appear here.\"\n action={<Button onClick={() => navigate('../properties')}>Browse properties</Button>}\n />\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 xl:grid-cols-2\">\n {offers.map((offer) => {\n const image = offer.property?.images.find((img) => img.isPrimary) ?? offer.property?.images[0];\n const canAct = offer.status === 'submitted' || offer.status === 'countered';\n return (\n <GlassCard\n key={offer.id}\n treatment={canAct ? 'insight' : 'plain'}\n glow={canAct}\n className=\"flex flex-col gap-4 p-5\"\n >\n <div className=\"flex items-start gap-3\">\n <button\n type=\"button\"\n onClick={() => offer.property && navigate(`../properties/${offer.property.id}`)}\n className=\"shrink-0\"\n aria-label={`Open ${offer.property?.title ?? 'property'}`}\n >\n {image ? (\n <img src={image.url} alt=\"\" className=\"h-16 w-16 rounded-lg object-cover\" />\n ) : (\n <div className=\"flex h-16 w-16 items-center justify-center rounded-lg bg-bg-sunken\">\n <Home className=\"h-6 w-6 text-text-secondary\" />\n </div>\n )}\n </button>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <button\n type=\"button\"\n onClick={() => offer.property && navigate(`../properties/${offer.property.id}`)}\n className=\"text-left\"\n >\n <h3 className=\"font-semibold text-text-primary hover:text-primary\">\n {offer.property?.title ?? 'Property'}\n </h3>\n {offer.property && (\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {offer.property.location.city}, {offer.property.location.state}\n </p>\n )}\n </button>\n <StatusBadge label={offer.status} variant={statusVariant(offer.status)} dot />\n </div>\n <div className=\"mt-2 flex items-baseline gap-3\">\n <span className=\"text-xl font-bold tabular-nums text-text-primary\">\n {formatCurrency(offer.amount, offer.currency)}\n </span>\n {offer.counterAmount != null && (\n <span className=\"text-sm text-text-secondary\">\n Counter{' '}\n <span className=\"font-semibold text-status-warning-text\">\n {formatCurrency(offer.counterAmount, offer.currency)}\n </span>\n </span>\n )}\n </div>\n <p className=\"mt-0.5 text-xs text-text-secondary\">Submitted {formatDate(offer.createdAt)}</p>\n {(offer.status === 'submitted' || offer.status === 'countered') && offer.expiresAt && (\n <p className=\"mt-0.5 text-xs text-text-secondary\">Expires {formatDate(offer.expiresAt)}</p>\n )}\n </div>\n </div>\n\n <div className=\"rounded-lg bg-bg-sunken/50 p-3\">\n <OfferTimeline offer={offer} />\n </div>\n\n {(canAct || offer.property) && (\n <div className=\"flex flex-wrap gap-2\">\n {canAct && (\n <Button size=\"sm\" onClick={() => openRespond(offer)}>\n <Check className=\"h-4 w-4\" />\n <span className=\"ml-1\">Respond</span>\n </Button>\n )}\n {canAct && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={withdrawing}\n onClick={() => handleWithdraw(offer)}\n >\n <Undo2 className=\"h-4 w-4\" />\n <span className=\"ml-1\">Withdraw</span>\n </Button>\n )}\n {offer.property && (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setMessageTarget(offer)}>\n <MessageSquare className=\"h-4 w-4\" />\n <span className=\"ml-1\">Message</span>\n </Button>\n )}\n {offer.property && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigate(`../properties/${offer.property?.id}`)}\n >\n <span>View property</span>\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n )}\n </div>\n )}\n </GlassCard>\n );\n })}\n </div>\n\n <Pagination\n page={pagination.page}\n pageSize={pagination.pageSize}\n hasNextPage={offers.length >= pagination.pageSize}\n hasPreviousPage={pagination.page > 1}\n onPageChange={pagination.setPage}\n onPageSizeChange={pagination.setPageSize}\n />\n </div>\n )}\n\n {/* Message the other party about this offer — the conversation thread is\n property-scoped (ContactOwnerDialog), the same thread both parties\n already reach from the property detail page; keyed by property id so\n switching offers never reuses a stale conversation (BOFF-8220). */}\n {messageTarget?.property && (\n <ContactOwnerDialog\n key={messageTarget.property.id}\n open={messageTarget !== null}\n onOpenChange={(open) => !open && setMessageTarget(null)}\n entityId={messageTarget.property.id}\n entityType=\"housingvista-property\"\n title={messageTarget.property.title}\n pagePath=\"/housingvista/offers\"\n heading=\"Message\"\n description={`Discuss ${messageTarget.property.title} with the other party. The thread also appears in Messages.`}\n />\n )}\n\n {/* Respond to offer */}\n <ResponsiveDialog open={respondTarget !== null} onOpenChange={(open) => !open && setRespondTarget(null)}>\n <ResponsiveDialogContent className=\"sm:max-w-md\">\n <form onSubmit={handleRespond}>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Respond to offer</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {respondTarget?.property?.title\n ? `Offer of ${formatCurrency(respondTarget.amount, respondTarget.currency)} on ${respondTarget.property.title}.`\n : 'Accept, counter, or reject this offer.'}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n <div className=\"space-y-3 py-2\">\n <div className=\"flex flex-wrap gap-2\">\n {(['accept', 'counter', 'reject'] as const).map((value) => (\n <button\n key={value}\n type=\"button\"\n onClick={() => setAction(value)}\n className={cn(\n 'inline-flex items-center gap-1 rounded-md px-3 py-1.5 text-sm font-medium capitalize transition-colors',\n action === value\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-sunken text-text-secondary hover:text-text-primary'\n )}\n >\n {value === 'accept' && <Check className=\"h-4 w-4\" />}\n {value === 'counter' && <Repeat className=\"h-4 w-4\" />}\n {value === 'reject' && <X className=\"h-4 w-4\" />}\n {value}\n </button>\n ))}\n </div>\n {action === 'counter' && (\n <div>\n <label className=\"mb-1.5 block text-sm font-medium text-text-primary\">Counter amount *</label>\n <input\n type=\"number\"\n min={1}\n step=\"any\"\n value={counterAmount}\n onChange={(event) => setCounterAmount(event.target.value)}\n className={inputClass}\n />\n </div>\n )}\n <div>\n <label className=\"mb-1.5 block text-sm font-medium text-text-primary\">Note (optional)</label>\n <textarea\n value={notes}\n onChange={(event) => setNotes(event.target.value)}\n rows={2}\n className=\"w-full resize-none rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => setRespondTarget(null)}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={responding}>\n {responding ? 'Sending…' : 'Send response'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AddPropertyPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/AddPropertyPage.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiEH,eAAO,MAAM,eAAe,mCA4rB3B,CAAC"}
1
+ {"version":3,"file":"AddPropertyPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/properties/AddPropertyPage.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAmEH,eAAO,MAAM,eAAe,mCAwtB3B,CAAC"}