@aglyn/plugins-crm 1.0.0-beta.199 → 1.0.0-beta.201

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.
Files changed (169) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/activity-list.d.ts +36 -5
  3. package/src/lib/components/activity-list.js +159 -137
  4. package/src/lib/components/activity-list.js.map +1 -1
  5. package/src/lib/components/activity-timeline.d.ts +84 -0
  6. package/src/lib/components/activity-timeline.js +224 -0
  7. package/src/lib/components/activity-timeline.js.map +1 -0
  8. package/src/lib/components/companies-section.d.ts +6 -6
  9. package/src/lib/components/companies-section.js +54 -84
  10. package/src/lib/components/companies-section.js.map +1 -1
  11. package/src/lib/components/company-edit-drawer.js +17 -4
  12. package/src/lib/components/company-edit-drawer.js.map +1 -1
  13. package/src/lib/components/company-picker.js +6 -2
  14. package/src/lib/components/company-picker.js.map +1 -1
  15. package/src/lib/components/company-properties-card.js +53 -36
  16. package/src/lib/components/company-properties-card.js.map +1 -1
  17. package/src/lib/components/contact-associations-card.d.ts +3 -2
  18. package/src/lib/components/contact-associations-card.js +12 -12
  19. package/src/lib/components/contact-associations-card.js.map +1 -1
  20. package/src/lib/components/contact-detail-page.d.ts +4 -3
  21. package/src/lib/components/contact-detail-page.js +18 -12
  22. package/src/lib/components/contact-detail-page.js.map +1 -1
  23. package/src/lib/components/contact-list-columns.d.ts +3 -5
  24. package/src/lib/components/contact-list-columns.js +6 -8
  25. package/src/lib/components/contact-list-columns.js.map +1 -1
  26. package/src/lib/components/contact-properties-card.d.ts +21 -4
  27. package/src/lib/components/contact-properties-card.js +129 -29
  28. package/src/lib/components/contact-properties-card.js.map +1 -1
  29. package/src/lib/components/contact-timeline-card.d.ts +12 -6
  30. package/src/lib/components/contact-timeline-card.js +132 -174
  31. package/src/lib/components/contact-timeline-card.js.map +1 -1
  32. package/src/lib/components/contacts-bulk-bar.js +10 -1
  33. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  34. package/src/lib/components/contacts-section.js +282 -331
  35. package/src/lib/components/contacts-section.js.map +1 -1
  36. package/src/lib/components/crm-email-state-chip.js +4 -1
  37. package/src/lib/components/crm-email-state-chip.js.map +1 -1
  38. package/src/lib/components/crm-next-activity-column.d.ts +4 -2
  39. package/src/lib/components/crm-next-activity-column.js +6 -3
  40. package/src/lib/components/crm-next-activity-column.js.map +1 -1
  41. package/src/lib/components/deal-edit-drawer.js +8 -4
  42. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  43. package/src/lib/components/deal-products-card.js +15 -13
  44. package/src/lib/components/deal-products-card.js.map +1 -1
  45. package/src/lib/components/deal-properties-card.js +7 -0
  46. package/src/lib/components/deal-properties-card.js.map +1 -1
  47. package/src/lib/components/deal-stage-card.js +31 -29
  48. package/src/lib/components/deal-stage-card.js.map +1 -1
  49. package/src/lib/components/deals-section.d.ts +2 -2
  50. package/src/lib/components/deals-section.js +67 -97
  51. package/src/lib/components/deals-section.js.map +1 -1
  52. package/src/lib/components/fields-section.d.ts +4 -0
  53. package/src/lib/components/fields-section.js +368 -154
  54. package/src/lib/components/fields-section.js.map +1 -1
  55. package/src/lib/components/form-contact-fields-card.js +24 -20
  56. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  57. package/src/lib/components/lead-campaigns-card.d.ts +1 -1
  58. package/src/lib/components/lead-campaigns-card.js +43 -38
  59. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  60. package/src/lib/components/lead-properties-card.d.ts +14 -6
  61. package/src/lib/components/lead-properties-card.js +323 -311
  62. package/src/lib/components/lead-properties-card.js.map +1 -1
  63. package/src/lib/components/lead-surfaces-note.js +13 -2
  64. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  65. package/src/lib/components/leads-bulk-bar.js +16 -3
  66. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  67. package/src/lib/components/leads-section.d.ts +2 -1
  68. package/src/lib/components/leads-section.js +110 -148
  69. package/src/lib/components/leads-section.js.map +1 -1
  70. package/src/lib/components/linked-deals-card.js +12 -10
  71. package/src/lib/components/linked-deals-card.js.map +1 -1
  72. package/src/lib/components/recent-activity-feed.d.ts +12 -8
  73. package/src/lib/components/recent-activity-feed.js +54 -29
  74. package/src/lib/components/recent-activity-feed.js.map +1 -1
  75. package/src/lib/components/record-activity-card.d.ts +4 -2
  76. package/src/lib/components/record-activity-card.js +29 -11
  77. package/src/lib/components/record-activity-card.js.map +1 -1
  78. package/src/lib/components/reports/activity-card.js +18 -12
  79. package/src/lib/components/reports/activity-card.js.map +1 -1
  80. package/src/lib/components/reports/lead-funnel-card.js +47 -37
  81. package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
  82. package/src/lib/components/reports/pipeline-card.js +22 -16
  83. package/src/lib/components/reports/pipeline-card.js.map +1 -1
  84. package/src/lib/components/reports/report-export.d.ts +4 -11
  85. package/src/lib/components/reports/report-export.js +16 -33
  86. package/src/lib/components/reports/report-export.js.map +1 -1
  87. package/src/lib/components/reports/source-conversion-card.js +18 -12
  88. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  89. package/src/lib/components/reports/tasks-card.js +35 -25
  90. package/src/lib/components/reports/tasks-card.js.map +1 -1
  91. package/src/lib/components/tasks-section.js +151 -144
  92. package/src/lib/components/tasks-section.js.map +1 -1
  93. package/src/lib/constants/company-filters.d.ts +13 -0
  94. package/src/lib/constants/company-filters.js +47 -17
  95. package/src/lib/constants/company-filters.js.map +1 -1
  96. package/src/lib/constants/contact-filters.d.ts +61 -10
  97. package/src/lib/constants/contact-filters.js +373 -141
  98. package/src/lib/constants/contact-filters.js.map +1 -1
  99. package/src/lib/constants/deal-filters.d.ts +44 -0
  100. package/src/lib/constants/deal-filters.js +78 -0
  101. package/src/lib/constants/deal-filters.js.map +1 -0
  102. package/src/lib/constants/field-list-query.d.ts +23 -0
  103. package/src/lib/constants/field-list-query.js +80 -0
  104. package/src/lib/constants/field-list-query.js.map +1 -0
  105. package/src/lib/hooks/use-crm-list-query.d.ts +68 -0
  106. package/src/lib/hooks/use-crm-list-query.js +182 -0
  107. package/src/lib/hooks/use-crm-list-query.js.map +1 -0
  108. package/src/lib/hooks/use-crm-tasks.d.ts +23 -6
  109. package/src/lib/hooks/use-crm-tasks.js +44 -42
  110. package/src/lib/hooks/use-crm-tasks.js.map +1 -1
  111. package/src/lib/hooks/use-deals.d.ts +2 -34
  112. package/src/lib/hooks/use-deals.js +3 -62
  113. package/src/lib/hooks/use-deals.js.map +1 -1
  114. package/src/lib/model/crm-list-query.d.ts +91 -0
  115. package/src/lib/model/crm-list-query.js +182 -0
  116. package/src/lib/model/crm-list-query.js.map +1 -0
  117. package/src/lib/model/lead-filters.d.ts +50 -44
  118. package/src/lib/model/lead-filters.js +194 -88
  119. package/src/lib/model/lead-filters.js.map +1 -1
  120. package/src/lib/model/list-fields-api.d.ts +26 -0
  121. package/src/lib/model/list-fields-api.js +47 -0
  122. package/src/lib/model/list-fields-api.js.map +1 -0
  123. package/src/lib/model/list-fields.d.ts +47 -0
  124. package/src/lib/model/list-fields.js +39 -0
  125. package/src/lib/model/list-fields.js.map +1 -0
  126. package/src/lib/model/task-views.d.ts +15 -10
  127. package/src/lib/model/task-views.js +68 -9
  128. package/src/lib/model/task-views.js.map +1 -1
  129. package/src/lib/server/capture-contact.js +7 -5
  130. package/src/lib/server/capture-contact.js.map +1 -1
  131. package/src/lib/server/companies-import.js +3 -3
  132. package/src/lib/server/companies-import.js.map +1 -1
  133. package/src/lib/server/company-delete.js +3 -1
  134. package/src/lib/server/company-delete.js.map +1 -1
  135. package/src/lib/server/consent-group-participant.js +4 -0
  136. package/src/lib/server/consent-group-participant.js.map +1 -1
  137. package/src/lib/server/contact-update.js +14 -8
  138. package/src/lib/server/contact-update.js.map +1 -1
  139. package/src/lib/server/contacts-import.js +4 -2
  140. package/src/lib/server/contacts-import.js.map +1 -1
  141. package/src/lib/server/deals-import.js +4 -2
  142. package/src/lib/server/deals-import.js.map +1 -1
  143. package/src/lib/server/erase-person.js +2 -1
  144. package/src/lib/server/erase-person.js.map +1 -1
  145. package/src/lib/server/lead-create.js +4 -1
  146. package/src/lib/server/lead-create.js.map +1 -1
  147. package/src/lib/server/lead-source-values.js +9 -5
  148. package/src/lib/server/lead-source-values.js.map +1 -1
  149. package/src/lib/server/leads-import.js +4 -1
  150. package/src/lib/server/leads-import.js.map +1 -1
  151. package/src/lib/server/list-fields-routes.d.ts +29 -0
  152. package/src/lib/server/list-fields-routes.js +85 -0
  153. package/src/lib/server/list-fields-routes.js.map +1 -0
  154. package/src/lib/server/record-email-state.js +5 -2
  155. package/src/lib/server/record-email-state.js.map +1 -1
  156. package/src/lib/server/record-timeline.js +3 -2
  157. package/src/lib/server/record-timeline.js.map +1 -1
  158. package/src/lib/server/task-routes.js +6 -3
  159. package/src/lib/server/task-routes.js.map +1 -1
  160. package/src/lib/server/tasks-import.js +5 -2
  161. package/src/lib/server/tasks-import.js.map +1 -1
  162. package/src/lib/server.js +10 -1
  163. package/src/lib/server.js.map +1 -1
  164. package/src/lib/components/contact-custom-fields-card.d.ts +0 -39
  165. package/src/lib/components/contact-custom-fields-card.js +0 -204
  166. package/src/lib/components/contact-custom-fields-card.js.map +0 -1
  167. package/src/lib/hooks/use-org-leads.d.ts +0 -50
  168. package/src/lib/hooks/use-org-leads.js +0 -104
  169. package/src/lib/hooks/use-org-leads.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","dealHasLineItems","findOrgMember","nameSearchKey","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-edit-drawer.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n dealHasLineItems,\n findOrgMember,\n nameSearchKey,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport { NavigationDrawerComponent } from '@aglyn/shared-ui-jsx/components/navigation-drawer.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Autocomplete,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n addDoc,\n collection,\n deleteField,\n doc,\n endAt,\n getDocs,\n limit,\n orderBy,\n query,\n startAt,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmActivityLogger } from '../hooks/use-crm-activity-logger'\nimport { useCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport {\n type CrmOrgDoc,\n crmScopeListable,\n crmVisibleToClause,\n useCrmScope,\n} from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport { contactPrimaryGroup } from '../model/contact-record'\nimport {\n type CrmCustomDraft,\n crmCustomDraftDocument,\n crmCustomDraftMissingRequired,\n crmCustomDraftValue,\n crmCustomDraftWrites,\n} from '../model/crm-custom-draft'\nimport { CrmCustomFieldControl } from './crm-custom-field-control'\nimport { CrmSitePicker } from './crm-site-picker'\nimport { CRM_CLIENT_SEARCH_FIELDS, crmClientListFields } from '../model/crm-list-query'\nimport {\n DEAL_CURRENCIES,\n type DealDoc,\n openStages,\n type PipelineDoc,\n} from '../model/deal-board-model'\nimport {\n type ContactChoice,\n contactChoicesFor,\n DEAL_NOTES_MAX,\n DEAL_TITLE_MAX,\n dealDocumentFromForm,\n dealFormFromDoc,\n dealFormProblem,\n type DealFormValues,\n dealPatchFromForm,\n emptyDealForm,\n} from '../model/deal-form-model'\n\n/**\n * How many recent contacts the picker matches against. The window is the\n * ask — it is read when the drawer opens, not when the section does — and\n * a person outside it is still linkable by pasting their address, which\n * the match tests as a prefix of the email.\n */\nconst CONTACT_WINDOW = 300\n\n/** Companies offered per keystroke; a longer list is a scroll, not a pick. */\nconst COMPANY_MATCHES = 8\n\n/** A company as the picker offers it. */\ninterface CompanyChoice {\n id: string\n name: string\n}\n\nexport interface DealEditDrawerProps {\n open: boolean\n onClose: () => void\n /**\n * The site the drawer is opened under, or `null` at the organization\n * level (AGL-2630), where a NEW deal asks which site captures it and an\n * edit keeps the deal's own.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The deal being edited; absent for a new one. */\n deal?: DealDoc | null\n /** Links and values a new deal opens with — a contact's page passes itself. */\n defaults?: Partial<DealFormValues>\n pipelines: PipelineDoc[]\n /** Where a new deal lands when nobody picks a pipeline. */\n defaultPipeline: PipelineDoc | null\n /** The listener's verdict on the row being edited, for the stale-seed guard. */\n unreadable?: boolean\n fromCache?: boolean\n /** After a save, with the id of the deal written. */\n onSaved?: (dealId: string, mode: 'create' | 'edit') => void\n}\n\n/**\n * The one form a deal is created and edited through (AGL-2598).\n *\n * ## What it writes, and what it does not\n *\n * Client-direct against the rules, like a contact's notes: the title, the\n * amount and its currency, the expected close, the owner, the linked\n * contact and company, and the notes. The stage and the status are shown on\n * a new deal — it has to start somewhere — and never on an existing one,\n * because a stage change is the moment an automation listens for and only\n * the server route can emit that event. A drawer that wrote `stageId`\n * directly would move a deal without anybody hearing it.\n *\n * Nor the amount of a deal that has line items (AGL-2620): it is their\n * sum, the products card on the deal's page owns it, and the field here\n * is read-only with a caption saying so. The pipeline picker on a new\n * deal lists every ACTIVE pipeline (`pipelines`) and opens on the one the\n * caller was looking at.\n *\n * ## The pickers\n *\n * The owner is picked from the org's roster, the company by a server-side\n * prefix on `nameLower` (indexed with the scope), and the contact from a\n * bounded window of the newest contacts matched in memory — see\n * `contactChoicesFor` for why the contact search cannot be a query. Both\n * pickers copy the chosen NAME onto the deal beside the id, so a board of\n * cards can caption itself without a read per card.\n */\nexport function DealEditDrawer(props: DealEditDrawerProps) {\n const {\n open,\n onClose,\n hostId,\n org,\n deal,\n defaults,\n pipelines,\n defaultPipeline,\n unreadable = false,\n fromCache = false,\n onSaved,\n } = props\n const mode: 'create' | 'edit' = deal ? 'edit' : 'create'\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { data: user } = useUser()\n const { orgId, consentGroup, visibleTo, createHostId, createTokens } =\n useCrmScope({ hostId, org })\n // The feed the act is logged in is the level it was PERFORMED at\n // (AGL-2738): this site's under a site, the organization's at the org\n // hub. `createHostId` is where the deal is STAMPED, which at the hub is a\n // site the reader picked and may hold no role on — a different question\n // from where the line belongs, and a gate the record write never faced.\n const logActivity = useCrmActivityLogger(hostId)\n const roster = useOrgMemberDirectory(open ? orgId : null)\n\n const [values, setValues] = useState<DealFormValues>(() =>\n emptyDealForm(defaultPipeline, defaults),\n )\n const owner = findOrgMember(roster.members, values.ownerUid)\n const [busy, setBusy] = useState(false)\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's deal definitions, and the keys\n * the reader has touched over what the deal stores — saved as the\n * difference (`crm-custom-draft`) in the same write as the fixed fields.\n */\n const fields = useContactFieldDefinitions(open ? orgId : null, 'deal')\n const storedCustom = useMemo(() => deal?.custom ?? {}, [deal?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n\n // Re-seeded on every open: an existing deal's stored values, or a blank\n // form aimed at the default pipeline with whatever the opener preselected.\n useEffect(() => {\n if (!open) return\n setValues(deal ? dealFormFromDoc(deal) : emptyDealForm(defaultPipeline, defaults))\n setCustom({})\n }, [open, deal, defaultPipeline, defaults])\n\n const update = useCallback(\n (patch: Partial<DealFormValues>) =>\n setValues((current) => ({ ...current, ...patch })),\n [],\n )\n\n const pipeline = useMemo(\n () =>\n pipelines.find((entry) => entry.$id === values.pipelineId) ??\n defaultPipeline,\n [pipelines, values.pipelineId, defaultPipeline],\n )\n const stages = useMemo(() => openStages(pipeline), [pipeline])\n\n /*\n * THE CONTACT WINDOW: the newest contacts this viewer may see, read only\n * while the drawer is open. Scoped and ordered like the contacts list —\n * the same `(visibleTo, updatedAt DESC)` index — and bounded, because the\n * picker's job is to find one person, not to list the audience.\n */\n const { data: contactRows } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n open && orgId && crmScopeListable(visibleTo)\n ? query(\n collection(firestore, 'orgs', orgId, 'contacts'),\n ...crmVisibleToClause(visibleTo),\n orderBy('updatedAt', 'desc'),\n limit(CONTACT_WINDOW),\n )\n : null,\n [firestore, open, orgId, visibleTo],\n { idField: '$id' },\n )\n const [contactQuery, setContactQuery] = useState('')\n // Named through the viewing group under a site; at the organization level\n // through each person's own primary holder.\n const contactChoices = useMemo(\n () =>\n contactChoicesFor(\n contactQuery,\n contactRows ?? [],\n consentGroup?.groupId ??\n ((row) => contactPrimaryGroup(row, org as Record<string, unknown>).groupId),\n ),\n [contactQuery, contactRows, consentGroup, org],\n )\n const contactValue: ContactChoice | null = values.contactId\n ? {\n id: values.contactId,\n name: values.contactName,\n email:\n contactChoices.find((choice) => choice.id === values.contactId)?.email ?? '',\n }\n : null\n\n /*\n * THE COMPANY SEARCH: a prefix on `nameLower` with the scope, which the\n * companies collection indexes. Debounced so a name typed at speed is one\n * query rather than one per letter.\n */\n const [companyQuery, setCompanyQuery] = useState('')\n const [companyChoices, setCompanyChoices] = useState<CompanyChoice[]>([])\n useEffect(() => {\n if (!open || !orgId || !crmScopeListable(visibleTo)) return\n const key = nameSearchKey(companyQuery)\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n ...(key ? [startAt(key), endAt(`${key}\\uf8ff`)] : []),\n limit(COMPANY_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n setCompanyChoices(\n snapshot.docs.map((entry) => ({\n id: entry.id,\n name: String(entry.get('name') ?? ''),\n })),\n )\n })\n .catch(() => {\n if (active) setCompanyChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, orgId, visibleTo, companyQuery])\n const companyValue: CompanyChoice | null = values.companyId\n ? { id: values.companyId, name: values.companyName }\n : null\n\n const customMissing = crmCustomDraftMissingRequired(fields.active, storedCustom, custom, mode)\n const problem =\n dealFormProblem(values, mode) ??\n (customMissing.length ? `${customMissing.join(', ')} is required.` : null)\n const amountDerived = Boolean(deal && dealHasLineItems(deal))\n\n /*\n * THE RECORDS BAND (AGL-2611), read only while a CREATE is open: a deal\n * is a record of the band the contacts list is banded by, and on an org\n * at a full hard band this drawer refuses the way `upsertHostContact`\n * refuses a capture — same number, same sentence. Memoized, because the\n * tuple is an effect dependency and a fresh one per render would re-read\n * the three aggregates on every keystroke.\n */\n const recordsScope = useMemo(\n () => (open && mode === 'create' && orgId ? (['orgs', orgId] as const) : null),\n [open, mode, orgId],\n )\n const records = useCrmRecordsQuota(recordsScope, org)\n const bandFull = mode === 'create' && records.ready && !records.quota.allowed\n\n const handleSave = useCallback(async () => {\n if (problem || !orgId || !user?.uid) return\n if (bandFull) {\n enqueueSnackbar(CRM_RECORDS_BAND_FULL_MESSAGE, { variant: 'warning', persist: false })\n return\n }\n setBusy(true)\n try {\n const nowMs = Date.now()\n if (deal) {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n const { set, clear } = dealPatchFromForm(values, nowMs, { amountDerived })\n await updateDoc(\n doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id),\n {\n ...set,\n ...Object.fromEntries(clear.map((key) => [key, deleteField()])),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n // The title is what the Deals table searches (AGL-3321),\n // from the listener's row with the edit over it.\n ...crmClientListFields(\n 'deals',\n deal as unknown as Record<string, unknown>,\n set,\n CRM_CLIENT_SEARCH_FIELDS,\n ),\n },\n )\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return\n }\n enqueueSnackbar('Deal saved', { variant: 'success', persist: false })\n onSaved?.(deal.$id, 'edit')\n } else {\n // Held by the button until the capturing site is known; checked\n // again here because a callback can outlive the render that held it.\n if (!createHostId) return\n const customDocument = crmCustomDraftDocument(custom)\n const record: Record<string, unknown> = {\n ...dealDocumentFromForm(values, {\n visibleTo: [...createTokens],\n hostId: createHostId,\n uid: user.uid,\n nowMs,\n }),\n ...(customDocument ? { custom: customDocument } : {}),\n }\n const created = await addDoc(\n collection(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals),\n // What the Deals table searches and filters by (AGL-3321), and\n // nothing scheduled against a deal that did not exist.\n { ...record, ...crmNewRecordListFields('deals', record) },\n )\n // Setup → Activity shows CRM work (AGL-2622): the deal is org data,\n // but the act happened in this site's console and belongs in its\n // feed. Creation only — an edit is a save the card reports.\n logActivity('Added deal', { type: 'deal', id: created.id, name: values.title })\n enqueueSnackbar('Deal created', { variant: 'success', persist: false })\n onSaved?.(created.id, 'create')\n }\n onClose()\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }, [\n problem,\n bandFull,\n orgId,\n user,\n deal,\n values,\n unreadable,\n fromCache,\n firestore,\n createTokens,\n createHostId,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={busy ? undefined : onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton color=\"inherit\" edge=\"start\" onClick={onClose} sx={{ mr: 2 }}>\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>close drawer</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {deal ? 'Edit deal' : 'New deal'}\n </Typography>\n </>\n }\n appBarRight={\n <Button variant=\"outlined\" color=\"inherit\" onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n }\n >\n <Container gutterY>\n <Stack spacing={2} sx={{ width: { xs: '100%', sm: 420 } }}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new deal names the site that captures\n it. Nothing under a site, and nothing on an edit — a deal keeps\n the site it was made on (AGL-2630).\n */}\n {mode === 'create' ? (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this deal belongs to — it decides which of your sites may see it.\"\n />\n ) : null}\n <TextField\n label=\"Title\"\n value={values.title}\n onChange={(event) => update({ title: event.target.value })}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_TITLE_MAX } }}\n />\n {mode === 'create' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n select\n label=\"Pipeline\"\n value={pipelines.some((entry) => entry.$id === values.pipelineId) ? values.pipelineId : ''}\n onChange={(event) => {\n const next = pipelines.find(\n (entry) => entry.$id === event.target.value,\n )\n update({\n pipelineId: event.target.value,\n stageId: openStages(next)[0]?.id ?? '',\n })\n }}\n sx={{ flex: 1 }}\n >\n {pipelines.map((entry) => (\n <MenuItem key={entry.$id} value={entry.$id}>\n {entry.name}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n select\n label=\"Stage\"\n value={stages.some((stage) => stage.id === values.stageId) ? values.stageId : ''}\n onChange={(event) => update({ stageId: event.target.value })}\n sx={{ flex: 1 }}\n >\n {stages.map((stage) => (\n <MenuItem key={stage.id} value={stage.id}>\n {stage.name}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n ) : null}\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'flex-start' }}>\n <TextField\n label=\"Amount\"\n value={values.amount}\n onChange={(event) => update({ amount: event.target.value })}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n helperText={\n amountDerived\n ? \"The sum of the deal's line items — edit them on the Products card.\"\n : undefined\n }\n slotProps={{\n htmlInput: { inputMode: 'decimal', readOnly: amountDerived },\n }}\n />\n <TextField\n select\n label=\"Currency\"\n value={values.currency}\n onChange={(event) => update({ currency: event.target.value })}\n disabled={amountDerived}\n sx={{ width: 120 }}\n >\n {DEAL_CURRENCIES.map((code) => (\n <MenuItem key={code} value={code}>\n {code.toUpperCase()}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n <TextField\n type=\"date\"\n label=\"Expected close\"\n value={values.expectedClose}\n onChange={(event) => update({ expectedClose: event.target.value })}\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n select\n label=\"Owner\"\n // The stored owner resolved to a member — by uid, or by an\n // address the roster has — so the picker highlights the person\n // and a save writes their uid.\n value={owner ? owner.uid : values.ownerUid ? '__keep' : ''}\n onChange={(event) =>\n update({\n ownerUid: event.target.value === '__keep' ? values.ownerUid : event.target.value,\n })\n }\n fullWidth\n helperText={\n !roster.loading && !roster.members.length\n ? 'The roster could not be read; the owner stays as stored.'\n : undefined\n }\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {values.ownerUid && !owner ? (\n // The stored owner is not on the roster the route returned —\n // a member who left, or a roster that failed to load. Shown by\n // uid so the field is never blank while the deal has an owner.\n <MenuItem value=\"__keep\">{values.ownerUid}</MenuItem>\n ) : null}\n {roster.members.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n <Autocomplete<ContactChoice, false, false, false>\n options={contactChoices}\n value={contactValue}\n inputValue={contactQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setContactQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n contactId: picked?.id ?? '',\n contactName: picked ? picked.name || picked.email : '',\n })\n }\n getOptionLabel={(choice) => choice.name || choice.email}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n // Matching is `contactChoicesFor`'s — the list's grammar over the\n // window — and never MUI's own substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.id}>\n <Stack sx={{ lineHeight: 1.25 }}>\n <Typography variant=\"body2\">{choice.name || choice.email}</Typography>\n {choice.name ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {choice.email}\n </Typography>\n ) : null}\n </Stack>\n </li>\n )}\n noOptionsText=\"No recent contact matches\"\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Contact\"\n placeholder=\"Name or email\"\n helperText=\"Searches your most recent contacts by name or email.\"\n />\n )}\n />\n <Autocomplete<CompanyChoice, false, false, false>\n options={companyChoices}\n value={companyValue}\n inputValue={companyQuery}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setCompanyQuery(next)\n }}\n onChange={(_event, picked) =>\n update({\n companyId: picked?.id ?? '',\n companyName: picked?.name ?? '',\n })\n }\n getOptionLabel={(choice) => choice.name}\n isOptionEqualToValue={(option, value) => option.id === value.id}\n filterOptions={(options) => options}\n noOptionsText=\"No company matches\"\n renderInput={(params) => (\n <TextField {...params} label=\"Company\" placeholder=\"Company name\" />\n )}\n />\n <TextField\n label=\"Notes\"\n value={values.notes}\n onChange={(event) => update({ notes: event.target.value })}\n multiline\n minRows={3}\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_NOTES_MAX } }}\n />\n {fields.active.length ? (\n <>\n <Typography variant=\"subtitle2\">{'Custom fields'}</Typography>\n {fields.active.map((definition) => (\n <CrmCustomFieldControl\n key={definition.$id}\n definition={definition}\n value={crmCustomDraftValue(storedCustom, custom, definition.key)}\n onChange={(value) =>\n setCustom((current) => ({ ...current, [definition.key]: value }))\n }\n disabled={busy}\n />\n ))}\n </>\n ) : null}\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n {problem && values.title ? <Alert severity=\"warning\">{problem}</Alert> : null}\n <Button\n variant=\"contained\"\n disabled={\n busy ||\n Boolean(problem) ||\n !orgId ||\n bandFull ||\n // A new deal waits for its capturing site.\n (mode === 'create' && !createHostId)\n }\n onClick={() => void handleSave()}\n >\n {deal ? 'Save deal' : 'Create deal'}\n </Button>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nDealEditDrawer.displayName = 'DealEditDrawer'\n\nexport default DealEditDrawer\n"],"names":["CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","dealHasLineItems","findOrgMember","nameSearchKey","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useFirestoreCollection","useUser","writeGuardedBySeed","Alert","Autocomplete","Button","IconButton","MenuItem","Stack","TextField","Typography","addDoc","collection","deleteField","doc","endAt","getDocs","limit","orderBy","query","startAt","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","crmScopeListable","crmVisibleToClause","useCrmScope","useOrgMemberDirectory","contactPrimaryGroup","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","CRM_CLIENT_SEARCH_FIELDS","crmClientListFields","DEAL_CURRENCIES","openStages","contactChoicesFor","DEAL_NOTES_MAX","DEAL_TITLE_MAX","dealDocumentFromForm","dealFormFromDoc","dealFormProblem","dealPatchFromForm","emptyDealForm","CONTACT_WINDOW","COMPANY_MATCHES","DealEditDrawer","props","contactChoices","open","onClose","hostId","org","deal","defaults","pipelines","defaultPipeline","unreadable","fromCache","onSaved","mode","firestore","enqueueSnackbar","data","user","orgId","consentGroup","visibleTo","createHostId","createTokens","logActivity","roster","values","setValues","owner","members","ownerUid","busy","setBusy","fields","storedCustom","custom","setCustom","update","patch","current","pipeline","find","entry","$id","pipelineId","stages","contactRows","idField","contactQuery","setContactQuery","groupId","row","contactValue","contactId","id","name","contactName","email","choice","companyQuery","setCompanyQuery","companyChoices","setCompanyChoices","key","active","timer","setTimeout","companies","then","snapshot","docs","map","String","get","catch","clearTimeout","companyValue","companyId","companyName","customMissing","problem","length","join","amountDerived","Boolean","recordsScope","records","bandFull","ready","quota","allowed","handleSave","uid","variant","persist","nowMs","Date","now","verdict","subject","set","clear","deals","Object","fromEntries","ok","message","customDocument","record","created","type","title","error","console","allowDuplicate","anchor","undefined","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","appBarRight","disabled","gutterY","spacing","width","xs","sm","helperText","label","value","onChange","event","target","autoFocus","fullWidth","slotProps","htmlInput","maxLength","direction","select","some","next","stageId","flex","stage","alignItems","amount","placeholder","inputMode","readOnly","currency","code","toUpperCase","expectedClose","inputLabel","shrink","loading","member","options","inputValue","onInputChange","_event","reason","picked","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","lineHeight","noOptionsText","renderInput","params","notes","multiline","minRows","definition","severity","displayName"],"mappings":"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"}
@@ -159,21 +159,23 @@ import { amountInputValue, DEFAULT_DEAL_CURRENCY, formatMoney, parseAmountInput
159
159
  help: pluginDocsHelp('deals', {
160
160
  anchor: '#line-items'
161
161
  }),
162
- actions: /*#__PURE__*/ _jsx(Tooltip, {
163
- title: full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : '',
164
- children: /*#__PURE__*/ _jsx("span", {
165
- children: /*#__PURE__*/ _jsx(Button, {
166
- size: "small",
167
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
168
- path: mdiPlus.path,
169
- size: 0.8
170
- }),
171
- disabled: busy || full,
172
- onClick: ()=>setAdding(true),
173
- children: 'Add line'
162
+ HeaderProps: {
163
+ action: /*#__PURE__*/ _jsx(Tooltip, {
164
+ title: full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : '',
165
+ children: /*#__PURE__*/ _jsx("span", {
166
+ children: /*#__PURE__*/ _jsx(Button, {
167
+ size: "small",
168
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
169
+ path: mdiPlus.path,
170
+ size: 0.8
171
+ }),
172
+ disabled: busy || full,
173
+ onClick: ()=>setAdding(true),
174
+ children: 'Add line'
175
+ })
174
176
  })
175
177
  })
176
- }),
178
+ },
177
179
  contentGutterX: true,
178
180
  contentGutterY: true,
179
181
  children: items.length === 0 ? /*#__PURE__*/ _jsx(Typography, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-products-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n CRM_COLLECTIONS,\n type CrmDealLineItem,\n DEAL_LINE_ITEM_NAME_MAX,\n DEAL_LINE_ITEM_QUANTITY_MAX,\n DEAL_LINE_ITEMS_MAX,\n lineItemsTotalCents,\n nameSearchToken,\n pluginDocsHelp,\n readDealLineItems,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n getDocs,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n amountInputValue,\n type DealDoc,\n DEFAULT_DEAL_CURRENCY,\n formatMoney,\n parseAmountInput,\n} from '../model/deal-board-model'\n\n/** Catalog matches offered per keystroke; a longer list is a scroll, not a pick. */\nconst CATALOG_MATCHES = 8\n\n/** One thing the catalog search offers: a product, or one variant of it. */\ninterface CatalogChoice {\n key: string\n productId: string\n label: string\n unitAmountCents: number\n}\n\nexport interface DealProductsCardProps {\n deal: DealDoc\n orgId: string\n /**\n * The site whose catalog the search reads — the console's own, or\n * `null` at the organization level (AGL-2630), where the deal's own\n * capturing site is searched; a deal with none takes lines by hand only.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The listener's verdict on the deal, for the stale-seed guard. */\n fromCache: boolean\n unreadable: boolean\n}\n\n/**\n * The products behind a deal's amount (AGL-2620).\n *\n * ## The amount follows the lines\n *\n * Every write here stores `lineItems` AND `amountCents` as their sum, in\n * one update, so the board, the reports and the API keep reading the one\n * field they always read and no reader adds up lines. Once a deal has a\n * line, the drawer's Amount goes read-only and says why; remove the last\n * line and the amount is typed again, keeping the last sum rather than\n * dropping to nothing. The currency is the deal's — a line cannot be in\n * another, because their sum is one number.\n *\n * ## Two doors to a line\n *\n * **From the catalog** searches THIS site's products — a prefix on\n * `nameTokens` over active products, the query the products hub runs and\n * the `(status, nameTokens, nameLower)` index serves — and offers each\n * variant priced. The catalog is USD; a deal in another currency is told\n * so and can edit the unit amount after. **By hand** takes a name and a\n * price with no product behind them, which is also all a plan without\n * commerce gets: the catalog door is not drawn for it, because the site\n * has no catalog to search.\n *\n * Adding opens a dialog rather than a row of fields above the table, and\n * a quantity is edited in place and written on blur.\n */\nexport function DealProductsCard(props: DealProductsCardProps) {\n const { deal, orgId, hostId, org, fromCache, unreadable } = props\n const firestore = useFirestore()\n // The catalog searched: the mounted site's, or the deal's own.\n const catalogHostId = hostId ?? deal.hostId ?? null\n const { enqueueSnackbar } = useSnackbar()\n const currency = String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n const items = useMemo(() => deal.lineItems ?? [], [deal.lineItems])\n const hasCommerce = checkEntitlement(\n (org ?? null) as Partial<AglynOrgBilling> | null,\n 'commerce',\n )\n const [busy, setBusy] = useState(false)\n const [adding, setAdding] = useState(false)\n\n /** The one write: the lines and their sum, together. */\n const save = useCallback(\n async (next: CrmDealLineItem[], done: string) => {\n const read = readDealLineItems(next, currency)\n if ('error' in read) {\n enqueueSnackbar(read.error, { variant: 'warning', persist: false })\n return false\n }\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id), {\n lineItems: read.items,\n // The last sum stays on a deal whose lines were all removed:\n // the forecast should not drop to zero because a quote was\n // cleared to be retyped.\n ...(read.items.length ? { amountCents: lineItemsTotalCents(read.items) } : {}),\n updatedAt: new Date(),\n })\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [currency, enqueueSnackbar, unreadable, fromCache, firestore, orgId, deal.$id],\n )\n\n const handleRemove = useCallback(\n (index: number) =>\n void save(\n items.filter((_item, at) => at !== index),\n 'Line removed',\n ),\n [items, save],\n )\n const handleQuantity = useCallback(\n (index: number, quantity: number) => {\n if (quantity === items[index]?.quantity) return\n void save(\n items.map((item, at) => (at === index ? { ...item, quantity } : item)),\n 'Quantity saved',\n )\n },\n [items, save],\n )\n const handleAdd = useCallback(\n async (line: CrmDealLineItem) => {\n const ok = await save([...items, line], 'Line added')\n if (ok) setAdding(false)\n },\n [items, save],\n )\n\n const total = lineItemsTotalCents(items)\n const full = items.length >= DEAL_LINE_ITEMS_MAX\n\n return (\n <>\n <CardDisplay\n header={'Products'}\n help={pluginDocsHelp('deals', { anchor: '#line-items' })}\n actions={\n <Tooltip title={full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : ''}>\n <span>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={busy || full}\n onClick={() => setAdding(true)}\n >\n {'Add line'}\n </Button>\n </span>\n </Tooltip>\n }\n contentGutterX\n contentGutterY\n >\n {items.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {hasCommerce\n ? 'No line items yet. Add products from your catalog or by hand; the amount becomes their sum.'\n : 'No line items yet. Add what the deal is for, priced by hand; the amount becomes their sum.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\" aria-label=\"Line items\">\n <TableHead>\n <TableRow>\n <TableCell>{'Item'}</TableCell>\n <TableCell align=\"right\" sx={{ width: 110 }}>\n {'Quantity'}\n </TableCell>\n <TableCell align=\"right\">{'Unit'}</TableCell>\n <TableCell align=\"right\">{'Total'}</TableCell>\n <TableCell sx={{ width: 48 }} />\n </TableRow>\n </TableHead>\n <TableBody>\n {items.map((item, index) => (\n <TableRow key={`${index}:${item.productId ?? item.name}`}>\n <TableCell>\n <Typography variant=\"body2\">{item.name}</Typography>\n {item.productId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'From the catalog'}\n </Typography>\n ) : null}\n </TableCell>\n <TableCell align=\"right\">\n <QuantityField\n value={item.quantity}\n disabled={busy}\n label={`Quantity of ${item.name}`}\n onCommit={(quantity) => handleQuantity(index, quantity)}\n />\n </TableCell>\n <TableCell align=\"right\">{formatMoney(item.unitAmountCents, currency)}</TableCell>\n <TableCell align=\"right\">\n {formatMoney(item.quantity * item.unitAmountCents, currency)}\n </TableCell>\n <TableCell padding=\"none\" align=\"right\">\n <Tooltip title=\"Remove line\">\n <span>\n <IconButton\n size=\"small\"\n color=\"error\"\n disabled={busy}\n aria-label={`Remove ${item.name}`}\n onClick={() => handleRemove(index)}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </span>\n </Tooltip>\n </TableCell>\n </TableRow>\n ))}\n <TableRow>\n <TableCell colSpan={3}>\n <Typography variant=\"subtitle2\">{'Amount'}</Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"subtitle2\" data-testid=\"line-items-total\">\n {formatMoney(total, currency)}\n </Typography>\n </TableCell>\n <TableCell padding=\"none\" />\n </TableRow>\n </TableBody>\n </ScrollTable>\n )}\n </CardDisplay>\n <AddLineDialog\n open={adding}\n onClose={() => setAdding(false)}\n hostId={catalogHostId}\n currency={currency}\n catalog={hasCommerce && Boolean(catalogHostId)}\n busy={busy}\n onAdd={handleAdd}\n />\n </>\n )\n}\nDealProductsCard.displayName = 'DealProductsCard'\n\ninterface QuantityFieldProps {\n value: number\n disabled: boolean\n label: string\n onCommit: (quantity: number) => void\n}\n\n/** A quantity edited in place, committed on blur or Enter, refused below one. */\nfunction QuantityField(props: QuantityFieldProps) {\n const { value, disabled, label, onCommit } = props\n const [draft, setDraft] = useState(String(value))\n useEffect(() => setDraft(String(value)), [value])\n const commit = () => {\n const quantity = Number(draft)\n if (Number.isInteger(quantity) && quantity >= 1 && quantity <= DEAL_LINE_ITEM_QUANTITY_MAX) {\n onCommit(quantity)\n } else {\n setDraft(String(value))\n }\n }\n return (\n <TextField\n size=\"small\"\n type=\"number\"\n value={draft}\n disabled={disabled}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n }\n }}\n sx={{ width: 88 }}\n slotProps={{\n htmlInput: {\n min: 1,\n max: DEAL_LINE_ITEM_QUANTITY_MAX,\n step: 1,\n 'aria-label': label,\n style: { textAlign: 'right' },\n },\n }}\n />\n )\n}\nQuantityField.displayName = 'QuantityField'\n\ninterface AddLineDialogProps {\n open: boolean\n onClose: () => void\n /** The site whose catalog is searched; `null` draws no catalog door. */\n hostId: string | null\n currency: string\n /** Whether the catalog door is drawn — the plan has commerce. */\n catalog: boolean\n busy: boolean\n onAdd: (line: CrmDealLineItem) => Promise<void>\n}\n\ntype Door = 'catalog' | 'manual'\n\nfunction AddLineDialog(props: AddLineDialogProps) {\n const { open, onClose, hostId, currency, catalog, busy, onAdd } = props\n const firestore = useFirestore()\n const [door, setDoor] = useState<Door>(catalog ? 'catalog' : 'manual')\n const [picked, setPicked] = useState<CatalogChoice | null>(null)\n const [name, setName] = useState('')\n const [quantity, setQuantity] = useState('1')\n const [unit, setUnit] = useState('')\n const [search, setSearch] = useState('')\n const [choices, setChoices] = useState<CatalogChoice[]>([])\n\n useEffect(() => {\n if (!open) return\n setDoor(catalog ? 'catalog' : 'manual')\n setPicked(null)\n setName('')\n setQuantity('1')\n setUnit('')\n setSearch('')\n setChoices([])\n }, [open, catalog])\n\n /*\n * THE CATALOG SEARCH: the first typed word as a prefix token over active\n * products, ordered by name, debounced so a name typed at speed is one\n * query rather than one per letter. Each variant is its own choice,\n * priced from `priceUsd` in cents.\n */\n useEffect(() => {\n if (!open || door !== 'catalog' || !hostId) return\n const token = nameSearchToken(search)\n if (!token) {\n setChoices([])\n return\n }\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'hosts', hostId, 'products'),\n where('status', '==', 'active'),\n where('nameTokens', 'array-contains', token),\n orderBy('nameLower'),\n limit(CATALOG_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n const next: CatalogChoice[] = []\n for (const entry of snapshot.docs) {\n const productName = String(entry.get('name') ?? '')\n const variants = (entry.get('variants') as Array<Record<string, unknown>> | undefined) ?? []\n const priced = variants.length\n ? variants\n : [{ id: '', priceUsd: entry.get('priceUsd') }]\n for (const variant of priced) {\n const options = Object.values((variant['options'] as Record<string, string>) ?? {})\n const label = options.length ? `${productName} — ${options.join(' / ')}` : productName\n next.push({\n key: `${entry.id}:${String(variant['id'] ?? '')}`,\n productId: entry.id,\n label,\n unitAmountCents: Math.max(0, Math.round((Number(variant['priceUsd']) || 0) * 100)),\n })\n }\n }\n setChoices(next)\n })\n .catch(() => {\n if (active) setChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, door, hostId, search])\n\n const unitCents = parseAmountInput(unit)\n const quantityNumber = Number(quantity)\n const lineName = (door === 'catalog' ? (name || picked?.label || '') : name).trim()\n const problem = !lineName\n ? door === 'catalog'\n ? 'Pick a product.'\n : 'A line needs a name.'\n : !Number.isInteger(quantityNumber) || quantityNumber < 1\n ? 'The quantity has to be a whole number of one or more.'\n : unitCents === null\n ? 'The unit amount has to be a number of zero or more.'\n : null\n\n const handleAdd = () => {\n if (problem || unitCents === null) return\n void onAdd({\n ...(door === 'catalog' && picked ? { productId: picked.productId } : {}),\n name: lineName.slice(0, DEAL_LINE_ITEM_NAME_MAX),\n quantity: quantityNumber,\n unitAmountCents: unitCents,\n currency,\n })\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle>{'Add a line'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {catalog ? (\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={door}\n onChange={(_event, next) => {\n if (next) setDoor(next as Door)\n }}\n aria-label=\"Where the line comes from\"\n >\n <ToggleButton value=\"catalog\">{'From the catalog'}</ToggleButton>\n <ToggleButton value=\"manual\">{'By hand'}</ToggleButton>\n </ToggleButtonGroup>\n ) : null}\n {door === 'catalog' ? (\n <Autocomplete<CatalogChoice, false, false, false>\n options={choices}\n value={picked}\n inputValue={search}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setSearch(next)\n }}\n onChange={(_event, choice) => {\n setPicked(choice)\n setName(choice?.label ?? '')\n setUnit(choice ? amountInputValue(choice.unitAmountCents) : '')\n }}\n getOptionLabel={(choice) => choice.label}\n isOptionEqualToValue={(option, value) => option.key === value.key}\n // The match is the query's own prefix over `nameTokens`, never\n // MUI's substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.key}>\n <Stack direction=\"row\" spacing={1} sx={{ width: '100%', alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ flex: 1 }}>\n {choice.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {formatMoney(choice.unitAmountCents, 'usd')}\n </Typography>\n </Stack>\n </li>\n )}\n noOptionsText={search.trim() ? 'No active product matches' : 'Type a product name'}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Product\"\n placeholder=\"Search this site's catalog\"\n autoFocus\n helperText={\n currency !== 'usd'\n ? `Catalog prices are in USD; this deal is in ${currency.toUpperCase()}. Check the unit amount.`\n : undefined\n }\n />\n )}\n />\n ) : (\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_LINE_ITEM_NAME_MAX } }}\n />\n )}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Quantity\"\n type=\"number\"\n value={quantity}\n onChange={(event) => setQuantity(event.target.value)}\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { min: 1, max: DEAL_LINE_ITEM_QUANTITY_MAX, step: 1 } }}\n />\n <TextField\n label={`Unit amount (${currency.toUpperCase()})`}\n value={unit}\n onChange={(event) => setUnit(event.target.value)}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n </Stack>\n {problem && (lineName || unit) ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {problem}\n </Typography>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || Boolean(problem)} onClick={handleAdd}>\n {'Add line'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAddLineDialog.displayName = 'AddLineDialog'\n\nexport default DealProductsCard\n"],"names":["checkEntitlement","CRM_COLLECTIONS","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","DEAL_LINE_ITEMS_MAX","lineItemsTotalCents","nameSearchToken","pluginDocsHelp","readDealLineItems","mdiDeleteOutline","mdiPlus","CardDisplay","MdiIcon","ScrollTable","useSnackbar","useFirestore","writeGuardedBySeed","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TableBody","TableCell","TableHead","TableRow","TextField","ToggleButton","ToggleButtonGroup","Tooltip","Typography","collection","doc","getDocs","limit","orderBy","query","updateDoc","where","useCallback","useEffect","useMemo","useState","amountInputValue","DEFAULT_DEAL_CURRENCY","formatMoney","parseAmountInput","CATALOG_MATCHES","DealProductsCard","props","hostId","deal","orgId","org","fromCache","unreadable","firestore","catalogHostId","enqueueSnackbar","currency","String","toLowerCase","items","lineItems","hasCommerce","busy","setBusy","adding","setAdding","save","next","done","read","error","variant","persist","verdict","subject","deals","$id","length","amountCents","updatedAt","Date","ok","message","console","allowDuplicate","handleRemove","index","filter","_item","at","handleQuantity","quantity","map","item","handleAdd","line","total","full","header","help","anchor","actions","title","span","size","startIcon","path","disabled","onClick","contentGutterX","contentGutterY","color","aria-label","align","sx","width","name","productId","QuantityField","value","label","onCommit","unitAmountCents","padding","colSpan","data-testid","AddLineDialog","open","onClose","catalog","Boolean","onAdd","displayName","draft","setDraft","commit","Number","isInteger","type","onChange","event","target","onBlur","onKeyDown","key","preventDefault","blur","slotProps","htmlInput","min","max","step","style","textAlign","door","setDoor","picked","setPicked","setName","setQuantity","unit","setUnit","search","setSearch","choices","setChoices","token","active","timer","setTimeout","then","snapshot","entry","docs","productName","get","variants","priced","id","priceUsd","options","Object","values","join","push","Math","round","catch","clearTimeout","unitCents","quantityNumber","lineName","trim","problem","slice","undefined","fullWidth","maxWidth","spacing","pt","exclusive","_event","inputValue","onInputChange","reason","choice","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","direction","alignItems","flex","noOptionsText","renderInput","params","placeholder","autoFocus","helperText","toUpperCase","maxLength","inputMode"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-products-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type AglynOrgBilling,\n checkEntitlement,\n CRM_COLLECTIONS,\n type CrmDealLineItem,\n DEAL_LINE_ITEM_NAME_MAX,\n DEAL_LINE_ITEM_QUANTITY_MAX,\n DEAL_LINE_ITEMS_MAX,\n lineItemsTotalCents,\n nameSearchToken,\n pluginDocsHelp,\n readDealLineItems,\n} from '@aglyn/aglyn'\nimport { mdiDeleteOutline, mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { useFirestore, writeGuardedBySeed } from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n IconButton,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n ToggleButton,\n ToggleButtonGroup,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n getDocs,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { CrmOrgDoc } from '../hooks/use-crm-scope'\nimport {\n amountInputValue,\n type DealDoc,\n DEFAULT_DEAL_CURRENCY,\n formatMoney,\n parseAmountInput,\n} from '../model/deal-board-model'\n\n/** Catalog matches offered per keystroke; a longer list is a scroll, not a pick. */\nconst CATALOG_MATCHES = 8\n\n/** One thing the catalog search offers: a product, or one variant of it. */\ninterface CatalogChoice {\n key: string\n productId: string\n label: string\n unitAmountCents: number\n}\n\nexport interface DealProductsCardProps {\n deal: DealDoc\n orgId: string\n /**\n * The site whose catalog the search reads — the console's own, or\n * `null` at the organization level (AGL-2630), where the deal's own\n * capturing site is searched; a deal with none takes lines by hand only.\n */\n hostId: string | null\n org: CrmOrgDoc\n /** The listener's verdict on the deal, for the stale-seed guard. */\n fromCache: boolean\n unreadable: boolean\n}\n\n/**\n * The products behind a deal's amount (AGL-2620).\n *\n * ## The amount follows the lines\n *\n * Every write here stores `lineItems` AND `amountCents` as their sum, in\n * one update, so the board, the reports and the API keep reading the one\n * field they always read and no reader adds up lines. Once a deal has a\n * line, the drawer's Amount goes read-only and says why; remove the last\n * line and the amount is typed again, keeping the last sum rather than\n * dropping to nothing. The currency is the deal's — a line cannot be in\n * another, because their sum is one number.\n *\n * ## Two doors to a line\n *\n * **From the catalog** searches THIS site's products — a prefix on\n * `nameTokens` over active products, the query the products hub runs and\n * the `(status, nameTokens, nameLower)` index serves — and offers each\n * variant priced. The catalog is USD; a deal in another currency is told\n * so and can edit the unit amount after. **By hand** takes a name and a\n * price with no product behind them, which is also all a plan without\n * commerce gets: the catalog door is not drawn for it, because the site\n * has no catalog to search.\n *\n * Adding opens a dialog rather than a row of fields above the table, and\n * a quantity is edited in place and written on blur.\n */\nexport function DealProductsCard(props: DealProductsCardProps) {\n const { deal, orgId, hostId, org, fromCache, unreadable } = props\n const firestore = useFirestore()\n // The catalog searched: the mounted site's, or the deal's own.\n const catalogHostId = hostId ?? deal.hostId ?? null\n const { enqueueSnackbar } = useSnackbar()\n const currency = String(deal.currency || DEFAULT_DEAL_CURRENCY).toLowerCase()\n const items = useMemo(() => deal.lineItems ?? [], [deal.lineItems])\n const hasCommerce = checkEntitlement(\n (org ?? null) as Partial<AglynOrgBilling> | null,\n 'commerce',\n )\n const [busy, setBusy] = useState(false)\n const [adding, setAdding] = useState(false)\n\n /** The one write: the lines and their sum, together. */\n const save = useCallback(\n async (next: CrmDealLineItem[], done: string) => {\n const read = readDealLineItems(next, currency)\n if ('error' in read) {\n enqueueSnackbar(read.error, { variant: 'warning', persist: false })\n return false\n }\n setBusy(true)\n try {\n const verdict = await writeGuardedBySeed(\n { subject: 'deal', unreadable, fromCache },\n async () => {\n await updateDoc(doc(firestore, 'orgs', orgId, CRM_COLLECTIONS.deals, deal.$id), {\n lineItems: read.items,\n // The last sum stays on a deal whose lines were all removed:\n // the forecast should not drop to zero because a quote was\n // cleared to be retyped.\n ...(read.items.length ? { amountCents: lineItemsTotalCents(read.items) } : {}),\n updatedAt: new Date(),\n })\n },\n )\n if (!verdict.ok) {\n enqueueSnackbar(verdict.message, { variant: 'warning', persist: false })\n return false\n }\n enqueueSnackbar(done, { variant: 'success', persist: false })\n return true\n } catch (error) {\n console.error(error)\n enqueueSnackbar('An error has occurred', { variant: 'error', allowDuplicate: true })\n return false\n } finally {\n setBusy(false)\n }\n },\n [currency, enqueueSnackbar, unreadable, fromCache, firestore, orgId, deal.$id],\n )\n\n const handleRemove = useCallback(\n (index: number) =>\n void save(\n items.filter((_item, at) => at !== index),\n 'Line removed',\n ),\n [items, save],\n )\n const handleQuantity = useCallback(\n (index: number, quantity: number) => {\n if (quantity === items[index]?.quantity) return\n void save(\n items.map((item, at) => (at === index ? { ...item, quantity } : item)),\n 'Quantity saved',\n )\n },\n [items, save],\n )\n const handleAdd = useCallback(\n async (line: CrmDealLineItem) => {\n const ok = await save([...items, line], 'Line added')\n if (ok) setAdding(false)\n },\n [items, save],\n )\n\n const total = lineItemsTotalCents(items)\n const full = items.length >= DEAL_LINE_ITEMS_MAX\n\n return (\n <>\n <CardDisplay\n header={'Products'}\n help={pluginDocsHelp('deals', { anchor: '#line-items' })}\n HeaderProps={{\n action: (\n <Tooltip title={full ? `A deal carries at most ${DEAL_LINE_ITEMS_MAX} lines` : ''}>\n <span>\n <Button\n size=\"small\"\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n disabled={busy || full}\n onClick={() => setAdding(true)}\n >\n {'Add line'}\n </Button>\n </span>\n </Tooltip>\n ),\n }}\n contentGutterX\n contentGutterY\n >\n {items.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {hasCommerce\n ? 'No line items yet. Add products from your catalog or by hand; the amount becomes their sum.'\n : 'No line items yet. Add what the deal is for, priced by hand; the amount becomes their sum.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\" aria-label=\"Line items\">\n <TableHead>\n <TableRow>\n <TableCell>{'Item'}</TableCell>\n <TableCell align=\"right\" sx={{ width: 110 }}>\n {'Quantity'}\n </TableCell>\n <TableCell align=\"right\">{'Unit'}</TableCell>\n <TableCell align=\"right\">{'Total'}</TableCell>\n <TableCell sx={{ width: 48 }} />\n </TableRow>\n </TableHead>\n <TableBody>\n {items.map((item, index) => (\n <TableRow key={`${index}:${item.productId ?? item.name}`}>\n <TableCell>\n <Typography variant=\"body2\">{item.name}</Typography>\n {item.productId ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'From the catalog'}\n </Typography>\n ) : null}\n </TableCell>\n <TableCell align=\"right\">\n <QuantityField\n value={item.quantity}\n disabled={busy}\n label={`Quantity of ${item.name}`}\n onCommit={(quantity) => handleQuantity(index, quantity)}\n />\n </TableCell>\n <TableCell align=\"right\">{formatMoney(item.unitAmountCents, currency)}</TableCell>\n <TableCell align=\"right\">\n {formatMoney(item.quantity * item.unitAmountCents, currency)}\n </TableCell>\n <TableCell padding=\"none\" align=\"right\">\n <Tooltip title=\"Remove line\">\n <span>\n <IconButton\n size=\"small\"\n color=\"error\"\n disabled={busy}\n aria-label={`Remove ${item.name}`}\n onClick={() => handleRemove(index)}\n >\n <MdiIcon path={mdiDeleteOutline.path} size={0.8} />\n </IconButton>\n </span>\n </Tooltip>\n </TableCell>\n </TableRow>\n ))}\n <TableRow>\n <TableCell colSpan={3}>\n <Typography variant=\"subtitle2\">{'Amount'}</Typography>\n </TableCell>\n <TableCell align=\"right\">\n <Typography variant=\"subtitle2\" data-testid=\"line-items-total\">\n {formatMoney(total, currency)}\n </Typography>\n </TableCell>\n <TableCell padding=\"none\" />\n </TableRow>\n </TableBody>\n </ScrollTable>\n )}\n </CardDisplay>\n <AddLineDialog\n open={adding}\n onClose={() => setAdding(false)}\n hostId={catalogHostId}\n currency={currency}\n catalog={hasCommerce && Boolean(catalogHostId)}\n busy={busy}\n onAdd={handleAdd}\n />\n </>\n )\n}\nDealProductsCard.displayName = 'DealProductsCard'\n\ninterface QuantityFieldProps {\n value: number\n disabled: boolean\n label: string\n onCommit: (quantity: number) => void\n}\n\n/** A quantity edited in place, committed on blur or Enter, refused below one. */\nfunction QuantityField(props: QuantityFieldProps) {\n const { value, disabled, label, onCommit } = props\n const [draft, setDraft] = useState(String(value))\n useEffect(() => setDraft(String(value)), [value])\n const commit = () => {\n const quantity = Number(draft)\n if (Number.isInteger(quantity) && quantity >= 1 && quantity <= DEAL_LINE_ITEM_QUANTITY_MAX) {\n onCommit(quantity)\n } else {\n setDraft(String(value))\n }\n }\n return (\n <TextField\n size=\"small\"\n type=\"number\"\n value={draft}\n disabled={disabled}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n ;(event.target as HTMLInputElement).blur()\n }\n }}\n sx={{ width: 88 }}\n slotProps={{\n htmlInput: {\n min: 1,\n max: DEAL_LINE_ITEM_QUANTITY_MAX,\n step: 1,\n 'aria-label': label,\n style: { textAlign: 'right' },\n },\n }}\n />\n )\n}\nQuantityField.displayName = 'QuantityField'\n\ninterface AddLineDialogProps {\n open: boolean\n onClose: () => void\n /** The site whose catalog is searched; `null` draws no catalog door. */\n hostId: string | null\n currency: string\n /** Whether the catalog door is drawn — the plan has commerce. */\n catalog: boolean\n busy: boolean\n onAdd: (line: CrmDealLineItem) => Promise<void>\n}\n\ntype Door = 'catalog' | 'manual'\n\nfunction AddLineDialog(props: AddLineDialogProps) {\n const { open, onClose, hostId, currency, catalog, busy, onAdd } = props\n const firestore = useFirestore()\n const [door, setDoor] = useState<Door>(catalog ? 'catalog' : 'manual')\n const [picked, setPicked] = useState<CatalogChoice | null>(null)\n const [name, setName] = useState('')\n const [quantity, setQuantity] = useState('1')\n const [unit, setUnit] = useState('')\n const [search, setSearch] = useState('')\n const [choices, setChoices] = useState<CatalogChoice[]>([])\n\n useEffect(() => {\n if (!open) return\n setDoor(catalog ? 'catalog' : 'manual')\n setPicked(null)\n setName('')\n setQuantity('1')\n setUnit('')\n setSearch('')\n setChoices([])\n }, [open, catalog])\n\n /*\n * THE CATALOG SEARCH: the first typed word as a prefix token over active\n * products, ordered by name, debounced so a name typed at speed is one\n * query rather than one per letter. Each variant is its own choice,\n * priced from `priceUsd` in cents.\n */\n useEffect(() => {\n if (!open || door !== 'catalog' || !hostId) return\n const token = nameSearchToken(search)\n if (!token) {\n setChoices([])\n return\n }\n let active = true\n const timer = setTimeout(() => {\n void getDocs(\n query(\n collection(firestore, 'hosts', hostId, 'products'),\n where('status', '==', 'active'),\n where('nameTokens', 'array-contains', token),\n orderBy('nameLower'),\n limit(CATALOG_MATCHES),\n ),\n )\n .then((snapshot) => {\n if (!active) return\n const next: CatalogChoice[] = []\n for (const entry of snapshot.docs) {\n const productName = String(entry.get('name') ?? '')\n const variants = (entry.get('variants') as Array<Record<string, unknown>> | undefined) ?? []\n const priced = variants.length\n ? variants\n : [{ id: '', priceUsd: entry.get('priceUsd') }]\n for (const variant of priced) {\n const options = Object.values((variant['options'] as Record<string, string>) ?? {})\n const label = options.length ? `${productName} — ${options.join(' / ')}` : productName\n next.push({\n key: `${entry.id}:${String(variant['id'] ?? '')}`,\n productId: entry.id,\n label,\n unitAmountCents: Math.max(0, Math.round((Number(variant['priceUsd']) || 0) * 100)),\n })\n }\n }\n setChoices(next)\n })\n .catch(() => {\n if (active) setChoices([])\n })\n }, 200)\n return () => {\n active = false\n clearTimeout(timer)\n }\n }, [firestore, open, door, hostId, search])\n\n const unitCents = parseAmountInput(unit)\n const quantityNumber = Number(quantity)\n const lineName = (door === 'catalog' ? (name || picked?.label || '') : name).trim()\n const problem = !lineName\n ? door === 'catalog'\n ? 'Pick a product.'\n : 'A line needs a name.'\n : !Number.isInteger(quantityNumber) || quantityNumber < 1\n ? 'The quantity has to be a whole number of one or more.'\n : unitCents === null\n ? 'The unit amount has to be a number of zero or more.'\n : null\n\n const handleAdd = () => {\n if (problem || unitCents === null) return\n void onAdd({\n ...(door === 'catalog' && picked ? { productId: picked.productId } : {}),\n name: lineName.slice(0, DEAL_LINE_ITEM_NAME_MAX),\n quantity: quantityNumber,\n unitAmountCents: unitCents,\n currency,\n })\n }\n\n return (\n <Dialog open={open} onClose={busy ? undefined : onClose} fullWidth maxWidth=\"xs\">\n <DialogTitle>{'Add a line'}</DialogTitle>\n <DialogContent>\n <Stack spacing={2} sx={{ pt: 1 }}>\n {catalog ? (\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n color=\"primary\"\n value={door}\n onChange={(_event, next) => {\n if (next) setDoor(next as Door)\n }}\n aria-label=\"Where the line comes from\"\n >\n <ToggleButton value=\"catalog\">{'From the catalog'}</ToggleButton>\n <ToggleButton value=\"manual\">{'By hand'}</ToggleButton>\n </ToggleButtonGroup>\n ) : null}\n {door === 'catalog' ? (\n <Autocomplete<CatalogChoice, false, false, false>\n options={choices}\n value={picked}\n inputValue={search}\n onInputChange={(_event, next, reason) => {\n if (reason !== 'reset') setSearch(next)\n }}\n onChange={(_event, choice) => {\n setPicked(choice)\n setName(choice?.label ?? '')\n setUnit(choice ? amountInputValue(choice.unitAmountCents) : '')\n }}\n getOptionLabel={(choice) => choice.label}\n isOptionEqualToValue={(option, value) => option.key === value.key}\n // The match is the query's own prefix over `nameTokens`, never\n // MUI's substring filter over labels.\n filterOptions={(options) => options}\n renderOption={(optionProps, choice) => (\n <li {...optionProps} key={choice.key}>\n <Stack direction=\"row\" spacing={1} sx={{ width: '100%', alignItems: 'baseline' }}>\n <Typography variant=\"body2\" sx={{ flex: 1 }}>\n {choice.label}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {formatMoney(choice.unitAmountCents, 'usd')}\n </Typography>\n </Stack>\n </li>\n )}\n noOptionsText={search.trim() ? 'No active product matches' : 'Type a product name'}\n renderInput={(params) => (\n <TextField\n {...params}\n label=\"Product\"\n placeholder=\"Search this site's catalog\"\n autoFocus\n helperText={\n currency !== 'usd'\n ? `Catalog prices are in USD; this deal is in ${currency.toUpperCase()}. Check the unit amount.`\n : undefined\n }\n />\n )}\n />\n ) : (\n <TextField\n label=\"Name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n autoFocus\n fullWidth\n slotProps={{ htmlInput: { maxLength: DEAL_LINE_ITEM_NAME_MAX } }}\n />\n )}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Quantity\"\n type=\"number\"\n value={quantity}\n onChange={(event) => setQuantity(event.target.value)}\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { min: 1, max: DEAL_LINE_ITEM_QUANTITY_MAX, step: 1 } }}\n />\n <TextField\n label={`Unit amount (${currency.toUpperCase()})`}\n value={unit}\n onChange={(event) => setUnit(event.target.value)}\n placeholder=\"0.00\"\n sx={{ flex: 1 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n </Stack>\n {problem && (lineName || unit) ? (\n <Typography variant=\"caption\" color=\"warning.main\">\n {problem}\n </Typography>\n ) : null}\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n <Button variant=\"contained\" disabled={busy || Boolean(problem)} onClick={handleAdd}>\n {'Add line'}\n </Button>\n </DialogActions>\n </Dialog>\n )\n}\nAddLineDialog.displayName = 'AddLineDialog'\n\nexport default DealProductsCard\n"],"names":["checkEntitlement","CRM_COLLECTIONS","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","DEAL_LINE_ITEMS_MAX","lineItemsTotalCents","nameSearchToken","pluginDocsHelp","readDealLineItems","mdiDeleteOutline","mdiPlus","CardDisplay","MdiIcon","ScrollTable","useSnackbar","useFirestore","writeGuardedBySeed","Autocomplete","Button","Dialog","DialogActions","DialogContent","DialogTitle","IconButton","Stack","TableBody","TableCell","TableHead","TableRow","TextField","ToggleButton","ToggleButtonGroup","Tooltip","Typography","collection","doc","getDocs","limit","orderBy","query","updateDoc","where","useCallback","useEffect","useMemo","useState","amountInputValue","DEFAULT_DEAL_CURRENCY","formatMoney","parseAmountInput","CATALOG_MATCHES","DealProductsCard","props","hostId","deal","orgId","org","fromCache","unreadable","firestore","catalogHostId","enqueueSnackbar","currency","String","toLowerCase","items","lineItems","hasCommerce","busy","setBusy","adding","setAdding","save","next","done","read","error","variant","persist","verdict","subject","deals","$id","length","amountCents","updatedAt","Date","ok","message","console","allowDuplicate","handleRemove","index","filter","_item","at","handleQuantity","quantity","map","item","handleAdd","line","total","full","header","help","anchor","HeaderProps","action","title","span","size","startIcon","path","disabled","onClick","contentGutterX","contentGutterY","color","aria-label","align","sx","width","name","productId","QuantityField","value","label","onCommit","unitAmountCents","padding","colSpan","data-testid","AddLineDialog","open","onClose","catalog","Boolean","onAdd","displayName","draft","setDraft","commit","Number","isInteger","type","onChange","event","target","onBlur","onKeyDown","key","preventDefault","blur","slotProps","htmlInput","min","max","step","style","textAlign","door","setDoor","picked","setPicked","setName","setQuantity","unit","setUnit","search","setSearch","choices","setChoices","token","active","timer","setTimeout","then","snapshot","entry","docs","productName","get","variants","priced","id","priceUsd","options","Object","values","join","push","Math","round","catch","clearTimeout","unitCents","quantityNumber","lineName","trim","problem","slice","undefined","fullWidth","maxWidth","spacing","pt","exclusive","_event","inputValue","onInputChange","reason","choice","getOptionLabel","isOptionEqualToValue","option","filterOptions","renderOption","optionProps","li","direction","alignItems","flex","noOptionsText","renderInput","params","placeholder","autoFocus","helperText","toUpperCase","maxLength","inputMode"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;;AAEA,SAEEA,gBAAgB,EAChBC,eAAe,EAEfC,uBAAuB,EACvBC,2BAA2B,EAC3BC,mBAAmB,EACnBC,mBAAmB,EACnBC,eAAe,EACfC,cAAc,EACdC,iBAAiB,QACZ,eAAc;AACrB,SAASC,gBAAgB,EAAEC,OAAO,QAAQ,yBAAwB;AAClE,SAASC,WAAW,EAAEC,OAAO,QAAQ,uBAAsB;AAC3D,SAASC,WAAW,QAAQ,yDAAwD;AACpF,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SAASC,YAAY,EAAEC,kBAAkB,QAAQ,iCAAgC;AACjF,SACEC,YAAY,EACZC,MAAM,EACNC,MAAM,EACNC,aAAa,EACbC,aAAa,EACbC,WAAW,EACXC,UAAU,EACVC,KAAK,EACLC,SAAS,EACTC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,SAAS,EACTC,YAAY,EACZC,iBAAiB,EACjBC,OAAO,EACPC,UAAU,QACL,gBAAe;AACtB,SACEC,UAAU,EACVC,GAAG,EACHC,OAAO,EACPC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,SAAS,EACTC,KAAK,QACA,qBAAoB;AAC3B,SAASC,WAAW,EAAEC,SAAS,EAAEC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAEjE,SACEC,gBAAgB,EAEhBC,qBAAqB,EACrBC,WAAW,EACXC,gBAAgB,QACX,+BAA2B;AAElC,kFAAkF,GAClF,MAAMC,kBAAkB;AAyBxB;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,SAASC,iBAAiBC,KAA4B;QAIrCC;IAHtB,MAAM,EAAEC,IAAI,EAAEC,KAAK,EAAEF,MAAM,EAAEG,GAAG,EAAEC,SAAS,EAAEC,UAAU,EAAE,GAAGN;IAC5D,MAAMO,YAAY5C;IAClB,+DAA+D;IAC/D,MAAM6C,iBAAgBP,OAAAA,iBAAAA,SAAUC,KAAKD,MAAM,YAArBA,OAAyB;IAC/C,MAAM,EAAEQ,eAAe,EAAE,GAAG/C;IAC5B,MAAMgD,WAAWC,OAAOT,KAAKQ,QAAQ,IAAIf,uBAAuBiB,WAAW;IAC3E,MAAMC,QAAQrB,QAAQ;YAAMU;gBAAAA,kBAAAA,KAAKY,SAAS,YAAdZ,kBAAkB,EAAE;OAAE;QAACA,KAAKY,SAAS;KAAC;IAClE,MAAMC,cAAcnE,iBACjBwD,cAAAA,MAAO,MACR;IAEF,MAAM,CAACY,MAAMC,QAAQ,GAAGxB,SAAS;IACjC,MAAM,CAACyB,QAAQC,UAAU,GAAG1B,SAAS;IAErC,sDAAsD,GACtD,MAAM2B,OAAO9B,YACX,OAAO+B,MAAyBC;QAC9B,MAAMC,OAAOnE,kBAAkBiE,MAAMX;QACrC,IAAI,WAAWa,MAAM;YACnBd,gBAAgBc,KAAKC,KAAK,EAAE;gBAAEC,SAAS;gBAAWC,SAAS;YAAM;YACjE,OAAO;QACT;QACAT,QAAQ;QACR,IAAI;YACF,MAAMU,UAAU,MAAM/D,mBACpB;gBAAEgE,SAAS;gBAAQtB;gBAAYD;YAAU,GACzC;gBACE,MAAMjB,UAAUL,IAAIwB,WAAW,QAAQJ,OAAOtD,gBAAgBgF,KAAK,EAAE3B,KAAK4B,GAAG,GAAG;oBAC9EhB,WAAWS,KAAKV,KAAK;mBAIjBU,KAAKV,KAAK,CAACkB,MAAM,GAAG;oBAAEC,aAAa/E,oBAAoBsE,KAAKV,KAAK;gBAAE,IAAI,CAAC;oBAC5EoB,WAAW,IAAIC;;YAEnB;YAEF,IAAI,CAACP,QAAQQ,EAAE,EAAE;gBACf1B,gBAAgBkB,QAAQS,OAAO,EAAE;oBAAEX,SAAS;oBAAWC,SAAS;gBAAM;gBACtE,OAAO;YACT;YACAjB,gBAAgBa,MAAM;gBAAEG,SAAS;gBAAWC,SAAS;YAAM;YAC3D,OAAO;QACT,EAAE,OAAOF,OAAO;YACda,QAAQb,KAAK,CAACA;YACdf,gBAAgB,yBAAyB;gBAAEgB,SAAS;gBAASa,gBAAgB;YAAK;YAClF,OAAO;QACT,SAAU;YACRrB,QAAQ;QACV;IACF,GACA;QAACP;QAAUD;QAAiBH;QAAYD;QAAWE;QAAWJ;QAAOD,KAAK4B,GAAG;KAAC;IAGhF,MAAMS,eAAejD,YACnB,CAACkD,QACC,KAAKpB,KACHP,MAAM4B,MAAM,CAAC,CAACC,OAAOC,KAAOA,OAAOH,QACnC,iBAEJ;QAAC3B;QAAOO;KAAK;IAEf,MAAMwB,iBAAiBtD,YACrB,CAACkD,OAAeK;YACGhC;QAAjB,IAAIgC,eAAahC,eAAAA,KAAK,CAAC2B,MAAM,qBAAZ3B,aAAcgC,QAAQ,GAAE;QACzC,KAAKzB,KACHP,MAAMiC,GAAG,CAAC,CAACC,MAAMJ,KAAQA,OAAOH,QAAQ,aAAKO;gBAAMF;iBAAaE,OAChE;IAEJ,GACA;QAAClC;QAAOO;KAAK;IAEf,MAAM4B,YAAY1D,YAChB,OAAO2D;QACL,MAAMd,KAAK,MAAMf,KAAK;eAAIP;YAAOoC;SAAK,EAAE;QACxC,IAAId,IAAIhB,UAAU;IACpB,GACA;QAACN;QAAOO;KAAK;IAGf,MAAM8B,QAAQjG,oBAAoB4D;IAClC,MAAMsC,OAAOtC,MAAMkB,MAAM,IAAI/E;IAE7B,qBACE;;0BACE,KAACO;gBACC6F,QAAQ;gBACRC,MAAMlG,eAAe,SAAS;oBAAEmG,QAAQ;gBAAc;gBACtDC,aAAa;oBACXC,sBACE,KAAC5E;wBAAQ6E,OAAON,OAAO,CAAC,uBAAuB,EAAEnG,oBAAoB,MAAM,CAAC,GAAG;kCAC7E,cAAA,KAAC0G;sCACC,cAAA,KAAC5F;gCACC6F,MAAK;gCACLC,yBAAW,KAACpG;oCAAQqG,MAAMvG,QAAQuG,IAAI;oCAAEF,MAAM;;gCAC9CG,UAAU9C,QAAQmC;gCAClBY,SAAS,IAAM5C,UAAU;0CAExB;;;;gBAKX;gBACA6C,cAAc;gBACdC,cAAc;0BAEbpD,MAAMkB,MAAM,KAAK,kBAChB,KAAClD;oBAAW4C,SAAQ;oBAAQyC,OAAM;8BAC/BnD,cACG,gGACA;mCAGN,MAACtD;oBAAYkG,MAAK;oBAAQQ,cAAW;;sCACnC,KAAC5F;sCACC,cAAA,MAACC;;kDACC,KAACF;kDAAW;;kDACZ,KAACA;wCAAU8F,OAAM;wCAAQC,IAAI;4CAAEC,OAAO;wCAAI;kDACvC;;kDAEH,KAAChG;wCAAU8F,OAAM;kDAAS;;kDAC1B,KAAC9F;wCAAU8F,OAAM;kDAAS;;kDAC1B,KAAC9F;wCAAU+F,IAAI;4CAAEC,OAAO;wCAAG;;;;;sCAG/B,MAACjG;;gCACEwC,MAAMiC,GAAG,CAAC,CAACC,MAAMP;wCACWO;yDAA3B,MAACvE;;0DACC,MAACF;;kEACC,KAACO;wDAAW4C,SAAQ;kEAASsB,KAAKwB,IAAI;;oDACrCxB,KAAKyB,SAAS,iBACb,KAAC3F;wDAAW4C,SAAQ;wDAAUyC,OAAM;kEACjC;yDAED;;;0DAEN,KAAC5F;gDAAU8F,OAAM;0DACf,cAAA,KAACK;oDACCC,OAAO3B,KAAKF,QAAQ;oDACpBiB,UAAU9C;oDACV2D,OAAO,CAAC,YAAY,EAAE5B,KAAKwB,IAAI,EAAE;oDACjCK,UAAU,CAAC/B,WAAaD,eAAeJ,OAAOK;;;0DAGlD,KAACvE;gDAAU8F,OAAM;0DAASxE,YAAYmD,KAAK8B,eAAe,EAAEnE;;0DAC5D,KAACpC;gDAAU8F,OAAM;0DACdxE,YAAYmD,KAAKF,QAAQ,GAAGE,KAAK8B,eAAe,EAAEnE;;0DAErD,KAACpC;gDAAUwG,SAAQ;gDAAOV,OAAM;0DAC9B,cAAA,KAACxF;oDAAQ6E,OAAM;8DACb,cAAA,KAACC;kEACC,cAAA,KAACvF;4DACCwF,MAAK;4DACLO,OAAM;4DACNJ,UAAU9C;4DACVmD,cAAY,CAAC,OAAO,EAAEpB,KAAKwB,IAAI,EAAE;4DACjCR,SAAS,IAAMxB,aAAaC;sEAE5B,cAAA,KAAChF;gEAAQqG,MAAMxG,iBAAiBwG,IAAI;gEAAEF,MAAM;;;;;;;uCA/BvC,GAAGnB,MAAM,CAAC,GAAEO,kBAAAA,KAAKyB,SAAS,YAAdzB,kBAAkBA,KAAKwB,IAAI,EAAE;;8CAsC1D,MAAC/F;;sDACC,KAACF;4CAAUyG,SAAS;sDAClB,cAAA,KAAClG;gDAAW4C,SAAQ;0DAAa;;;sDAEnC,KAACnD;4CAAU8F,OAAM;sDACf,cAAA,KAACvF;gDAAW4C,SAAQ;gDAAYuD,eAAY;0DACzCpF,YAAYsD,OAAOxC;;;sDAGxB,KAACpC;4CAAUwG,SAAQ;;;;;;;;;0BAM7B,KAACG;gBACCC,MAAMhE;gBACNiE,SAAS,IAAMhE,UAAU;gBACzBlB,QAAQO;gBACRE,UAAUA;gBACV0E,SAASrE,eAAesE,QAAQ7E;gBAChCQ,MAAMA;gBACNsE,OAAOtC;;;;AAIf;AACAjD,iBAAiBwF,WAAW,GAAG;AAS/B,+EAA+E,GAC/E,SAASd,cAAczE,KAAyB;IAC9C,MAAM,EAAE0E,KAAK,EAAEZ,QAAQ,EAAEa,KAAK,EAAEC,QAAQ,EAAE,GAAG5E;IAC7C,MAAM,CAACwF,OAAOC,SAAS,GAAGhG,SAASkB,OAAO+D;IAC1CnF,UAAU,IAAMkG,SAAS9E,OAAO+D,SAAS;QAACA;KAAM;IAChD,MAAMgB,SAAS;QACb,MAAM7C,WAAW8C,OAAOH;QACxB,IAAIG,OAAOC,SAAS,CAAC/C,aAAaA,YAAY,KAAKA,YAAY9F,6BAA6B;YAC1F6H,SAAS/B;QACX,OAAO;YACL4C,SAAS9E,OAAO+D;QAClB;IACF;IACA,qBACE,KAACjG;QACCkF,MAAK;QACLkC,MAAK;QACLnB,OAAOc;QACP1B,UAAUA;QACVgC,UAAU,CAACC,QAAUN,SAASM,MAAMC,MAAM,CAACtB,KAAK;QAChDuB,QAAQP;QACRQ,WAAW,CAACH;YACV,IAAIA,MAAMI,GAAG,KAAK,SAAS;gBACzBJ,MAAMK,cAAc;gBAClBL,MAAMC,MAAM,CAAsBK,IAAI;YAC1C;QACF;QACAhC,IAAI;YAAEC,OAAO;QAAG;QAChBgC,WAAW;YACTC,WAAW;gBACTC,KAAK;gBACLC,KAAK1J;gBACL2J,MAAM;gBACN,cAAc/B;gBACdgC,OAAO;oBAAEC,WAAW;gBAAQ;YAC9B;QACF;;AAGN;AACAnC,cAAcc,WAAW,GAAG;AAgB5B,SAASN,cAAcjF,KAAyB;IAC9C,MAAM,EAAEkF,IAAI,EAAEC,OAAO,EAAElF,MAAM,EAAES,QAAQ,EAAE0E,OAAO,EAAEpE,IAAI,EAAEsE,KAAK,EAAE,GAAGtF;IAClE,MAAMO,YAAY5C;IAClB,MAAM,CAACkJ,MAAMC,QAAQ,GAAGrH,SAAe2F,UAAU,YAAY;IAC7D,MAAM,CAAC2B,QAAQC,UAAU,GAAGvH,SAA+B;IAC3D,MAAM,CAAC8E,MAAM0C,QAAQ,GAAGxH,SAAS;IACjC,MAAM,CAACoD,UAAUqE,YAAY,GAAGzH,SAAS;IACzC,MAAM,CAAC0H,MAAMC,QAAQ,GAAG3H,SAAS;IACjC,MAAM,CAAC4H,QAAQC,UAAU,GAAG7H,SAAS;IACrC,MAAM,CAAC8H,SAASC,WAAW,GAAG/H,SAA0B,EAAE;IAE1DF,UAAU;QACR,IAAI,CAAC2F,MAAM;QACX4B,QAAQ1B,UAAU,YAAY;QAC9B4B,UAAU;QACVC,QAAQ;QACRC,YAAY;QACZE,QAAQ;QACRE,UAAU;QACVE,WAAW,EAAE;IACf,GAAG;QAACtC;QAAME;KAAQ;IAElB;;;;;GAKC,GACD7F,UAAU;QACR,IAAI,CAAC2F,QAAQ2B,SAAS,aAAa,CAAC5G,QAAQ;QAC5C,MAAMwH,QAAQvK,gBAAgBmK;QAC9B,IAAI,CAACI,OAAO;YACVD,WAAW,EAAE;YACb;QACF;QACA,IAAIE,SAAS;QACb,MAAMC,QAAQC,WAAW;YACvB,KAAK5I,QACHG,MACEL,WAAWyB,WAAW,SAASN,QAAQ,aACvCZ,MAAM,UAAU,MAAM,WACtBA,MAAM,cAAc,kBAAkBoI,QACtCvI,QAAQ,cACRD,MAAMa,mBAGP+H,IAAI,CAAC,CAACC;gBACL,IAAI,CAACJ,QAAQ;gBACb,MAAMrG,OAAwB,EAAE;gBAChC,KAAK,MAAM0G,SAASD,SAASE,IAAI,CAAE;wBACND,YACTA;oBADlB,MAAME,cAActH,QAAOoH,aAAAA,MAAMG,GAAG,CAAC,mBAAVH,aAAqB;oBAChD,MAAMI,YAAYJ,cAAAA,MAAMG,GAAG,CAAC,uBAAVH,cAAwE,EAAE;oBAC5F,MAAMK,SAASD,SAASpG,MAAM,GAC1BoG,WACA;wBAAC;4BAAEE,IAAI;4BAAIC,UAAUP,MAAMG,GAAG,CAAC;wBAAY;qBAAE;oBACjD,KAAK,MAAMzG,WAAW2G,OAAQ;4BACG3G,kBAGFA;wBAH7B,MAAM8G,UAAUC,OAAOC,MAAM,EAAEhH,mBAAAA,OAAO,CAAC,UAAU,YAAlBA,mBAAiD,CAAC;wBACjF,MAAMkD,QAAQ4D,QAAQxG,MAAM,GAAG,GAAGkG,YAAY,GAAG,EAAEM,QAAQG,IAAI,CAAC,QAAQ,GAAGT;wBAC3E5G,KAAKsH,IAAI,CAAC;4BACRxC,KAAK,GAAG4B,MAAMM,EAAE,CAAC,CAAC,EAAE1H,QAAOc,cAAAA,OAAO,CAAC,KAAK,YAAbA,cAAiB,KAAK;4BACjD+C,WAAWuD,MAAMM,EAAE;4BACnB1D;4BACAE,iBAAiB+D,KAAKnC,GAAG,CAAC,GAAGmC,KAAKC,KAAK,CAAC,AAAClD,CAAAA,OAAOlE,OAAO,CAAC,WAAW,KAAK,CAAA,IAAK;wBAC/E;oBACF;gBACF;gBACA+F,WAAWnG;YACb,GACCyH,KAAK,CAAC;gBACL,IAAIpB,QAAQF,WAAW,EAAE;YAC3B;QACJ,GAAG;QACH,OAAO;YACLE,SAAS;YACTqB,aAAapB;QACf;IACF,GAAG;QAACpH;QAAW2E;QAAM2B;QAAM5G;QAAQoH;KAAO;IAE1C,MAAM2B,YAAYnJ,iBAAiBsH;IACnC,MAAM8B,iBAAiBtD,OAAO9C;IAC9B,MAAMqG,WAAW,AAACrC,CAAAA,SAAS,YAAatC,SAAQwC,0BAAAA,OAAQpC,KAAK,KAAI,KAAMJ,IAAG,EAAG4E,IAAI;IACjF,MAAMC,UAAU,CAACF,WACbrC,SAAS,YACP,oBACA,yBACF,CAAClB,OAAOC,SAAS,CAACqD,mBAAmBA,iBAAiB,IACpD,0DACAD,cAAc,OACZ,wDACA;IAER,MAAMhG,YAAY;QAChB,IAAIoG,WAAWJ,cAAc,MAAM;QACnC,KAAK1D,MAAM,aACLuB,SAAS,aAAaE,SAAS;YAAEvC,WAAWuC,OAAOvC,SAAS;QAAC,IAAI,CAAC;YACtED,MAAM2E,SAASG,KAAK,CAAC,GAAGvM;YACxB+F,UAAUoG;YACVpE,iBAAiBmE;YACjBtI;;IAEJ;IAEA,qBACE,MAAC3C;QAAOmH,MAAMA;QAAMC,SAASnE,OAAOsI,YAAYnE;QAASoE,SAAS;QAACC,UAAS;;0BAC1E,KAACtL;0BAAa;;0BACd,KAACD;0BACC,cAAA,MAACG;oBAAMqL,SAAS;oBAAGpF,IAAI;wBAAEqF,IAAI;oBAAE;;wBAC5BtE,wBACC,MAACzG;4BACCgL,SAAS;4BACThG,MAAK;4BACLO,OAAM;4BACNQ,OAAOmC;4BACPf,UAAU,CAAC8D,QAAQvI;gCACjB,IAAIA,MAAMyF,QAAQzF;4BACpB;4BACA8C,cAAW;;8CAEX,KAACzF;oCAAagG,OAAM;8CAAW;;8CAC/B,KAAChG;oCAAagG,OAAM;8CAAU;;;6BAE9B;wBACHmC,SAAS,0BACR,KAAChJ;4BACC0K,SAAShB;4BACT7C,OAAOqC;4BACP8C,YAAYxC;4BACZyC,eAAe,CAACF,QAAQvI,MAAM0I;gCAC5B,IAAIA,WAAW,SAASzC,UAAUjG;4BACpC;4BACAyE,UAAU,CAAC8D,QAAQI;;gCACjBhD,UAAUgD;gCACV/C,gBAAQ+C,0BAAAA,OAAQrF,KAAK,mBAAI;gCACzByC,QAAQ4C,SAAStK,iBAAiBsK,OAAOnF,eAAe,IAAI;4BAC9D;4BACAoF,gBAAgB,CAACD,SAAWA,OAAOrF,KAAK;4BACxCuF,sBAAsB,CAACC,QAAQzF,QAAUyF,OAAOhE,GAAG,KAAKzB,MAAMyB,GAAG;4BACjE,+DAA+D;4BAC/D,sCAAsC;4BACtCiE,eAAe,CAAC7B,UAAYA;4BAC5B8B,cAAc,CAACC,aAAaN,uBAC1B,eAACO,mBAAOD;oCAAanE,KAAK6D,OAAO7D,GAAG;kDAClC,MAAC/H;oCAAMoM,WAAU;oCAAMf,SAAS;oCAAGpF,IAAI;wCAAEC,OAAO;wCAAQmG,YAAY;oCAAW;;sDAC7E,KAAC5L;4CAAW4C,SAAQ;4CAAQ4C,IAAI;gDAAEqG,MAAM;4CAAE;sDACvCV,OAAOrF,KAAK;;sDAEf,KAAC9F;4CAAW4C,SAAQ;4CAAUyC,OAAM;sDACjCtE,YAAYoK,OAAOnF,eAAe,EAAE;;;;4BAK7C8F,eAAetD,OAAO8B,IAAI,KAAK,8BAA8B;4BAC7DyB,aAAa,CAACC,uBACZ,KAACpM,wBACKoM;oCACJlG,OAAM;oCACNmG,aAAY;oCACZC,SAAS;oCACTC,YACEtK,aAAa,QACT,CAAC,2CAA2C,EAAEA,SAASuK,WAAW,GAAG,wBAAwB,CAAC,GAC9F3B;;2CAMZ,KAAC7K;4BACCkG,OAAM;4BACND,OAAOH;4BACPuB,UAAU,CAACC,QAAUkB,QAAQlB,MAAMC,MAAM,CAACtB,KAAK;4BAC/CqG,SAAS;4BACTxB,SAAS;4BACTjD,WAAW;gCAAEC,WAAW;oCAAE2E,WAAWpO;gCAAwB;4BAAE;;sCAGnE,MAACsB;4BAAMoM,WAAU;4BAAMf,SAAS;;8CAC9B,KAAChL;oCACCkG,OAAM;oCACNkB,MAAK;oCACLnB,OAAO7B;oCACPiD,UAAU,CAACC,QAAUmB,YAAYnB,MAAMC,MAAM,CAACtB,KAAK;oCACnDL,IAAI;wCAAEC,OAAO;oCAAI;oCACjBgC,WAAW;wCAAEC,WAAW;4CAAEC,KAAK;4CAAGC,KAAK1J;4CAA6B2J,MAAM;wCAAE;oCAAE;;8CAEhF,KAACjI;oCACCkG,OAAO,CAAC,aAAa,EAAEjE,SAASuK,WAAW,GAAG,CAAC,CAAC;oCAChDvG,OAAOyC;oCACPrB,UAAU,CAACC,QAAUqB,QAAQrB,MAAMC,MAAM,CAACtB,KAAK;oCAC/CoG,aAAY;oCACZzG,IAAI;wCAAEqG,MAAM;oCAAE;oCACdpE,WAAW;wCAAEC,WAAW;4CAAE4E,WAAW;wCAAU;oCAAE;;;;wBAGpD/B,WAAYF,CAAAA,YAAY/B,IAAG,kBAC1B,KAACtI;4BAAW4C,SAAQ;4BAAUyC,OAAM;sCACjCkF;6BAED;;;;0BAGR,MAACpL;;kCACC,KAACF;wBAAOiG,SAASoB;wBAASrB,UAAU9C;kCACjC;;kCAEH,KAAClD;wBAAO2D,SAAQ;wBAAYqC,UAAU9C,QAAQqE,QAAQ+D;wBAAUrF,SAASf;kCACtE;;;;;;AAKX;AACAiC,cAAcM,WAAW,GAAG;AAE5B,eAAexF,iBAAgB"}
@@ -121,6 +121,13 @@ function Row(props) {
121
121
  children: deal.notes
122
122
  })
123
123
  }) : null,
124
+ customFields.length ? /*#__PURE__*/ _jsx(Typography, {
125
+ variant: "subtitle2",
126
+ sx: {
127
+ pt: 1
128
+ },
129
+ children: 'More fields'
130
+ }) : null,
124
131
  customFields.map((definition)=>{
125
132
  var _deal_custom;
126
133
  const text = formatContactCustomValue(definition, (_deal_custom = deal.custom) == null ? void 0 : _deal_custom[definition.key]);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ContactFieldDefinition,\n dealStageById,\n pluginDocsHelp,\n weightedDealAmountCents,\n} from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Link, Stack, Typography } from '@mui/material'\nimport type { ReactNode } from 'react'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport {\n type DealDoc,\n formatMoney,\n type PipelineDoc,\n timestampMs,\n} from '../model/deal-board-model'\nimport type { CrmRoutes } from '../model/crm-routes'\n\nexport interface DealPropertiesCardProps {\n deal: DealDoc\n pipeline: PipelineDoc | null\n ownerLabel: string\n routes: CrmRoutes\n /**\n * The org's ACTIVE deal field definitions (AGL-2661), one row each under\n * the fixed properties. Handed in by the page, which resolves the org\n * once for every card on it.\n */\n customFields?: readonly ContactFieldDefinition[]\n}\n\nfunction Row(props: { label: string; children: ReactNode }) {\n return (\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'baseline' }}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ width: 120, flexShrink: 0 }}\n >\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\" sx={{ flex: 1, minWidth: 0 }}>\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\n/**\n * What the deal is worth and who it is with (AGL-2598).\n *\n * The weighted value is computed here from the live pipeline rather than\n * stored, because the stage's probability is the merchant's to change and\n * a stored figure would go stale the moment they did. The contact and the\n * company are links into their own pages — the id is the link; the name on\n * the deal is a caption that may lag a rename.\n */\nexport function DealPropertiesCard(props: DealPropertiesCardProps) {\n const { deal, pipeline, ownerLabel, routes, customFields = [] } = props\n const stage = dealStageById(pipeline, deal.stageId)\n const weighted = weightedDealAmountCents(deal, stage)\n const createdMs = timestampMs(deal.createdAt)\n return (\n <CardDisplay\n header={'Properties'}\n help={pluginDocsHelp('deals', { anchor: '#a-deals-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.25}>\n <Row label=\"Amount\">\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : 'Not set'}\n </Row>\n <Row label=\"Weighted value\">\n {`${formatMoney(weighted, deal.currency)}` +\n (deal.status === 'open' && stage ? ` at ${stage.probability}%` : '')}\n </Row>\n <Row label=\"Expected close\">\n {typeof deal.expectedCloseAtMs === 'number'\n ? new Date(deal.expectedCloseAtMs).toLocaleDateString()\n : 'Not set'}\n </Row>\n <Row label=\"Owner\">{ownerLabel || 'Nobody yet'}</Row>\n <Row label=\"Contact\">\n {deal.contactId ? (\n <AppLink href={routes.contact(deal.contactId)}>\n {deal.contactName || 'Open contact'}\n </AppLink>\n ) : (\n 'None'\n )}\n </Row>\n <Row label=\"Company\">\n {deal.companyId ? (\n <AppLink href={routes.company(deal.companyId)}>\n {deal.companyName || 'Open company'}\n </AppLink>\n ) : (\n 'None'\n )}\n </Row>\n <Row label=\"Pipeline\">{pipeline?.name ?? deal.pipelineId}</Row>\n <Row label=\"Created\">\n {createdMs ? new Date(createdMs).toLocaleDateString() : '—'}\n </Row>\n {deal.notes ? (\n <Row label=\"Notes\">\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap' }}>\n {deal.notes}\n </Typography>\n </Row>\n ) : null}\n {customFields.map((definition) => {\n const text = formatContactCustomValue(definition, deal.custom?.[definition.key])\n return (\n <Row key={definition.key} label={definition.label || definition.key}>\n {text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text || 'Not set'\n )}\n </Row>\n )\n })}\n </Stack>\n </CardDisplay>\n )\n}\nDealPropertiesCard.displayName = 'DealPropertiesCard'\n\nexport default DealPropertiesCard\n"],"names":["dealStageById","pluginDocsHelp","weightedDealAmountCents","AppLink","CardDisplay","Link","Stack","Typography","formatContactCustomValue","formatMoney","timestampMs","Row","props","direction","spacing","sx","alignItems","variant","color","width","flexShrink","label","component","flex","minWidth","children","DealPropertiesCard","deal","pipeline","ownerLabel","routes","customFields","stage","stageId","weighted","createdMs","createdAt","header","help","anchor","contentGutterX","contentGutterY","amountCents","currency","status","probability","expectedCloseAtMs","Date","toLocaleDateString","contactId","href","contact","contactName","companyId","company","companyName","name","pipelineId","notes","whiteSpace","map","definition","text","custom","key","type","target","rel","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/deal-properties-card.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport {\n type ContactFieldDefinition,\n dealStageById,\n pluginDocsHelp,\n weightedDealAmountCents,\n} from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Link, Stack, Typography } from '@mui/material'\nimport type { ReactNode } from 'react'\nimport { formatContactCustomValue } from './contact-custom-columns'\nimport {\n type DealDoc,\n formatMoney,\n type PipelineDoc,\n timestampMs,\n} from '../model/deal-board-model'\nimport type { CrmRoutes } from '../model/crm-routes'\n\nexport interface DealPropertiesCardProps {\n deal: DealDoc\n pipeline: PipelineDoc | null\n ownerLabel: string\n routes: CrmRoutes\n /**\n * The org's ACTIVE deal field definitions (AGL-2661), one row each under\n * the fixed properties. Handed in by the page, which resolves the org\n * once for every card on it.\n */\n customFields?: readonly ContactFieldDefinition[]\n}\n\nfunction Row(props: { label: string; children: ReactNode }) {\n return (\n <Stack direction=\"row\" spacing={2} sx={{ alignItems: 'baseline' }}>\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ width: 120, flexShrink: 0 }}\n >\n {props.label}\n </Typography>\n <Typography variant=\"body2\" component=\"div\" sx={{ flex: 1, minWidth: 0 }}>\n {props.children}\n </Typography>\n </Stack>\n )\n}\n\n/**\n * What the deal is worth and who it is with (AGL-2598).\n *\n * The weighted value is computed here from the live pipeline rather than\n * stored, because the stage's probability is the merchant's to change and\n * a stored figure would go stale the moment they did. The contact and the\n * company are links into their own pages — the id is the link; the name on\n * the deal is a caption that may lag a rename.\n */\nexport function DealPropertiesCard(props: DealPropertiesCardProps) {\n const { deal, pipeline, ownerLabel, routes, customFields = [] } = props\n const stage = dealStageById(pipeline, deal.stageId)\n const weighted = weightedDealAmountCents(deal, stage)\n const createdMs = timestampMs(deal.createdAt)\n return (\n <CardDisplay\n header={'Properties'}\n help={pluginDocsHelp('deals', { anchor: '#a-deals-page' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.25}>\n <Row label=\"Amount\">\n {typeof deal.amountCents === 'number'\n ? formatMoney(deal.amountCents, deal.currency)\n : 'Not set'}\n </Row>\n <Row label=\"Weighted value\">\n {`${formatMoney(weighted, deal.currency)}` +\n (deal.status === 'open' && stage ? ` at ${stage.probability}%` : '')}\n </Row>\n <Row label=\"Expected close\">\n {typeof deal.expectedCloseAtMs === 'number'\n ? new Date(deal.expectedCloseAtMs).toLocaleDateString()\n : 'Not set'}\n </Row>\n <Row label=\"Owner\">{ownerLabel || 'Nobody yet'}</Row>\n <Row label=\"Contact\">\n {deal.contactId ? (\n <AppLink href={routes.contact(deal.contactId)}>\n {deal.contactName || 'Open contact'}\n </AppLink>\n ) : (\n 'None'\n )}\n </Row>\n <Row label=\"Company\">\n {deal.companyId ? (\n <AppLink href={routes.company(deal.companyId)}>\n {deal.companyName || 'Open company'}\n </AppLink>\n ) : (\n 'None'\n )}\n </Row>\n <Row label=\"Pipeline\">{pipeline?.name ?? deal.pipelineId}</Row>\n <Row label=\"Created\">\n {createdMs ? new Date(createdMs).toLocaleDateString() : '—'}\n </Row>\n {deal.notes ? (\n <Row label=\"Notes\">\n <Typography variant=\"body2\" sx={{ whiteSpace: 'pre-wrap' }}>\n {deal.notes}\n </Typography>\n </Row>\n ) : null}\n {/*\n The org's own deal fields (AGL-2661), after the built-in ones under\n \"More fields\", as a record's Details keep them; none defined, none\n drawn.\n */}\n {customFields.length ? (\n <Typography variant=\"subtitle2\" sx={{ pt: 1 }}>\n {'More fields'}\n </Typography>\n ) : null}\n {customFields.map((definition) => {\n const text = formatContactCustomValue(definition, deal.custom?.[definition.key])\n return (\n <Row key={definition.key} label={definition.label || definition.key}>\n {text && definition.type === 'url' ? (\n <Link href={text} target=\"_blank\" rel=\"noreferrer noopener\">\n {text}\n </Link>\n ) : (\n text || 'Not set'\n )}\n </Row>\n )\n })}\n </Stack>\n </CardDisplay>\n )\n}\nDealPropertiesCard.displayName = 'DealPropertiesCard'\n\nexport default DealPropertiesCard\n"],"names":["dealStageById","pluginDocsHelp","weightedDealAmountCents","AppLink","CardDisplay","Link","Stack","Typography","formatContactCustomValue","formatMoney","timestampMs","Row","props","direction","spacing","sx","alignItems","variant","color","width","flexShrink","label","component","flex","minWidth","children","DealPropertiesCard","deal","pipeline","ownerLabel","routes","customFields","stage","stageId","weighted","createdMs","createdAt","header","help","anchor","contentGutterX","contentGutterY","amountCents","currency","status","probability","expectedCloseAtMs","Date","toLocaleDateString","contactId","href","contact","contactName","companyId","company","companyName","name","pipelineId","notes","whiteSpace","length","pt","map","definition","text","custom","key","type","target","rel","displayName"],"mappings":"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"}
@@ -68,36 +68,38 @@ import { LostReasonDialog } from "./lost-reason-dialog.js";
68
68
  help: pluginDocsHelp('deals', {
69
69
  anchor: '#moving-winning-and-losing'
70
70
  }),
71
- actions: !closed ? /*#__PURE__*/ _jsxs(Stack, {
72
- direction: "row",
73
- spacing: 1,
74
- children: [
75
- /*#__PURE__*/ _jsx(Button, {
76
- size: "small",
77
- color: "success",
78
- variant: "outlined",
79
- disabled: busy || !pipeline,
80
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
81
- path: mdiTrophyOutline.path,
82
- size: 0.8
83
- }),
84
- onClick: ()=>void run(()=>api.markWon(deal), 'Deal won'),
85
- children: 'Won'
86
- }),
87
- /*#__PURE__*/ _jsx(Button, {
88
- size: "small",
89
- color: "error",
90
- variant: "outlined",
91
- disabled: busy || !pipeline,
92
- startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
93
- path: mdiThumbDownOutline.path,
94
- size: 0.8
71
+ HeaderProps: {
72
+ action: !closed ? /*#__PURE__*/ _jsxs(Stack, {
73
+ direction: "row",
74
+ spacing: 1,
75
+ children: [
76
+ /*#__PURE__*/ _jsx(Button, {
77
+ size: "small",
78
+ color: "success",
79
+ variant: "outlined",
80
+ disabled: busy || !pipeline,
81
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
82
+ path: mdiTrophyOutline.path,
83
+ size: 0.8
84
+ }),
85
+ onClick: ()=>void run(()=>api.markWon(deal), 'Deal won'),
86
+ children: 'Won'
95
87
  }),
96
- onClick: ()=>setLosing(true),
97
- children: 'Lost'
98
- })
99
- ]
100
- }) : null,
88
+ /*#__PURE__*/ _jsx(Button, {
89
+ size: "small",
90
+ color: "error",
91
+ variant: "outlined",
92
+ disabled: busy || !pipeline,
93
+ startIcon: /*#__PURE__*/ _jsx(MdiIcon, {
94
+ path: mdiThumbDownOutline.path,
95
+ size: 0.8
96
+ }),
97
+ onClick: ()=>setLosing(true),
98
+ children: 'Lost'
99
+ })
100
+ ]
101
+ }) : null
102
+ },
101
103
  contentGutterX: true,
102
104
  contentGutterY: true,
103
105
  children: [