@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/companies-section.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 CRM_COLLECTIONS,\n type CrmCompany,\n filterByNextActivity,\n isNoNextActivityClause,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport {\n listFilterColumn,\n type ListFilterRequest,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport {\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n CRM_NEXT_ACTIVITY_FILTER_HEADER,\n nextActivityColumn,\n} from './crm-next-activity-column'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport CrmViewsControl from './crm-views-control'\nimport { CrmListActions, CrmListToolbar } from './crm-list-toolbar'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport {\n listFilterConstraints,\n usePagedCollection,\n useFirestore,\n} from '@aglyn/tenant-feature-instance'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport {\n getGridSingleSelectOperators,\n type GridColDef,\n} from '@mui/x-data-grid'\nimport { collection, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useMemo, useState } from 'react'\nimport { COMPANY_LIST_FILTER_FIELDS } from '../constants/company-filters'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CompanyCsvOptions, companiesCsv } from '../model/companies-csv'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport CompaniesBulkBar from './companies-bulk-bar'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { CompanyImportButton } from './company-import-drawer'\nimport { customFieldColumns } from './contact-custom-columns'\n\nexport interface CompaniesSectionProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n /** The CRM hub URL, which every company route hangs beneath. */\n basePath: string\n}\n\ntype CompanyRow = Partial<CrmCompany> & { $id: string; updatedAt?: any }\n\n/** What the companies grid's panel offers: the served fields, and \"No next activity\". */\nconst COMPANY_GRID_FILTER_FIELDS = [\n ...COMPANY_LIST_FILTER_FIELDS,\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n]\nconst COMPANY_GRID_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Company',\n ownerUid: 'Owner',\n [CRM_NEXT_ACTIVITY_FILTER_FIELD.column]: CRM_NEXT_ACTIVITY_FILTER_HEADER,\n}\n\n/**\n * `/crm/companies` — the organizations behind the people (AGL-2597).\n *\n * A section of its own rather than a column on the contact list, because a\n * company is known by several contacts and carries records of its own: a\n * domain the email suggestion keys on, an address, an owner, and the deals\n * and tasks filed against it. The row opens `/crm/companies/{id}`, where all\n * of that lives; this list is the cheaper surface and reads nothing a row\n * does not show.\n *\n * ## What the listener carries\n *\n * Paged and ordered by the server — `updatedAt desc`, so a company somebody\n * just touched is on page one — and SCOPED by the same `visibleTo\n * array-contains-any` predicate the contact list runs. That predicate is what\n * the rules evaluate: a filtered query is provable per document, and an\n * unfiltered one is refused rather than quietly returning the whole org's\n * accounts to a site that may see one client's.\n *\n * ## What a filter may be\n *\n * The scope predicate is an array clause, and Firestore carries one per\n * query, so the word-prefix search the contact list offers cannot run here.\n * The name is a prefix range and an exact match instead, and the owner is a\n * choice from the roster — each reaching the whole collection, which is the\n * property a search must have: a company on page four is found, not reported\n * missing. `COMPANY_LIST_FILTER_FIELDS` states what the indexes serve.\n *\n * ## Creating is a drawer\n *\n * \"New company\" opens the same eight-field drawer the company's page edits\n * with, and the new record's page opens when it is saved. A form above a\n * list has nowhere to grow, and this one has an address in it.\n *\n * ## Selection, the bar and the file (AGL-2621)\n *\n * The rows are selectable, and a selection raises `CompaniesBulkBar` over\n * the table. Export CSV writes the loaded page through `companiesCsv()` —\n * the same file the bar writes over the selection, headed as the import\n * reads it — and Import CSV opens the companies drawer beside it.\n */\nexport function CompaniesSection(props: CompaniesSectionProps) {\n const { hostId, org, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const firestore = useFirestore()\n const { scope, orgId, visibleTo } = useCrmScope({ hostId, org })\n /*\n * The team, read once for this surface: the Owner column names a person\n * from a uid, and the create drawer picks from the same roster. One fetch\n * serves both, which is why the drawer takes it as a prop.\n */\n const members = useOrgMemberOptions(orgId)\n // The org's company fields, for the optional columns below (AGL-2661).\n const companyFields = useContactFieldDefinitions(orgId, 'company')\n\n /*\n * The column filter is the saved VIEW'S first clause (AGL-2617): this\n * grid's panel holds one, and a view of companies holds that one beside\n * the columns and the sort. Declared BEFORE the listener that reads it,\n * as the filter always was — the query is rebuilt from it.\n */\n const views = useCrmSavedView({ section: 'companies', hostId, org, basePath })\n /*\n * ...beside the \"No next activity\" clause (AGL-2661), which is not a\n * query the listener can run — absence has no index — and narrows the\n * loaded page below instead. The grid's one clause is the first that is\n * not it, and setting the grid's clause keeps it.\n */\n const viewFilters = views.state.filters\n const filter: ListFilterRequest | null =\n viewFilters.find((clause) => !isNoNextActivityClause(clause)) ?? null\n const setFilter = useCallback(\n (request: ListFilterRequest | null) =>\n views.setFilters([\n ...(request ? [request] : []),\n ...viewFilters.filter(isNoNextActivityClause),\n ]),\n [views.setFilters, viewFilters],\n )\n\n const {\n rows: loaded,\n status,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<CompanyRow>(\n (pageLimit) => {\n if (!scope) return null\n const constraints = listFilterConstraints(\n COMPANY_LIST_FILTER_FIELDS,\n filter,\n )\n return query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n // Absent at the organization level, where the tokens are `null` (AGL-2630).\n ...crmVisibleToClause(visibleTo),\n ...(constraints ?? [orderBy('updatedAt', 'desc')]),\n limit(pageLimit),\n )\n },\n [firestore, scope, visibleTo, filter],\n { idField: '$id' },\n )\n const companies = useMemo(\n () => filterByNextActivity(loaded, viewFilters),\n [loaded, viewFilters],\n )\n const nowMs = useMemo(() => Date.now(), [])\n\n const [createOpen, setCreateOpen] = useState(false)\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n /*\n * The owner is written by ADDRESS, because the companies import resolves\n * an owner by email; the same options reach the bar, so the page's file\n * and the selection's are one format.\n */\n const csvOptions: CompanyCsvOptions = useMemo(\n () => ({ ownerEmail: members.emailFor }),\n [members.emailFor],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('companies.csv', 'text/csv', companiesCsv(companies, csvOptions))\n }, [companies, csvOptions])\n const openCompany = useCallback(\n (id: string) => router.push(routes.company(id)),\n [router, routes],\n )\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'name',\n headerName: 'Company',\n flex: 1.6,\n minWidth: 240,\n ...listFilterColumn(COMPANY_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: CompanyRow) => String(row.name ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Stack\n sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}\n >\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name}\n </Typography>\n {row.industry ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.industry}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n // Stored normalized, so it could be filtered exactly — but a domain\n // lookup is what the company suggestion on a contact does, and a\n // filter here would need an index of its own for one more way to\n // find a row the name filter already finds.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => String(row.domain ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.domain ? (\n <Typography variant=\"body2\">{row.domain}</Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'contactsCount',\n headerName: 'Contacts',\n width: 110,\n align: 'right',\n headerAlign: 'right',\n /*\n * The denormalized count every link moves (AGL-2613) — a stored\n * number, so a page of companies costs no read per row. Absent on a\n * company nobody has linked since the counter existed, which reads\n * as zero; the company's own page takes the live aggregate.\n */\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => Number(row.contactsCount ?? 0),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.contactsCount ? 'text.primary' : 'text.secondary'}\n >\n {Number(row.contactsCount ?? 0).toLocaleString()}\n </Typography>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 140,\n // Written by the drawer and the bulk bar (AGL-2621); read here, not\n // filtered — a tag filter would need an `array-contains` index of\n // its own beside the scope predicate.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.tags?.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', overflow: 'hidden' }}>\n {row.tags.slice(0, 3).map((tag) => (\n <Chip key={tag} size=\"small\" variant=\"outlined\" label={tag} />\n ))}\n {row.tags.length > 3 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${row.tags.length - 3}`}\n </Typography>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 160,\n sortable: false,\n /*\n * A CHOICE from the roster, not a text box for a uid. The grid's\n * single-select `is` operator is what the panel offers; the handler\n * below maps it onto the declared field's `equals`, which the\n * translator turns into the equality the index serves.\n */\n type: 'singleSelect',\n valueOptions: members.options.map((option) => ({\n value: option.uid,\n label: crmMemberPickerLabel(option),\n })),\n filterable: members.options.length > 0,\n filterOperators: getGridSingleSelectOperators().filter(\n (operator) => operator.value === 'is',\n ),\n valueGetter: (_value, row: CompanyRow) => String(row.ownerUid ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.ownerUid ? 'text.primary' : 'text.secondary'}\n >\n {row.ownerUid\n ? members.ready\n ? members.labelFor(row.ownerUid)\n : '…'\n : '—'}\n </Typography>\n ),\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) =>\n row.updatedAt?.seconds ? new Date(row.updatedAt.seconds * 1000) : null,\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.updatedAt?.seconds\n ? new Date(row.updatedAt.seconds * 1000).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the company is due (AGL-2661).\n nextActivityColumn(nowMs),\n // The org's company fields as optional columns (AGL-2661), read off\n // the row's own `custom` map the way the contacts list reads its own.\n ...customFieldColumns(companyFields.active),\n ],\n [members, companyFields.active, nowMs],\n )\n\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313). The query serves ONE clause — a paged list has no window to\n * narrow beyond the page — so the panel's clause replaces the last, and\n * \"No next activity\", which narrows the loaded page, stands beside it.\n * The search box is the name's prefix: the one text search the indexes\n * serve here, so typing in it is the same clause as \"Company starts with\".\n */\n const ownerOptions = useMemo(\n () =>\n members.options.map((option) => ({\n value: option.uid,\n label: crmMemberPickerLabel(option),\n })),\n [members.options],\n )\n const filterColumns = useMemo(\n () =>\n listFilterGridColumns(columns, COMPANY_GRID_FILTER_FIELDS, { ownerUid: ownerOptions }, COMPANY_GRID_FILTER_HEADERS),\n [columns, ownerOptions],\n )\n const searchWords = useMemo(\n () => (filter?.field === 'name' && filter.op === 'startsWith' ? [filter.value] : []),\n [filter],\n )\n const setSearchWords = useCallback(\n (words: string[]) => {\n const term = words.join(' ').trim()\n if (term) setFilter({ field: 'name', op: 'startsWith', value: term })\n else if (filter?.field === 'name' && filter.op === 'startsWith') setFilter(null)\n },\n [filter, setFilter],\n )\n const gridFilter = useListGridFilter({\n clauses: viewFilters,\n onChange: views.setFilters,\n selectFields: ['ownerUid'],\n single: true,\n keepAlongside: isNoNextActivityClause,\n search: { words: searchWords, onChange: setSearchWords },\n })\n /*\n * The grid's models are the view's (AGL-2617). The filter model shows the\n * view's clause in the panel as a typed one would appear — the owner's\n * stored `equals` back as the single-select `is` the panel offers — so a\n * view opened from its address reads as filtered, not as a mystery.\n */\n const grid = useCrmViewGrid(views, filterColumns)\n\n const newCompanyButton = (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!scope}\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n onClick={() => setCreateOpen(true)}\n >\n {'New company'}\n </Button>\n )\n\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('companies', { anchor: '#the-companies-list' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n <CompanyImportButton hostId={hostId} org={org} />\n <Button size=\"small\" onClick={handleExport} disabled={!companies.length}>\n {'Export CSV'}\n </Button>\n {newCompanyButton}\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The organizations your contacts belong to. Open one to see its ' +\n 'people, its deals and its open tasks, or to link a contact ' +\n 'to it.'}\n </Typography>\n {/* The saved view, and the clauses narrowing it as chips (AGL-3313). */}\n <CrmListToolbar label=\"Company filters\">\n <CrmViewsControl controller={views} allLabel=\"All companies\" />\n <ListFilterChips\n fields={COMPANY_GRID_FILTER_FIELDS}\n headers={COMPANY_GRID_FILTER_HEADERS}\n clauses={viewFilters}\n onChange={views.setFilters}\n options={{ ownerUid: ownerOptions }}\n servedField={filter?.field ?? null}\n />\n </CrmListToolbar>\n <CompaniesBulkBar\n hostId={hostId}\n scope={scope}\n rows={companies}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n members={members}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n rows={companies}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n noRowsLabel=\"No companies yet\"\n noRowsDescription=\"A company groups the contacts who work at one business, with its domain, owner and address. Create the first one, or link a contact to a company from their page.\"\n noRowsAction={newCompanyButton}\n onOpen={(id) => openCompany(String(id))}\n // An empty table while the read is in flight reads as \"you have\n // none\" rather than \"these are on their way\".\n loading={!scope || status === 'loading'}\n /*\n * The grid must NOT also filter. The query answers it, so a client\n * pass could only drop rows the query already matched.\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={companies.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n {/*\n Mounted only while it is open, so the list pays for none of the\n drawer's state — and a fresh mount is what seeds an empty form.\n */}\n {createOpen ? (\n <CompanyEditDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n members={members}\n onSaved={openCompany}\n />\n ) : null}\n </CardDisplay>\n )\n}\nCompaniesSection.displayName = 'CompaniesSection'\n\nexport default CompaniesSection\n"],"names":["CRM_COLLECTIONS","filterByNextActivity","isNoNextActivityClause","pluginDocsHelp","crmMemberPickerLabel","mdiPlus","CardDisplay","MdiIcon","ListPagination","ListTable","listFilterColumn","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_NEXT_ACTIVITY_FILTER_HEADER","nextActivityColumn","ListFilterChips","listFilterGridColumns","useListGridFilter","CrmViewsControl","CrmListActions","CrmListToolbar","TABLE_ROW_HEIGHT","listFilterConstraints","usePagedCollection","useFirestore","Button","Chip","Stack","Typography","getGridSingleSelectOperators","collection","limit","orderBy","query","useRouter","useCallback","useMemo","useState","COMPANY_LIST_FILTER_FIELDS","useContactFieldDefinitions","crmVisibleToClause","useCrmScope","useOrgMemberOptions","companiesCsv","downloadTextFile","crmRoutes","CompaniesBulkBar","CompanyEditDrawer","CompanyImportButton","customFieldColumns","COMPANY_GRID_FILTER_FIELDS","COMPANY_GRID_FILTER_HEADERS","name","ownerUid","column","CompaniesSection","props","viewFilters","hostId","org","basePath","routes","router","firestore","scope","orgId","visibleTo","members","companyFields","views","section","state","filters","filter","find","clause","setFilter","request","setFilters","rows","loaded","status","hasMore","page","setPage","pageSize","setPageSize","pageLimit","constraints","companies","idField","nowMs","Date","now","createOpen","setCreateOpen","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","handleExport","openCompany","id","push","company","columns","field","headerName","flex","minWidth","valueGetter","_value","row","String","renderCell","sx","justifyContent","height","lineHeight","variant","industry","color","noWrap","filterable","sortable","domain","width","align","headerAlign","Number","contactsCount","toLocaleString","tags","join","length","direction","spacing","alignItems","overflow","slice","map","tag","size","label","type","valueOptions","options","option","value","uid","filterOperators","operator","ready","labelFor","updatedAt","seconds","toLocaleDateString","active","ownerOptions","filterColumns","searchWords","op","setSearchWords","words","term","trim","gridFilter","clauses","onChange","selectFields","single","keepAlongside","search","grid","newCompanyButton","disabled","startIcon","path","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","controller","allLabel","fields","headers","servedField","selected","onSelectedChange","csv","columnOrder","rowHeight","slots","selectable","noRowsLabel","noRowsDescription","noRowsAction","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","onSaved","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/companies-section.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 CRM_COLLECTIONS,\n type CrmCompany,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiPlus } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { listFilterColumn } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport ListQueryNotices, {\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport { useCrmSavedView } from '../hooks/use-crm-saved-view'\nimport { useCrmViewGrid } from '../hooks/use-crm-view-grid'\nimport { CRM_LIST_SLOTS, CrmColumnOrderProvider } from './crm-column-menu'\nimport {\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n CRM_NEXT_ACTIVITY_FILTER_HEADER,\n nextActivityColumn,\n} from './crm-next-activity-column'\nimport { useCrmFoldsScope, useCrmListQuery } from '../hooks/use-crm-list-query'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport CrmViewsControl from './crm-views-control'\nimport { CrmListActions, CrmListToolbar } from './crm-list-toolbar'\nimport { TABLE_ROW_HEIGHT } from '@aglyn/shared-ui-jsx/const/table-pagination'\nimport { Button, Chip, Stack, Typography } from '@mui/material'\nimport {\n getGridSingleSelectOperators,\n type GridColDef,\n} from '@mui/x-data-grid'\nimport { useRouter } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport {\n COMPANY_LIST_DECLARATION,\n COMPANY_LIST_FILTER_FIELDS,\n COMPANY_PREFIX_SEARCH,\n} from '../constants/company-filters'\nimport { useContactFieldDefinitions } from '../hooks/use-contact-field-definitions'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberOptions } from '../hooks/use-org-member-options'\nimport { type CompanyCsvOptions, companiesCsv } from '../model/companies-csv'\nimport { downloadTextFile } from '../model/contacts-csv'\nimport { crmRoutes } from '../model/crm-routes'\nimport CompaniesBulkBar from './companies-bulk-bar'\nimport CompanyEditDrawer from './company-edit-drawer'\nimport { CompanyImportButton } from './company-import-drawer'\nimport { customFieldColumns } from './contact-custom-columns'\n\nexport interface CompaniesSectionProps {\n /** The site the list is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling>\n /** The CRM hub URL, which every company route hangs beneath. */\n basePath: string\n}\n\ntype CompanyRow = Partial<CrmCompany> & { $id: string; updatedAt?: any }\n\n/** What the companies grid's panel offers — every field the query asks. */\nconst COMPANY_GRID_FILTER_FIELDS = COMPANY_LIST_FILTER_FIELDS\nconst COMPANY_GRID_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Company',\n ownerUid: 'Owner',\n [CRM_NEXT_ACTIVITY_FILTER_FIELD.column]: CRM_NEXT_ACTIVITY_FILTER_HEADER,\n}\n\n/**\n * `/crm/companies` — the organizations behind the people (AGL-2597).\n *\n * A section of its own rather than a column on the contact list, because a\n * company is known by several contacts and carries records of its own: a\n * domain the email suggestion keys on, an address, an owner, and the deals\n * and tasks filed against it. The row opens `/crm/companies/{id}`, where all\n * of that lives; this list is the cheaper surface and reads nothing a row\n * does not show.\n *\n * ## What the listener carries\n *\n * Paged and ordered by the server — `updatedAt desc`, so a company somebody\n * just touched is on page one — and SCOPED by the same `visibleTo\n * array-contains-any` predicate the contact list runs. That predicate is what\n * the rules evaluate: a filtered query is provable per document, and an\n * unfiltered one is refused rather than quietly returning the whole org's\n * accounts to a site that may see one client's.\n *\n * ## What a filter may be\n *\n * Every clause and the search word are on the one query (AGL-3321): the\n * search reads the name's and the domain's word prefixes, the name is also\n * an exact match and a prefix, the owner a choice from the roster, and \"No\n * next activity\" a `null` — each reaching the whole collection, which is\n * the property a filter must have: a company on page four is found, not\n * reported missing. `COMPANY_LIST_DECLARATION` states what the query asks.\n *\n * ## Creating is a drawer\n *\n * \"New company\" opens the same eight-field drawer the company's page edits\n * with, and the new record's page opens when it is saved. A form above a\n * list has nowhere to grow, and this one has an address in it.\n *\n * ## Selection, the bar and the file (AGL-2621)\n *\n * The rows are selectable, and a selection raises `CompaniesBulkBar` over\n * the table. Export CSV writes the loaded page through `companiesCsv()` —\n * the same file the bar writes over the selection, headed as the import\n * reads it — and Import CSV opens the companies drawer beside it.\n */\nexport function CompaniesSection(props: CompaniesSectionProps) {\n const { hostId, org, basePath } = props\n const routes = crmRoutes(basePath)\n const router = useRouter()\n const { scope, orgId, visibleTo } = useCrmScope({ hostId, org })\n /*\n * The team, read once for this surface: the Owner column names a person\n * from a uid, and the create drawer picks from the same roster. One fetch\n * serves both, which is why the drawer takes it as a prop.\n */\n const members = useOrgMemberOptions(orgId)\n // The org's company fields, for the optional columns below (AGL-2661).\n const companyFields = useContactFieldDefinitions(orgId, 'company')\n\n /*\n * The clauses are the saved VIEW'S (AGL-2617), beside its columns and\n * sort; the grid's panel and the chips edit them, and the query answers\n * every one and the search word together (AGL-3321).\n */\n const views = useCrmSavedView({ section: 'companies', hostId, org, basePath })\n const viewFilters = views.state.filters\n const [searchWords, setSearchWords] = useState<string[]>([])\n const foldsScope = useCrmFoldsScope(orgId, visibleTo)\n const {\n rows: companies,\n status,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useCrmListQuery<CompanyRow>({\n scope,\n collection: CRM_COLLECTIONS.companies,\n visibleTo,\n foldsScope,\n declaration: COMPANY_LIST_DECLARATION,\n clauses: viewFilters,\n search: searchWords,\n prefixSearch: COMPANY_PREFIX_SEARCH,\n })\n const nowMs = useMemo(() => Date.now(), [])\n\n const [createOpen, setCreateOpen] = useState(false)\n const [selectedIds, setSelectedIds] = useState<string[]>([])\n // A filter, a search or a page is a different set of rows.\n useEffect(() => setSelectedIds([]), [plan, page])\n /*\n * The owner is written by ADDRESS, because the companies import resolves\n * an owner by email; the same options reach the bar, so the page's file\n * and the selection's are one format.\n */\n const csvOptions: CompanyCsvOptions = useMemo(\n () => ({ ownerEmail: members.emailFor }),\n [members.emailFor],\n )\n const handleExport = useCallback(() => {\n downloadTextFile('companies.csv', 'text/csv', companiesCsv(companies, csvOptions))\n }, [companies, csvOptions])\n const openCompany = useCallback(\n (id: string) => router.push(routes.company(id)),\n [router, routes],\n )\n\n const columns: GridColDef[] = useMemo(\n () => [\n {\n field: 'name',\n headerName: 'Company',\n flex: 1.6,\n minWidth: 240,\n ...listFilterColumn(COMPANY_LIST_FILTER_FIELDS, 'name'),\n valueGetter: (_value, row: CompanyRow) => String(row.name ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Stack\n sx={{ justifyContent: 'center', height: '100%', lineHeight: 1.25 }}\n >\n <Typography variant=\"body2\" sx={{ lineHeight: 1.25 }}>\n {row.name}\n </Typography>\n {row.industry ? (\n <Typography\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ lineHeight: 1.25 }}\n noWrap\n >\n {row.industry}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'domain',\n headerName: 'Domain',\n flex: 1,\n minWidth: 160,\n // Stored normalized, so it could be filtered exactly — but a domain\n // lookup is what the company suggestion on a contact does, and a\n // filter here would need an index of its own for one more way to\n // find a row the name filter already finds.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => String(row.domain ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.domain ? (\n <Typography variant=\"body2\">{row.domain}</Typography>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'contactsCount',\n headerName: 'Contacts',\n width: 110,\n align: 'right',\n headerAlign: 'right',\n /*\n * The denormalized count every link moves (AGL-2613) — a stored\n * number, so a page of companies costs no read per row. Absent on a\n * company nobody has linked since the counter existed, which reads\n * as zero; the company's own page takes the live aggregate.\n */\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => Number(row.contactsCount ?? 0),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.contactsCount ? 'text.primary' : 'text.secondary'}\n >\n {Number(row.contactsCount ?? 0).toLocaleString()}\n </Typography>\n ),\n },\n {\n field: 'tags',\n headerName: 'Tags',\n flex: 1,\n minWidth: 140,\n // Written by the drawer and the bulk bar (AGL-2621); read here, not\n // filtered — a tag filter would need an `array-contains` index of\n // its own beside the scope predicate.\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) => (row.tags ?? []).join(', '),\n renderCell: ({ row }: { row: CompanyRow }) =>\n row.tags?.length ? (\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center', overflow: 'hidden' }}>\n {row.tags.slice(0, 3).map((tag) => (\n <Chip key={tag} size=\"small\" variant=\"outlined\" label={tag} />\n ))}\n {row.tags.length > 3 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${row.tags.length - 3}`}\n </Typography>\n ) : null}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'—'}\n </Typography>\n ),\n },\n {\n field: 'ownerUid',\n headerName: 'Owner',\n flex: 1,\n minWidth: 160,\n sortable: false,\n /*\n * A CHOICE from the roster, not a text box for a uid. The grid's\n * single-select `is` operator is what the panel offers; the handler\n * below maps it onto the declared field's `equals`, which the\n * translator turns into the equality the index serves.\n */\n type: 'singleSelect',\n valueOptions: members.options.map((option) => ({\n value: option.uid,\n label: crmMemberPickerLabel(option),\n })),\n filterable: members.options.length > 0,\n filterOperators: getGridSingleSelectOperators().filter(\n (operator) => operator.value === 'is',\n ),\n valueGetter: (_value, row: CompanyRow) => String(row.ownerUid ?? ''),\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography\n variant=\"body2\"\n color={row.ownerUid ? 'text.primary' : 'text.secondary'}\n >\n {row.ownerUid\n ? members.ready\n ? members.labelFor(row.ownerUid)\n : '…'\n : '—'}\n </Typography>\n ),\n },\n {\n field: 'updatedAt',\n headerName: 'Updated',\n flex: 0.8,\n minWidth: 140,\n filterable: false,\n sortable: false,\n valueGetter: (_value, row: CompanyRow) =>\n row.updatedAt?.seconds ? new Date(row.updatedAt.seconds * 1000) : null,\n renderCell: ({ row }: { row: CompanyRow }) => (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {row.updatedAt?.seconds\n ? new Date(row.updatedAt.seconds * 1000).toLocaleDateString()\n : '—'}\n </Typography>\n ),\n },\n // When the earliest open task against the company is due (AGL-2661).\n nextActivityColumn(nowMs),\n // The org's company fields as optional columns (AGL-2661), read off\n // the row's own `custom` map the way the contacts list reads its own.\n ...customFieldColumns(companyFields.active),\n ],\n [members, companyFields.active, nowMs],\n )\n\n /*\n * The grid's own Filters panel and quick search edit the view's clauses\n * (AGL-3313); the query answers them all (AGL-3321).\n */\n const ownerOptions = useMemo(\n () =>\n members.options.map((option) => ({\n value: option.uid,\n label: crmMemberPickerLabel(option),\n })),\n [members.options],\n )\n const filterColumns = useMemo(\n () =>\n listFilterGridColumns(columns, COMPANY_GRID_FILTER_FIELDS, { ownerUid: ownerOptions }, COMPANY_GRID_FILTER_HEADERS),\n [columns, ownerOptions],\n )\n const gridFilter = useListGridFilter({\n clauses: viewFilters,\n onChange: views.setFilters,\n selectFields: ['ownerUid'],\n search: { words: searchWords, onChange: setSearchWords },\n })\n // What the query could not hold, named by the clause the reader set.\n const refused = useMemo(\n () =>\n listQueryRefusals(plan.refused, {\n fields: COMPANY_GRID_FILTER_FIELDS,\n headers: COMPANY_GRID_FILTER_HEADERS,\n options: { ownerUid: ownerOptions },\n }),\n [plan.refused, ownerOptions],\n )\n /*\n * The grid's models are the view's (AGL-2617). The filter model shows the\n * view's clause in the panel as a typed one would appear — the owner's\n * stored `equals` back as the single-select `is` the panel offers — so a\n * view opened from its address reads as filtered, not as a mystery.\n */\n const grid = useCrmViewGrid(views, filterColumns)\n\n /** Whether anything narrows the list, so an empty one is \"no match\", not \"none yet\". */\n const narrowed = viewFilters.length > 0 || searchWords.some((word) => word.trim())\n\n const newCompanyButton = (\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n disabled={!scope}\n startIcon={<MdiIcon path={mdiPlus.path} size={0.8} />}\n onClick={() => setCreateOpen(true)}\n >\n {'New company'}\n </Button>\n )\n\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('companies', { anchor: '#the-companies-list' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n // The record actions, top right and never clipped (AGL-3311).\n action: (\n <CrmListActions>\n <CompanyImportButton hostId={hostId} org={org} />\n <Button size=\"small\" onClick={handleExport} disabled={!companies.length}>\n {'Export CSV'}\n </Button>\n {newCompanyButton}\n </CrmListActions>\n ),\n }}\n >\n <Stack spacing={1.5}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The organizations your contacts belong to. Open one to see its ' +\n 'people, its deals and its open tasks, or to link a contact ' +\n 'to it.'}\n </Typography>\n {/* The saved view, and the clauses narrowing it as chips (AGL-3313). */}\n <CrmListToolbar label=\"Company filters\">\n <CrmViewsControl controller={views} allLabel=\"All companies\" />\n <ListFilterChips\n fields={COMPANY_GRID_FILTER_FIELDS}\n headers={COMPANY_GRID_FILTER_HEADERS}\n clauses={viewFilters}\n onChange={views.setFilters}\n options={{ ownerUid: ownerOptions }}\n marksServed={false}\n />\n </CrmListToolbar>\n <ListQueryNotices refused={refused} notices={plan.notices} />\n <CompaniesBulkBar\n hostId={hostId}\n scope={scope}\n rows={companies}\n selected={selectedIds}\n onSelectedChange={setSelectedIds}\n members={members}\n csv={csvOptions}\n />\n <CrmColumnOrderProvider value={grid.columnOrder}>\n <ListTable\n rowHeight={TABLE_ROW_HEIGHT}\n columns={grid.columns}\n slots={CRM_LIST_SLOTS}\n rows={companies}\n selectable={{ selected: selectedIds, onChange: setSelectedIds }}\n noRowsLabel={narrowed ? 'No companies match these filters' : 'No companies yet'}\n noRowsDescription={\n narrowed\n ? undefined\n : 'A company groups the contacts who work at one business, with its domain, owner and address. Create the first one, or link a contact to a company from their page.'\n }\n noRowsAction={narrowed ? undefined : newCompanyButton}\n onOpen={(id) => openCompany(String(id))}\n // An empty table while the read is in flight reads as \"you have\n // none\" rather than \"these are on their way\".\n loading={!scope || status === 'loading'}\n /*\n * The grid must NOT also filter. The query answers it, so a client\n * pass could only drop rows the query already matched (AGL-3321).\n */\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // Columns and sort are the view's, controlled (AGL-2617).\n columnVisibilityModel={grid.columnVisibilityModel}\n onColumnVisibilityModelChange={grid.onColumnVisibilityModelChange}\n sortModel={grid.sortModel}\n onSortModelChange={grid.onSortModelChange}\n // Paged by the footer below, so the grid must not also slice.\n hideFooter\n />\n </CrmColumnOrderProvider>\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={companies.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n {/*\n Mounted only while it is open, so the list pays for none of the\n drawer's state — and a fresh mount is what seeds an empty form.\n */}\n {createOpen ? (\n <CompanyEditDrawer\n open\n onClose={() => setCreateOpen(false)}\n hostId={hostId}\n org={org}\n members={members}\n onSaved={openCompany}\n />\n ) : null}\n </CardDisplay>\n )\n}\nCompaniesSection.displayName = 'CompaniesSection'\n\nexport default CompaniesSection\n"],"names":["CRM_COLLECTIONS","pluginDocsHelp","crmMemberPickerLabel","mdiPlus","CardDisplay","MdiIcon","ListPagination","ListTable","listFilterColumn","ListQueryNotices","listQueryRefusals","useCrmSavedView","useCrmViewGrid","CRM_LIST_SLOTS","CrmColumnOrderProvider","CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_NEXT_ACTIVITY_FILTER_HEADER","nextActivityColumn","useCrmFoldsScope","useCrmListQuery","ListFilterChips","listFilterGridColumns","useListGridFilter","CrmViewsControl","CrmListActions","CrmListToolbar","TABLE_ROW_HEIGHT","Button","Chip","Stack","Typography","getGridSingleSelectOperators","useRouter","useCallback","useEffect","useMemo","useState","COMPANY_LIST_DECLARATION","COMPANY_LIST_FILTER_FIELDS","COMPANY_PREFIX_SEARCH","useContactFieldDefinitions","useCrmScope","useOrgMemberOptions","companiesCsv","downloadTextFile","crmRoutes","CompaniesBulkBar","CompanyEditDrawer","CompanyImportButton","customFieldColumns","COMPANY_GRID_FILTER_FIELDS","COMPANY_GRID_FILTER_HEADERS","name","ownerUid","column","CompaniesSection","props","hostId","org","basePath","routes","router","scope","orgId","visibleTo","members","companyFields","views","section","viewFilters","state","filters","searchWords","setSearchWords","foldsScope","rows","companies","status","hasMore","page","setPage","pageSize","setPageSize","plan","collection","declaration","clauses","search","prefixSearch","nowMs","Date","now","createOpen","setCreateOpen","selectedIds","setSelectedIds","csvOptions","ownerEmail","emailFor","handleExport","openCompany","id","push","company","columns","field","headerName","flex","minWidth","valueGetter","_value","row","String","renderCell","sx","justifyContent","height","lineHeight","variant","industry","color","noWrap","filterable","sortable","domain","width","align","headerAlign","Number","contactsCount","toLocaleString","tags","join","length","direction","spacing","alignItems","overflow","slice","map","tag","size","label","type","valueOptions","options","option","value","uid","filterOperators","filter","operator","ready","labelFor","updatedAt","seconds","toLocaleDateString","active","ownerOptions","filterColumns","gridFilter","onChange","setFilters","selectFields","words","refused","fields","headers","grid","narrowed","some","word","trim","newCompanyButton","disabled","startIcon","path","onClick","header","help","anchor","contentGutterX","contentGutterY","contentBordered","HeaderProps","action","controller","allLabel","marksServed","notices","selected","onSelectedChange","csv","columnOrder","rowHeight","slots","selectable","noRowsLabel","noRowsDescription","undefined","noRowsAction","onOpen","loading","filterMode","filterModel","onFilterModelChange","quickFilter","columnVisibilityModel","onColumnVisibilityModelChange","sortModel","onSortModelChange","hideFooter","rowCount","onPageChange","onPageSizeChange","open","onClose","onSaved","displayName"],"mappings":"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"}
@@ -15,8 +15,9 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
18
19
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
- import { createResourceUid, CRM_COLLECTIONS, CRM_RECORDS_BAND_FULL_MESSAGE, pluginDocsHelp, crmMemberPickerLabel } from "@aglyn/aglyn";
20
+ import { createResourceUid, CRM_COLLECTIONS, CRM_RECORDS_BAND_FULL_MESSAGE, crmNewRecordListFields, pluginDocsHelp, crmMemberPickerLabel } from "@aglyn/aglyn";
20
21
  import { ICON_VARIANT_CLOSE } from "@aglyn/shared-data-enums";
21
22
  import { Container, HelpTip, MdiIcon, SrOnly } from "@aglyn/shared-ui-jsx";
22
23
  import { NavigationDrawerComponent } from "@aglyn/shared-ui-jsx/components/navigation-drawer.component";
@@ -33,6 +34,7 @@ import { companyDraftFields, companyDraftFrom, EMPTY_COMPANY_DRAFT } from "../mo
33
34
  import { crmCustomDraftDocument, crmCustomDraftMissingRequired, crmCustomDraftValue, crmCustomDraftWrites } from "../model/crm-custom-draft.js";
34
35
  import { CrmCustomFieldControl } from "./crm-custom-field-control.js";
35
36
  import { CrmSitePicker } from "./crm-site-picker.js";
37
+ import { CRM_CLIENT_SEARCH_FIELDS, crmClientListFields } from "../model/crm-list-query.js";
36
38
  const ADDRESS_FIELDS = [
37
39
  {
38
40
  key: 'line1',
@@ -163,10 +165,17 @@ const ADDRESS_FIELDS = [
163
165
  unreadable: (_ref = seed == null ? void 0 : seed.unreadable) != null ? _ref : false,
164
166
  fromCache: (_ref1 = seed == null ? void 0 : seed.fromCache) != null ? _ref1 : false
165
167
  }, async ()=>{
166
- await updateDoc(ref, _extends({}, result.set, Object.fromEntries(result.cleared.map((field)=>[
168
+ const cleared = Object.fromEntries(result.cleared.map((field)=>[
167
169
  field,
168
170
  deleteField()
169
- ])), crmCustomDraftWrites(storedCustom, custom), {
171
+ ]));
172
+ // The stored record the list fields are computed from: the row
173
+ // without the id the listener stamped on it.
174
+ const { $id: _rowId } = company, stored = _object_without_properties_loose(company, [
175
+ "$id"
176
+ ]);
177
+ const searchFields = crmClientListFields('companies', stored, _extends({}, result.set, cleared), CRM_CLIENT_SEARCH_FIELDS);
178
+ await updateDoc(ref, _extends({}, result.set, cleared, searchFields, crmCustomDraftWrites(storedCustom, custom), {
170
179
  updatedAt: serverTimestamp()
171
180
  }));
172
181
  });
@@ -185,7 +194,11 @@ const ADDRESS_FIELDS = [
185
194
  if (!createHostId) return;
186
195
  await setDoc(doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id), _extends({}, result.set, customDocument ? {
187
196
  custom: customDocument
188
- } : {}, {
197
+ } : {}, crmNewRecordListFields('companies', _extends({}, result.set, {
198
+ visibleTo: [
199
+ ...createTokens
200
+ ]
201
+ })), {
189
202
  /*
190
203
  * The scope every CRM creator stamps: the whole org when the org
191
204
  * shares by default, and otherwise the sites that present as one
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-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 type AglynOrgBilling,\n type AglynPostalAddress,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n type CrmCompany,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, 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 useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n deleteField,\n doc,\n serverTimestamp,\n setDoc,\n updateDoc,\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 { useCrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport {\n type CompanyDraft,\n companyDraftFields,\n companyDraftFrom,\n EMPTY_COMPANY_DRAFT,\n} from '../model/companies'\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'\n\nexport interface CompanyEditDrawerProps {\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 company asks which site captures it and\n * an edit keeps the company's own.\n */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n /**\n * The company being EDITED, or nothing to create one.\n *\n * The two modes differ in what the fields start as and in which write\n * runs: an edit opens on the stored values and updates the document, a\n * create opens empty and stamps the scope, provenance and timestamps a new\n * record must carry.\n */\n company?: (Partial<CrmCompany> & { $id: string }) | null\n /**\n * Whether the stored values the edit opened on can be trusted.\n *\n * Every optional field is written on every save — a blank one is a\n * DELETE — so a form seeded from a cached read would write the cache's\n * blanks over the server's values. The card that holds the listen reports\n * its own `fromCache` and `status`; the drawer refuses to save on a seed\n * the server never confirmed, and says so.\n */\n seed?: { fromCache: boolean; unreadable: boolean }\n /**\n * The team, for the owner picker.\n *\n * Handed in rather than fetched here, because the surface opening this\n * drawer already holds the roster to name owners in its rows, and one\n * fetch per surface is the bargain.\n */\n members: OrgMemberOptions\n /** Called with the record's id after a successful write. */\n onSaved: (companyId: string) => void\n}\n\nconst ADDRESS_FIELDS: ReadonlyArray<{\n key: keyof AglynPostalAddress\n label: string\n}> = [\n { key: 'line1', label: 'Address line 1' },\n { key: 'line2', label: 'Address line 2' },\n { key: 'city', label: 'City' },\n { key: 'state', label: 'State or region' },\n { key: 'postalCode', label: 'Postal code' },\n { key: 'country', label: 'Country (two-letter code)' },\n]\n\n/**\n * ONE COMPANY, created or edited (AGL-2597).\n *\n * A drawer rather than a form above the list, and the same drawer for both\n * jobs: a company is eight fields and an address, which is too much to wedge\n * over a table and exactly the same set whether the record exists yet. The\n * list opens it empty; the company's page opens it on the record.\n *\n * Nothing is written per keystroke. The draft is held here and turned into a\n * document by `companyDraftFields` on Save, which is where a domain typed as\n * `acme` or a phone number with no country code is refused — as a message\n * beside the button, with the typed values left where they are.\n */\nexport function CompanyEditDrawer(props: CompanyEditDrawerProps) {\n const { open, onClose, hostId, org, company, seed, members, onSaved } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { scope, createTokens, createHostId } = 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 record is STAMPED, which at the hub is\n // a 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\n const [draft, setDraft] = useState<CompanyDraft>(EMPTY_COMPANY_DRAFT)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's company definitions, and the\n * keys the reader has touched over what the company stores. Saved as the\n * difference — see `crm-custom-draft` — beside the fixed fields, in the\n * same write, so a company is never half-saved.\n */\n const fields = useContactFieldDefinitions(scope?.[1] ?? null, 'company')\n const storedCustom = useMemo(() => company?.custom ?? {}, [company?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n /*\n * THE RECORDS BAND (AGL-2611), read only for a CREATE: a company is a\n * record of the band the contacts list is banded by, and on an org at a\n * full hard band this drawer refuses the way `upsertHostContact` refuses a\n * capture — same number, same sentence. An edit is not a record and pays\n * for no aggregate. Only a SETTLED refusal refuses: three reads that have\n * not answered are not a full band.\n */\n const records = useCrmRecordsQuota(open && !company ? scope : null, org)\n const bandFull = !company && records.ready && !records.quota.allowed\n\n /*\n * Seeded when the drawer OPENS, and not on every render of the card\n * behind it: the company listen is live, so re-seeding on each snapshot\n * would take a field away from whoever is typing in it — including on the\n * snapshot their own save produces. The create case seeds the owner to the\n * person filing the record, which is the answer nine times in ten and one\n * click to change.\n */\n const companyId = company?.$id\n useEffect(() => {\n if (!open) return\n setDraft(\n company\n ? companyDraftFrom(company)\n : { ...EMPTY_COMPANY_DRAFT, ownerUid: user?.uid ?? '' },\n )\n setCustom({})\n setError('')\n // The stored fields are read once per opening; `company` is a new object\n // on every snapshot and must not re-seed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, companyId])\n\n const patch = useCallback(\n (field: keyof Omit<CompanyDraft, 'address'>, value: string) =>\n setDraft((current) => ({ ...current, [field]: value })),\n [],\n )\n const patchAddress = useCallback(\n (field: keyof AglynPostalAddress, value: string) =>\n setDraft((current) => ({\n ...current,\n address: { ...current.address, [field]: value },\n })),\n [],\n )\n\n const handleSave = useCallback(async () => {\n if (busy || !scope) return\n const result = companyDraftFields(draft)\n if (result.ok === false) return setError(result.error)\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n company ? 'edit' : 'create',\n )\n if (missing.length) return setError(`${missing.join(', ')} is required.`)\n const customDocument = crmCustomDraftDocument(custom)\n setError('')\n setBusy(true)\n try {\n if (company) {\n const ref = doc(\n firestore,\n scope[0],\n scope[1],\n CRM_COLLECTIONS.companies,\n company.$id,\n )\n // The guard WRAPS the write — an early return is a shape you can\n // keep while losing the protection.\n const verdict = await writeGuardedBySeed(\n {\n subject: 'company',\n unreadable: seed?.unreadable ?? false,\n fromCache: seed?.fromCache ?? false,\n },\n async () => {\n await updateDoc(ref, {\n ...result.set,\n // A blank optional field is a DELETE on an edit, or the old\n // domain would stay stored and keep matching people by email.\n ...Object.fromEntries(\n result.cleared.map((field) => [field, deleteField()]),\n ),\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n updatedAt: serverTimestamp(),\n })\n },\n )\n if (!verdict.ok) return setError(verdict.message)\n enqueueSnackbar('Company saved', { variant: 'success', persist: false })\n onSaved(company.$id)\n } else {\n if (bandFull) return setError(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\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 await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n ...(customDocument ? { custom: customDocument } : {}),\n /*\n * The scope every CRM creator stamps: the whole org when the org\n * shares by default, and otherwise the sites that present as one\n * sender. Without it the record is seen by NOBODY — an absent\n * `visibleTo` matches no reader's predicate — and the rules\n * refuse a scoped member creating outside their own tokens.\n */\n visibleTo: [...createTokens],\n // Provenance: the site whose console filed it, never rewritten.\n hostId: createHostId,\n createdByUid: user?.uid ?? '',\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed. Creation only — an edit is a save the card reports.\n logActivity('Added company', { type: 'company', id, name: draft.name.trim() })\n enqueueSnackbar(`Company \"${draft.name.trim()}\" created`, {\n variant: 'success',\n persist: false,\n })\n onSaved(id)\n }\n onClose()\n } catch (caught) {\n console.error(caught)\n setError('The company could not be saved.')\n } finally {\n setBusy(false)\n }\n }, [\n busy,\n scope,\n draft,\n company,\n bandFull,\n firestore,\n seed,\n createTokens,\n createHostId,\n user,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n fields.active,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {company ? 'Edit company' : 'New company'}\n </Typography>\n <HelpTip\n {...pluginDocsHelp('companies', { anchor: '#create-a-company' })}\n />\n </>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new company names the site that\n captures it. Nothing under a site, and nothing on an edit — a\n company keeps the site it was made on (AGL-2630).\n */}\n {company ? null : (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this company belongs to — it decides which of your sites may see it.\"\n />\n )}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The organization behind one or more of your contacts. The ' +\n 'domain is what suggests this company for a person from their ' +\n 'email address, so enter the bare hostname.'}\n </Typography>\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Name\"\n required\n value={draft.name}\n onChange={(event) => patch('name', event.target.value)}\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n placeholder=\"example.com\"\n value={draft.domain}\n onChange={(event) => patch('domain', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n placeholder=\"https://www.example.com\"\n value={draft.website}\n onChange={(event) => patch('website', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n placeholder=\"+1 512 555 0123\"\n value={draft.phone}\n onChange={(event) => patch('phone', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Industry\"\n value={draft.industry}\n onChange={(event) => patch('industry', event.target.value)}\n />\n {/*\n The owner is chosen from the team, never typed: the value is a\n uid, and a picker that is still loading says so rather than\n offering an empty list that reads as \"nobody can own this\".\n */}\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={draft.ownerUid}\n disabled={!members.ready}\n helperText={\n !members.ready\n ? 'Loading your team…'\n : (members.error ?? 'The team member responsible for this account.')\n }\n onChange={(event) => patch('ownerUid', event.target.value)}\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {draft.ownerUid &&\n !members.options.some((option) => option.uid === draft.ownerUid) ? (\n <MenuItem value={draft.ownerUid}>\n {members.labelFor(draft.ownerUid)}\n </MenuItem>\n ) : null}\n {members.options.map((option) => (\n <MenuItem key={option.uid} value={option.uid}>\n {crmMemberPickerLabel(option)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"enterprise, west\"\n helperText=\"Comma-separated, lowercased — the same tags the bulk bar adds\"\n value={draft.tags}\n onChange={(event) => patch('tags', event.target.value)}\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n {ADDRESS_FIELDS.map((field) => (\n <TextField\n key={field.key}\n size=\"small\"\n label={field.label}\n value={draft.address[field.key] ?? ''}\n onChange={(event) => patchAddress(field.key, event.target.value)}\n />\n ))}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={draft.notes}\n onChange={(event) => patch('notes', event.target.value)}\n multiline\n minRows={3}\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 {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // A new company waits for its capturing site.\n disabled={busy || !scope || !draft.name.trim() || (!company && !createHostId)}\n onClick={() => void handleSave()}\n >\n {busy\n ? 'Saving…'\n : company\n ? 'Save company'\n : 'Create company'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCompanyEditDrawer.displayName = 'CompanyEditDrawer'\n\nexport default CompanyEditDrawer\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","pluginDocsHelp","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Alert","Button","IconButton","MenuItem","Stack","TextField","Typography","deleteField","doc","serverTimestamp","setDoc","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","useCrmScope","companyDraftFields","companyDraftFrom","EMPTY_COMPANY_DRAFT","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","ADDRESS_FIELDS","key","label","CompanyEditDrawer","props","members","open","onClose","hostId","org","company","seed","onSaved","firestore","data","user","enqueueSnackbar","scope","createTokens","createHostId","logActivity","draft","setDraft","busy","setBusy","error","setError","fields","storedCustom","custom","setCustom","records","bandFull","ready","quota","allowed","companyId","$id","ownerUid","uid","patch","field","value","current","patchAddress","address","handleSave","result","ok","missing","active","length","join","customDocument","ref","companies","verdict","subject","unreadable","fromCache","set","Object","fromEntries","cleared","map","updatedAt","message","variant","persist","id","visibleTo","createdByUid","createdAt","type","name","trim","caught","console","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","gutterY","spacing","disabled","helperText","severity","size","required","onChange","event","target","autoFocus","placeholder","domain","website","phone","industry","select","options","some","option","labelFor","tags","notes","multiline","minRows","definition","direction","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-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 type AglynOrgBilling,\n type AglynPostalAddress,\n createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n type CrmCompany,\n crmNewRecordListFields,\n pluginDocsHelp,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { ICON_VARIANT_CLOSE } from '@aglyn/shared-data-enums'\nimport { Container, HelpTip, 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 useUser,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n Button,\n IconButton,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n deleteField,\n doc,\n serverTimestamp,\n setDoc,\n updateDoc,\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 { useCrmScope } from '../hooks/use-crm-scope'\nimport type { OrgMemberOptions } from '../hooks/use-org-member-options'\nimport {\n type CompanyDraft,\n companyDraftFields,\n companyDraftFrom,\n EMPTY_COMPANY_DRAFT,\n} from '../model/companies'\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'\n\nexport interface CompanyEditDrawerProps {\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 company asks which site captures it and\n * an edit keeps the company's own.\n */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n /**\n * The company being EDITED, or nothing to create one.\n *\n * The two modes differ in what the fields start as and in which write\n * runs: an edit opens on the stored values and updates the document, a\n * create opens empty and stamps the scope, provenance and timestamps a new\n * record must carry.\n */\n company?: (Partial<CrmCompany> & { $id: string }) | null\n /**\n * Whether the stored values the edit opened on can be trusted.\n *\n * Every optional field is written on every save — a blank one is a\n * DELETE — so a form seeded from a cached read would write the cache's\n * blanks over the server's values. The card that holds the listen reports\n * its own `fromCache` and `status`; the drawer refuses to save on a seed\n * the server never confirmed, and says so.\n */\n seed?: { fromCache: boolean; unreadable: boolean }\n /**\n * The team, for the owner picker.\n *\n * Handed in rather than fetched here, because the surface opening this\n * drawer already holds the roster to name owners in its rows, and one\n * fetch per surface is the bargain.\n */\n members: OrgMemberOptions\n /** Called with the record's id after a successful write. */\n onSaved: (companyId: string) => void\n}\n\nconst ADDRESS_FIELDS: ReadonlyArray<{\n key: keyof AglynPostalAddress\n label: string\n}> = [\n { key: 'line1', label: 'Address line 1' },\n { key: 'line2', label: 'Address line 2' },\n { key: 'city', label: 'City' },\n { key: 'state', label: 'State or region' },\n { key: 'postalCode', label: 'Postal code' },\n { key: 'country', label: 'Country (two-letter code)' },\n]\n\n/**\n * ONE COMPANY, created or edited (AGL-2597).\n *\n * A drawer rather than a form above the list, and the same drawer for both\n * jobs: a company is eight fields and an address, which is too much to wedge\n * over a table and exactly the same set whether the record exists yet. The\n * list opens it empty; the company's page opens it on the record.\n *\n * Nothing is written per keystroke. The draft is held here and turned into a\n * document by `companyDraftFields` on Save, which is where a domain typed as\n * `acme` or a phone number with no country code is refused — as a message\n * beside the button, with the typed values left where they are.\n */\nexport function CompanyEditDrawer(props: CompanyEditDrawerProps) {\n const { open, onClose, hostId, org, company, seed, members, onSaved } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { scope, createTokens, createHostId } = 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 record is STAMPED, which at the hub is\n // a 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\n const [draft, setDraft] = useState<CompanyDraft>(EMPTY_COMPANY_DRAFT)\n const [busy, setBusy] = useState(false)\n const [error, setError] = useState('')\n /*\n * THE CUSTOM FIELDS (AGL-2661): the org's company definitions, and the\n * keys the reader has touched over what the company stores. Saved as the\n * difference — see `crm-custom-draft` — beside the fixed fields, in the\n * same write, so a company is never half-saved.\n */\n const fields = useContactFieldDefinitions(scope?.[1] ?? null, 'company')\n const storedCustom = useMemo(() => company?.custom ?? {}, [company?.custom])\n const [custom, setCustom] = useState<CrmCustomDraft>({})\n /*\n * THE RECORDS BAND (AGL-2611), read only for a CREATE: a company is a\n * record of the band the contacts list is banded by, and on an org at a\n * full hard band this drawer refuses the way `upsertHostContact` refuses a\n * capture — same number, same sentence. An edit is not a record and pays\n * for no aggregate. Only a SETTLED refusal refuses: three reads that have\n * not answered are not a full band.\n */\n const records = useCrmRecordsQuota(open && !company ? scope : null, org)\n const bandFull = !company && records.ready && !records.quota.allowed\n\n /*\n * Seeded when the drawer OPENS, and not on every render of the card\n * behind it: the company listen is live, so re-seeding on each snapshot\n * would take a field away from whoever is typing in it — including on the\n * snapshot their own save produces. The create case seeds the owner to the\n * person filing the record, which is the answer nine times in ten and one\n * click to change.\n */\n const companyId = company?.$id\n useEffect(() => {\n if (!open) return\n setDraft(\n company\n ? companyDraftFrom(company)\n : { ...EMPTY_COMPANY_DRAFT, ownerUid: user?.uid ?? '' },\n )\n setCustom({})\n setError('')\n // The stored fields are read once per opening; `company` is a new object\n // on every snapshot and must not re-seed.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open, companyId])\n\n const patch = useCallback(\n (field: keyof Omit<CompanyDraft, 'address'>, value: string) =>\n setDraft((current) => ({ ...current, [field]: value })),\n [],\n )\n const patchAddress = useCallback(\n (field: keyof AglynPostalAddress, value: string) =>\n setDraft((current) => ({\n ...current,\n address: { ...current.address, [field]: value },\n })),\n [],\n )\n\n const handleSave = useCallback(async () => {\n if (busy || !scope) return\n const result = companyDraftFields(draft)\n if (result.ok === false) return setError(result.error)\n const missing = crmCustomDraftMissingRequired(\n fields.active,\n storedCustom,\n custom,\n company ? 'edit' : 'create',\n )\n if (missing.length) return setError(`${missing.join(', ')} is required.`)\n const customDocument = crmCustomDraftDocument(custom)\n setError('')\n setBusy(true)\n try {\n if (company) {\n const ref = doc(\n firestore,\n scope[0],\n scope[1],\n CRM_COLLECTIONS.companies,\n company.$id,\n )\n // The guard WRAPS the write — an early return is a shape you can\n // keep while losing the protection.\n const verdict = await writeGuardedBySeed(\n {\n subject: 'company',\n unreadable: seed?.unreadable ?? false,\n fromCache: seed?.fromCache ?? false,\n },\n async () => {\n const cleared = Object.fromEntries(\n result.cleared.map((field) => [field, deleteField()]),\n )\n // The stored record the list fields are computed from: the row\n // without the id the listener stamped on it.\n const { $id: _rowId, ...stored } = company\n const searchFields = crmClientListFields(\n 'companies',\n stored,\n { ...result.set, ...cleared },\n CRM_CLIENT_SEARCH_FIELDS,\n )\n await updateDoc(ref, {\n ...result.set,\n // A blank optional field is a DELETE on an edit, or the old\n // domain would stay stored and keep matching people by email.\n ...cleared,\n // The name and domain are what the Companies list searches\n // (AGL-3321), from the listener's row with the edit over it.\n ...searchFields,\n // The custom keys that changed, merged into the stored map.\n ...crmCustomDraftWrites(storedCustom, custom),\n updatedAt: serverTimestamp(),\n })\n },\n )\n if (!verdict.ok) return setError(verdict.message)\n enqueueSnackbar('Company saved', { variant: 'success', persist: false })\n onSaved(company.$id)\n } else {\n if (bandFull) return setError(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\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 await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n ...(customDocument ? { custom: customDocument } : {}),\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', {\n ...result.set,\n visibleTo: [...createTokens],\n }),\n /*\n * The scope every CRM creator stamps: the whole org when the org\n * shares by default, and otherwise the sites that present as one\n * sender. Without it the record is seen by NOBODY — an absent\n * `visibleTo` matches no reader's predicate — and the rules\n * refuse a scoped member creating outside their own tokens.\n */\n visibleTo: [...createTokens],\n // Provenance: the site whose console filed it, never rewritten.\n hostId: createHostId,\n createdByUid: user?.uid ?? '',\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n // Setup → Activity shows CRM work (AGL-2622): the company is org\n // data, but the act happened in this site's console and belongs in\n // its feed. Creation only — an edit is a save the card reports.\n logActivity('Added company', { type: 'company', id, name: draft.name.trim() })\n enqueueSnackbar(`Company \"${draft.name.trim()}\" created`, {\n variant: 'success',\n persist: false,\n })\n onSaved(id)\n }\n onClose()\n } catch (caught) {\n console.error(caught)\n setError('The company could not be saved.')\n } finally {\n setBusy(false)\n }\n }, [\n busy,\n scope,\n draft,\n company,\n bandFull,\n firestore,\n seed,\n createTokens,\n createHostId,\n user,\n logActivity,\n enqueueSnackbar,\n onSaved,\n onClose,\n fields.active,\n storedCustom,\n custom,\n ])\n\n return (\n <NavigationDrawerComponent\n open={open}\n anchor=\"right\"\n variant=\"temporary\"\n onClose={onClose}\n AppBarProps={{ color: 'surface' }}\n appBarLeft={\n <>\n <IconButton\n color=\"inherit\"\n edge=\"start\"\n onClick={onClose}\n sx={{ mr: 2 }}\n >\n <MdiIcon path={ICON_VARIANT_CLOSE.path} />\n <SrOnly>{'close drawer'}</SrOnly>\n </IconButton>\n <Typography variant=\"h6\" component=\"div\">\n {company ? 'Edit company' : 'New company'}\n </Typography>\n <HelpTip\n {...pluginDocsHelp('companies', { anchor: '#create-a-company' })}\n />\n </>\n }\n >\n <Container gutterY>\n <Stack spacing={2}>\n {/*\n First, because the scope of everything below follows from it: at\n the organization level a new company names the site that\n captures it. Nothing under a site, and nothing on an edit — a\n company keeps the site it was made on (AGL-2630).\n */}\n {company ? null : (\n <CrmSitePicker\n hostId={hostId}\n disabled={busy}\n helperText=\"The site this company belongs to — it decides which of your sites may see it.\"\n />\n )}\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The organization behind one or more of your contacts. The ' +\n 'domain is what suggests this company for a person from their ' +\n 'email address, so enter the bare hostname.'}\n </Typography>\n {bandFull ? (\n <Alert severity=\"warning\">{CRM_RECORDS_BAND_FULL_MESSAGE}</Alert>\n ) : null}\n <TextField\n size=\"small\"\n label=\"Name\"\n required\n value={draft.name}\n onChange={(event) => patch('name', event.target.value)}\n autoFocus\n />\n <TextField\n size=\"small\"\n label=\"Domain\"\n placeholder=\"example.com\"\n value={draft.domain}\n onChange={(event) => patch('domain', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Website\"\n placeholder=\"https://www.example.com\"\n value={draft.website}\n onChange={(event) => patch('website', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Phone\"\n placeholder=\"+1 512 555 0123\"\n value={draft.phone}\n onChange={(event) => patch('phone', event.target.value)}\n />\n <TextField\n size=\"small\"\n label=\"Industry\"\n value={draft.industry}\n onChange={(event) => patch('industry', event.target.value)}\n />\n {/*\n The owner is chosen from the team, never typed: the value is a\n uid, and a picker that is still loading says so rather than\n offering an empty list that reads as \"nobody can own this\".\n */}\n <TextField\n select\n size=\"small\"\n label=\"Owner\"\n value={draft.ownerUid}\n disabled={!members.ready}\n helperText={\n !members.ready\n ? 'Loading your team…'\n : (members.error ?? 'The team member responsible for this account.')\n }\n onChange={(event) => patch('ownerUid', event.target.value)}\n >\n <MenuItem value=\"\">{'Nobody yet'}</MenuItem>\n {draft.ownerUid &&\n !members.options.some((option) => option.uid === draft.ownerUid) ? (\n <MenuItem value={draft.ownerUid}>\n {members.labelFor(draft.ownerUid)}\n </MenuItem>\n ) : null}\n {members.options.map((option) => (\n <MenuItem key={option.uid} value={option.uid}>\n {crmMemberPickerLabel(option)}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n size=\"small\"\n label=\"Tags\"\n placeholder=\"enterprise, west\"\n helperText=\"Comma-separated, lowercased — the same tags the bulk bar adds\"\n value={draft.tags}\n onChange={(event) => patch('tags', event.target.value)}\n />\n <Typography variant=\"subtitle2\">{'Address'}</Typography>\n {ADDRESS_FIELDS.map((field) => (\n <TextField\n key={field.key}\n size=\"small\"\n label={field.label}\n value={draft.address[field.key] ?? ''}\n onChange={(event) => patchAddress(field.key, event.target.value)}\n />\n ))}\n <TextField\n size=\"small\"\n label=\"Notes\"\n value={draft.notes}\n onChange={(event) => patch('notes', event.target.value)}\n multiline\n minRows={3}\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 {error ? <Alert severity=\"warning\">{error}</Alert> : null}\n <Stack direction=\"row\" spacing={1}>\n <Button\n variant=\"contained\"\n color=\"primary\"\n // A new company waits for its capturing site.\n disabled={busy || !scope || !draft.name.trim() || (!company && !createHostId)}\n onClick={() => void handleSave()}\n >\n {busy\n ? 'Saving…'\n : company\n ? 'Save company'\n : 'Create company'}\n </Button>\n <Button onClick={onClose} disabled={busy}>\n {'Cancel'}\n </Button>\n </Stack>\n </Stack>\n </Container>\n </NavigationDrawerComponent>\n )\n}\nCompanyEditDrawer.displayName = 'CompanyEditDrawer'\n\nexport default CompanyEditDrawer\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","pluginDocsHelp","crmMemberPickerLabel","ICON_VARIANT_CLOSE","Container","HelpTip","MdiIcon","SrOnly","NavigationDrawerComponent","useSnackbar","useFirestore","useUser","writeGuardedBySeed","Alert","Button","IconButton","MenuItem","Stack","TextField","Typography","deleteField","doc","serverTimestamp","setDoc","updateDoc","useCallback","useEffect","useMemo","useState","useContactFieldDefinitions","useCrmActivityLogger","useCrmRecordsQuota","useCrmScope","companyDraftFields","companyDraftFrom","EMPTY_COMPANY_DRAFT","crmCustomDraftDocument","crmCustomDraftMissingRequired","crmCustomDraftValue","crmCustomDraftWrites","CrmCustomFieldControl","CrmSitePicker","CRM_CLIENT_SEARCH_FIELDS","crmClientListFields","ADDRESS_FIELDS","key","label","CompanyEditDrawer","props","members","open","onClose","hostId","org","company","seed","onSaved","firestore","data","user","enqueueSnackbar","scope","createTokens","createHostId","logActivity","draft","setDraft","busy","setBusy","error","setError","fields","storedCustom","custom","setCustom","records","bandFull","ready","quota","allowed","companyId","$id","ownerUid","uid","patch","field","value","current","patchAddress","address","handleSave","result","ok","missing","active","length","join","customDocument","ref","companies","verdict","subject","unreadable","fromCache","cleared","Object","fromEntries","map","_rowId","stored","searchFields","set","updatedAt","message","variant","persist","id","visibleTo","createdByUid","createdAt","type","name","trim","caught","console","anchor","AppBarProps","color","appBarLeft","edge","onClick","sx","mr","path","component","gutterY","spacing","disabled","helperText","severity","size","required","onChange","event","target","autoFocus","placeholder","domain","website","phone","industry","select","options","some","option","labelFor","tags","notes","multiline","minRows","definition","direction","displayName"],"mappings":"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"}
@@ -16,7 +16,7 @@
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
18
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
- import { createResourceUid, CRM_COLLECTIONS, CRM_RECORDS_BAND_FULL_MESSAGE } from "@aglyn/aglyn";
19
+ import { createResourceUid, CRM_COLLECTIONS, CRM_RECORDS_BAND_FULL_MESSAGE, crmNewRecordListFields } from "@aglyn/aglyn";
20
20
  import { useFirestore, useFirestoreCollection, useUser } from "@aglyn/tenant-feature-instance";
21
21
  import { Autocomplete, Button, createFilterOptions, Stack, TextField, Typography } from "@mui/material";
22
22
  import { collection, doc, limit, orderBy, query, serverTimestamp, setDoc } from "firebase/firestore";
@@ -120,7 +120,11 @@ export { suggestCompanyForEmail };
120
120
  const quota = await readCrmRecordsQuota(firestore, scope, org);
121
121
  if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE);
122
122
  const id = createResourceUid();
123
- await setDoc(doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id), _extends({}, result.set, {
123
+ await setDoc(doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id), _extends({}, result.set, crmNewRecordListFields('companies', _extends({}, result.set, {
124
+ visibleTo: [
125
+ ...createTokens
126
+ ]
127
+ })), {
124
128
  visibleTo: [
125
129
  ...createTokens
126
130
  ],
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-picker.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 createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n createFilterOptions,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { readCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport {\n type CompanyOption,\n companyDraftFields,\n EMPTY_COMPANY_DRAFT,\n suggestCompanyForEmail,\n} from '../model/companies'\n\nexport { suggestCompanyForEmail, type CompanyOption }\n\n/**\n * How many companies the picker lists.\n *\n * A ceiling and not a page: somebody choosing a company knows which one, and\n * a workspace past two hundred accounts finds it from the company list's own\n * search and links the person from there. Bounded because this read happens\n * on a contact's page, where the company list is a control and not the\n * subject.\n */\nexport const COMPANY_OPTIONS_LIMIT = 200\n\nexport interface CompanyOptions {\n options: CompanyOption[]\n /** The listen has answered at least once. */\n ready: boolean\n /** The ceiling cut the list, so a company can exist and not be offered. */\n truncated: boolean\n}\n\n/**\n * The companies this viewer may link a contact to, alphabetically\n * (AGL-2597).\n *\n * Read under the same `visibleTo` predicate the company list runs, because\n * the rules refuse a listen without it for a scoped member — and because a\n * picker offering a company the reader could not open would let them file a\n * person under a record they cannot see. Ordered by `nameLower`, the key\n * every company write stores, so the picker reads as a list of names rather\n * than as document-id order dressed up.\n *\n * `enabled` gates the listen: a contact's properties card has one picker and\n * a merchant who opened the record to read a note must not pay for the\n * company list.\n */\nexport function useCompanyOptions(props: {\n /** The site whose console is reading, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n enabled?: boolean\n}): CompanyOptions {\n const { hostId, org, enabled = true } = props\n const firestore = useFirestore()\n const { scope, visibleTo } = useCrmScope({ hostId, org })\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && scope\n ? query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_LIMIT + 1),\n )\n : null,\n [firestore, scope, visibleTo, enabled],\n { idField: '$id' },\n )\n const options = useMemo<CompanyOption[]>(\n () =>\n (data ?? []).slice(0, COMPANY_OPTIONS_LIMIT).map((row) => ({\n id: String(row['$id']),\n name: String(row['name'] ?? ''),\n domain: typeof row['domain'] === 'string' ? row['domain'] : null,\n })),\n [data],\n )\n return {\n options,\n ready: Boolean(scope) && status !== 'loading',\n truncated: (data?.length ?? 0) > COMPANY_OPTIONS_LIMIT,\n }\n}\n\n/**\n * Create a company by name, answering the option the picker then selects.\n * Rejects with the sentence to show when the name cannot be stored.\n */\nexport type CreateCompany = (name: string) => Promise<CompanyOption>\n\n/**\n * The write behind \"Create “Acme”\" in the picker (AGL-2613).\n *\n * The same document the company drawer writes for a create — the search\n * keys, the scope every CRM creator stamps, the provenance — with only the\n * name filled in, because that is all the picker asked for: the domain, the\n * address and the rest are the company page's to add. The person filing it\n * owns it, as the drawer seeds too. `null` until the org has resolved, so a\n * picker offered before the scope is known offers no create.\n *\n * A company is a record of the band the drawers refuse on (AGL-2644): an\n * org at a full hard band is refused here with the drawers' sentence, and\n * the picker shows it beneath the field the way it shows any create that\n * failed. Measured on the click and not on the mount — see\n * `readCrmRecordsQuota` — and never for an edit, because linking an\n * existing company adds no record.\n */\nexport function useCreateCompany(props: {\n /** The site whose console is creating, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}): CreateCompany | null {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n // The provenance and the scope both come from the site the record is\n // captured by — the mounted one, or at the organization level the one the\n // reader picked (AGL-2630). No site picked, no create offered.\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n const uid = user?.uid ?? ''\n return useMemo<CreateCompany | null>(() => {\n if (!scope || !createHostId) return null\n return async (name) => {\n const result = companyDraftFields({\n ...EMPTY_COMPANY_DRAFT,\n name,\n ownerUid: uid,\n })\n if (result.ok === false) throw new Error(result.error)\n const quota = await readCrmRecordsQuota(firestore, scope, org)\n if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n visibleTo: [...createTokens],\n hostId: createHostId,\n createdByUid: uid,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n return { id, name: String(result.set['name']), domain: null }\n }\n }, [scope, createTokens, firestore, createHostId, uid, org])\n}\n\n/** The sentinel id of the \"Create …\" row the list grows when nothing matches. */\nconst CREATE_OPTION_ID = '__create__'\n\ntype PickerOption = CompanyOption | { id: typeof CREATE_OPTION_ID; name: string; create: string }\n\nconst isCreateOption = (\n option: PickerOption,\n): option is Extract<PickerOption, { create: string }> => 'create' in option\n\nconst filterCompanies = createFilterOptions<PickerOption>({\n stringify: (option) =>\n isCreateOption(option)\n ? option.create\n : option.domain\n ? `${option.name} ${option.domain}`\n : option.name,\n})\n\nconst optionLabel = (option: PickerOption): string =>\n isCreateOption(option)\n ? `Create “${option.create}”`\n : option.domain\n ? `${option.name} · ${option.domain}`\n : option.name\n\nexport interface CompanyPickerProps {\n options: readonly CompanyOption[]\n /** The linked company's id, or `null` for none. */\n value: string | null\n /**\n * The choice, with the option behind it when the list — or a create — can\n * name it, so a caller can echo the company's name onto the contact\n * without a second lookup. `null` for a company the list does not carry.\n */\n onChange: (companyId: string | null, company: CompanyOption | null) => void\n /**\n * Create a company from the typed name and select it. Absent — or `null`\n * while the scope is unresolved — the picker offers no create row.\n */\n onCreate?: CreateCompany | null\n label?: string\n helperText?: string\n disabled?: boolean\n /**\n * The contact's email address, when the caller has one. A company whose\n * domain matches it is offered as a suggestion beneath the field — a\n * suggestion and never an automatic link, because the person who typed\n * `@acme.com` may be a contractor, a customer of Acme's, or Acme itself.\n */\n email?: string | null\n /**\n * The company name the record carries as free text while no company is\n * linked — an import, or a save from before the picker existed. Offered\n * beneath the field as the company to link or create, so the label a\n * merchant already typed is one click from becoming the record it names.\n */\n fallbackName?: string | null\n /** The option list has answered; until then the field says it is loading. */\n ready?: boolean\n /** The list was cut at the ceiling, so the field says to narrow by typing. */\n truncated?: boolean\n}\n\n/**\n * One company, chosen from the list a contact may be filed under\n * (AGL-2597, AGL-2613).\n *\n * A search over the loaded list rather than free text, because the value is\n * a document id and a typed name is how two records called \"Acme\" come to\n * exist. Typing narrows by name and domain; a name nothing matches grows the\n * list by one row, \"Create “…”\", which writes the company and selects it —\n * so the person filing a contact never has to leave the form to make the\n * account it belongs to. The clear control is the unlink: a saved contact\n * with no company reads as a decision rather than a field somebody skipped.\n *\n * A stored id the list does not carry — a company past the picker's ceiling,\n * or one this viewer cannot see — still renders as SOMETHING selected, or\n * the field would read empty for a contact that has a company and the next\n * save would silently unlink them.\n */\nexport function CompanyPicker(props: CompanyPickerProps) {\n const {\n options,\n value,\n onChange,\n onCreate,\n label = 'Company',\n helperText,\n disabled,\n email,\n fallbackName,\n ready = true,\n truncated,\n } = props\n const [creating, setCreating] = useState(false)\n const [createError, setCreateError] = useState('')\n\n const suggestion = useMemo(\n () => (email ? suggestCompanyForEmail(email, options) : null),\n [email, options],\n )\n const fallback = useMemo(() => {\n const name = String(fallbackName ?? '').trim()\n if (value || !name) return null\n const lower = name.toLowerCase()\n return {\n name,\n match: options.find((option) => option.name.trim().toLowerCase() === lower) ?? null,\n }\n }, [fallbackName, options, value])\n\n const selected = useMemo<PickerOption | null>(() => {\n if (!value) return null\n return (\n options.find((option) => option.id === value) ?? {\n id: value,\n name: `Company ${value.slice(-6)}`,\n domain: null,\n }\n )\n }, [options, value])\n /*\n * The list the control searches: every option, plus the placeholder for a\n * stored id the list does not carry, so the selected value is always one\n * of the options and the control never reports it as invalid.\n */\n const listed = useMemo<PickerOption[]>(\n () =>\n selected && !options.some((option) => option.id === selected.id)\n ? [selected, ...options]\n : [...options],\n [options, selected],\n )\n\n const create = useCallback(\n async (name: string) => {\n if (!onCreate) return\n setCreating(true)\n setCreateError('')\n try {\n const company = await onCreate(name)\n onChange(company.id, company)\n } catch (error) {\n setCreateError(\n error instanceof Error && error.message\n ? error.message\n : 'The company could not be created.',\n )\n } finally {\n setCreating(false)\n }\n },\n [onCreate, onChange],\n )\n\n const busy = disabled || !ready || creating\n return (\n <Stack spacing={0.5}>\n <Autocomplete<PickerOption, false, false, false>\n size=\"small\"\n options={listed}\n value={selected}\n disabled={busy}\n loading={!ready}\n getOptionLabel={optionLabel}\n isOptionEqualToValue={(option, current) => option.id === current.id}\n filterOptions={(candidates, state) => {\n const filtered = filterCompanies(candidates, state)\n const typed = state.inputValue.trim()\n const exact = typed.toLowerCase()\n const named = candidates.some(\n (option) =>\n !isCreateOption(option) && option.name.trim().toLowerCase() === exact,\n )\n if (onCreate && typed && !named) {\n filtered.push({ id: CREATE_OPTION_ID, name: typed, create: typed })\n }\n return filtered\n }}\n onChange={(_event, next) => {\n if (!next) return onChange(null, null)\n if (isCreateOption(next)) return void create(next.create)\n onChange(next.id, next)\n }}\n renderInput={(params) => (\n <TextField\n {...params}\n label={label}\n error={Boolean(createError)}\n helperText={\n !ready\n ? 'Loading companies…'\n : creating\n ? 'Creating the company…'\n : createError ||\n helperText ||\n (truncated\n ? `The first ${COMPANY_OPTIONS_LIMIT} by name — type to narrow, or create one.`\n : undefined)\n }\n />\n )}\n />\n {suggestion && suggestion.id !== value ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Suggested from the email address: ${suggestion.name}`}\n </Typography>\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(suggestion.id, suggestion)}\n >\n {'Use'}\n </Button>\n </Stack>\n ) : fallback ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {fallback.match\n ? `Recorded as “${fallback.name}” — a company by that name exists.`\n : `Recorded as “${fallback.name}” without a link.`}\n </Typography>\n {fallback.match ? (\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(fallback.match?.id ?? null, fallback.match)}\n >\n {'Use'}\n </Button>\n ) : onCreate ? (\n <Button size=\"small\" disabled={busy} onClick={() => void create(fallback.name)}>\n {`Create “${fallback.name}”`}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nCompanyPicker.displayName = 'CompanyPicker'\n\nexport default CompanyPicker\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","useFirestore","useFirestoreCollection","useUser","Autocomplete","Button","createFilterOptions","Stack","TextField","Typography","collection","doc","limit","orderBy","query","serverTimestamp","setDoc","useCallback","useMemo","useState","readCrmRecordsQuota","crmVisibleToClause","useCrmScope","companyDraftFields","EMPTY_COMPANY_DRAFT","suggestCompanyForEmail","COMPANY_OPTIONS_LIMIT","useCompanyOptions","props","hostId","org","enabled","firestore","scope","visibleTo","data","status","companies","idField","options","slice","map","row","id","String","name","domain","ready","Boolean","truncated","length","useCreateCompany","user","createTokens","createHostId","uid","result","ownerUid","ok","Error","error","quota","allowed","set","createdByUid","createdAt","updatedAt","CREATE_OPTION_ID","isCreateOption","option","filterCompanies","stringify","create","optionLabel","CompanyPicker","value","onChange","onCreate","label","helperText","disabled","email","fallbackName","creating","setCreating","createError","setCreateError","suggestion","fallback","trim","lower","toLowerCase","match","find","selected","listed","some","company","message","busy","spacing","size","loading","getOptionLabel","isOptionEqualToValue","current","filterOptions","candidates","state","filtered","typed","inputValue","exact","named","push","_event","next","renderInput","params","undefined","direction","sx","alignItems","variant","color","onClick","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/company-picker.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 createResourceUid,\n CRM_COLLECTIONS,\n CRM_RECORDS_BAND_FULL_MESSAGE,\n crmNewRecordListFields,\n} from '@aglyn/aglyn'\nimport {\n useFirestore,\n useFirestoreCollection,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Autocomplete,\n Button,\n createFilterOptions,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n doc,\n limit,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useMemo, useState } from 'react'\nimport { readCrmRecordsQuota } from '../hooks/use-crm-records-quota'\nimport { crmVisibleToClause, useCrmScope } from '../hooks/use-crm-scope'\nimport {\n type CompanyOption,\n companyDraftFields,\n EMPTY_COMPANY_DRAFT,\n suggestCompanyForEmail,\n} from '../model/companies'\n\nexport { suggestCompanyForEmail, type CompanyOption }\n\n/**\n * How many companies the picker lists.\n *\n * A ceiling and not a page: somebody choosing a company knows which one, and\n * a workspace past two hundred accounts finds it from the company list's own\n * search and links the person from there. Bounded because this read happens\n * on a contact's page, where the company list is a control and not the\n * subject.\n */\nexport const COMPANY_OPTIONS_LIMIT = 200\n\nexport interface CompanyOptions {\n options: CompanyOption[]\n /** The listen has answered at least once. */\n ready: boolean\n /** The ceiling cut the list, so a company can exist and not be offered. */\n truncated: boolean\n}\n\n/**\n * The companies this viewer may link a contact to, alphabetically\n * (AGL-2597).\n *\n * Read under the same `visibleTo` predicate the company list runs, because\n * the rules refuse a listen without it for a scoped member — and because a\n * picker offering a company the reader could not open would let them file a\n * person under a record they cannot see. Ordered by `nameLower`, the key\n * every company write stores, so the picker reads as a list of names rather\n * than as document-id order dressed up.\n *\n * `enabled` gates the listen: a contact's properties card has one picker and\n * a merchant who opened the record to read a note must not pay for the\n * company list.\n */\nexport function useCompanyOptions(props: {\n /** The site whose console is reading, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n enabled?: boolean\n}): CompanyOptions {\n const { hostId, org, enabled = true } = props\n const firestore = useFirestore()\n const { scope, visibleTo } = useCrmScope({ hostId, org })\n const { data, status } = useFirestoreCollection<Record<string, unknown>>(\n () =>\n enabled && scope\n ? query(\n collection(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies),\n ...crmVisibleToClause(visibleTo),\n orderBy('nameLower'),\n limit(COMPANY_OPTIONS_LIMIT + 1),\n )\n : null,\n [firestore, scope, visibleTo, enabled],\n { idField: '$id' },\n )\n const options = useMemo<CompanyOption[]>(\n () =>\n (data ?? []).slice(0, COMPANY_OPTIONS_LIMIT).map((row) => ({\n id: String(row['$id']),\n name: String(row['name'] ?? ''),\n domain: typeof row['domain'] === 'string' ? row['domain'] : null,\n })),\n [data],\n )\n return {\n options,\n ready: Boolean(scope) && status !== 'loading',\n truncated: (data?.length ?? 0) > COMPANY_OPTIONS_LIMIT,\n }\n}\n\n/**\n * Create a company by name, answering the option the picker then selects.\n * Rejects with the sentence to show when the name cannot be stored.\n */\nexport type CreateCompany = (name: string) => Promise<CompanyOption>\n\n/**\n * The write behind \"Create “Acme”\" in the picker (AGL-2613).\n *\n * The same document the company drawer writes for a create — the search\n * keys, the scope every CRM creator stamps, the provenance — with only the\n * name filled in, because that is all the picker asked for: the domain, the\n * address and the rest are the company page's to add. The person filing it\n * owns it, as the drawer seeds too. `null` until the org has resolved, so a\n * picker offered before the scope is known offers no create.\n *\n * A company is a record of the band the drawers refuse on (AGL-2644): an\n * org at a full hard band is refused here with the drawers' sentence, and\n * the picker shows it beneath the field the way it shows any create that\n * failed. Measured on the click and not on the mount — see\n * `readCrmRecordsQuota` — and never for an edit, because linking an\n * existing company adds no record.\n */\nexport function useCreateCompany(props: {\n /** The site whose console is creating, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}): CreateCompany | null {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n // The provenance and the scope both come from the site the record is\n // captured by — the mounted one, or at the organization level the one the\n // reader picked (AGL-2630). No site picked, no create offered.\n const { scope, createTokens, createHostId } = useCrmScope({ hostId, org })\n const uid = user?.uid ?? ''\n return useMemo<CreateCompany | null>(() => {\n if (!scope || !createHostId) return null\n return async (name) => {\n const result = companyDraftFields({\n ...EMPTY_COMPANY_DRAFT,\n name,\n ownerUid: uid,\n })\n if (result.ok === false) throw new Error(result.error)\n const quota = await readCrmRecordsQuota(firestore, scope, org)\n if (!quota.allowed) throw new Error(CRM_RECORDS_BAND_FULL_MESSAGE)\n const id = createResourceUid()\n await setDoc(\n doc(firestore, scope[0], scope[1], CRM_COLLECTIONS.companies, id),\n {\n ...result.set,\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', { ...result.set, visibleTo: [...createTokens] }),\n visibleTo: [...createTokens],\n hostId: createHostId,\n createdByUid: uid,\n createdAt: serverTimestamp(),\n updatedAt: serverTimestamp(),\n },\n )\n return { id, name: String(result.set['name']), domain: null }\n }\n }, [scope, createTokens, firestore, createHostId, uid, org])\n}\n\n/** The sentinel id of the \"Create …\" row the list grows when nothing matches. */\nconst CREATE_OPTION_ID = '__create__'\n\ntype PickerOption = CompanyOption | { id: typeof CREATE_OPTION_ID; name: string; create: string }\n\nconst isCreateOption = (\n option: PickerOption,\n): option is Extract<PickerOption, { create: string }> => 'create' in option\n\nconst filterCompanies = createFilterOptions<PickerOption>({\n stringify: (option) =>\n isCreateOption(option)\n ? option.create\n : option.domain\n ? `${option.name} ${option.domain}`\n : option.name,\n})\n\nconst optionLabel = (option: PickerOption): string =>\n isCreateOption(option)\n ? `Create “${option.create}”`\n : option.domain\n ? `${option.name} · ${option.domain}`\n : option.name\n\nexport interface CompanyPickerProps {\n options: readonly CompanyOption[]\n /** The linked company's id, or `null` for none. */\n value: string | null\n /**\n * The choice, with the option behind it when the list — or a create — can\n * name it, so a caller can echo the company's name onto the contact\n * without a second lookup. `null` for a company the list does not carry.\n */\n onChange: (companyId: string | null, company: CompanyOption | null) => void\n /**\n * Create a company from the typed name and select it. Absent — or `null`\n * while the scope is unresolved — the picker offers no create row.\n */\n onCreate?: CreateCompany | null\n label?: string\n helperText?: string\n disabled?: boolean\n /**\n * The contact's email address, when the caller has one. A company whose\n * domain matches it is offered as a suggestion beneath the field — a\n * suggestion and never an automatic link, because the person who typed\n * `@acme.com` may be a contractor, a customer of Acme's, or Acme itself.\n */\n email?: string | null\n /**\n * The company name the record carries as free text while no company is\n * linked — an import, or a save from before the picker existed. Offered\n * beneath the field as the company to link or create, so the label a\n * merchant already typed is one click from becoming the record it names.\n */\n fallbackName?: string | null\n /** The option list has answered; until then the field says it is loading. */\n ready?: boolean\n /** The list was cut at the ceiling, so the field says to narrow by typing. */\n truncated?: boolean\n}\n\n/**\n * One company, chosen from the list a contact may be filed under\n * (AGL-2597, AGL-2613).\n *\n * A search over the loaded list rather than free text, because the value is\n * a document id and a typed name is how two records called \"Acme\" come to\n * exist. Typing narrows by name and domain; a name nothing matches grows the\n * list by one row, \"Create “…”\", which writes the company and selects it —\n * so the person filing a contact never has to leave the form to make the\n * account it belongs to. The clear control is the unlink: a saved contact\n * with no company reads as a decision rather than a field somebody skipped.\n *\n * A stored id the list does not carry — a company past the picker's ceiling,\n * or one this viewer cannot see — still renders as SOMETHING selected, or\n * the field would read empty for a contact that has a company and the next\n * save would silently unlink them.\n */\nexport function CompanyPicker(props: CompanyPickerProps) {\n const {\n options,\n value,\n onChange,\n onCreate,\n label = 'Company',\n helperText,\n disabled,\n email,\n fallbackName,\n ready = true,\n truncated,\n } = props\n const [creating, setCreating] = useState(false)\n const [createError, setCreateError] = useState('')\n\n const suggestion = useMemo(\n () => (email ? suggestCompanyForEmail(email, options) : null),\n [email, options],\n )\n const fallback = useMemo(() => {\n const name = String(fallbackName ?? '').trim()\n if (value || !name) return null\n const lower = name.toLowerCase()\n return {\n name,\n match: options.find((option) => option.name.trim().toLowerCase() === lower) ?? null,\n }\n }, [fallbackName, options, value])\n\n const selected = useMemo<PickerOption | null>(() => {\n if (!value) return null\n return (\n options.find((option) => option.id === value) ?? {\n id: value,\n name: `Company ${value.slice(-6)}`,\n domain: null,\n }\n )\n }, [options, value])\n /*\n * The list the control searches: every option, plus the placeholder for a\n * stored id the list does not carry, so the selected value is always one\n * of the options and the control never reports it as invalid.\n */\n const listed = useMemo<PickerOption[]>(\n () =>\n selected && !options.some((option) => option.id === selected.id)\n ? [selected, ...options]\n : [...options],\n [options, selected],\n )\n\n const create = useCallback(\n async (name: string) => {\n if (!onCreate) return\n setCreating(true)\n setCreateError('')\n try {\n const company = await onCreate(name)\n onChange(company.id, company)\n } catch (error) {\n setCreateError(\n error instanceof Error && error.message\n ? error.message\n : 'The company could not be created.',\n )\n } finally {\n setCreating(false)\n }\n },\n [onCreate, onChange],\n )\n\n const busy = disabled || !ready || creating\n return (\n <Stack spacing={0.5}>\n <Autocomplete<PickerOption, false, false, false>\n size=\"small\"\n options={listed}\n value={selected}\n disabled={busy}\n loading={!ready}\n getOptionLabel={optionLabel}\n isOptionEqualToValue={(option, current) => option.id === current.id}\n filterOptions={(candidates, state) => {\n const filtered = filterCompanies(candidates, state)\n const typed = state.inputValue.trim()\n const exact = typed.toLowerCase()\n const named = candidates.some(\n (option) =>\n !isCreateOption(option) && option.name.trim().toLowerCase() === exact,\n )\n if (onCreate && typed && !named) {\n filtered.push({ id: CREATE_OPTION_ID, name: typed, create: typed })\n }\n return filtered\n }}\n onChange={(_event, next) => {\n if (!next) return onChange(null, null)\n if (isCreateOption(next)) return void create(next.create)\n onChange(next.id, next)\n }}\n renderInput={(params) => (\n <TextField\n {...params}\n label={label}\n error={Boolean(createError)}\n helperText={\n !ready\n ? 'Loading companies…'\n : creating\n ? 'Creating the company…'\n : createError ||\n helperText ||\n (truncated\n ? `The first ${COMPANY_OPTIONS_LIMIT} by name — type to narrow, or create one.`\n : undefined)\n }\n />\n )}\n />\n {suggestion && suggestion.id !== value ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Suggested from the email address: ${suggestion.name}`}\n </Typography>\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(suggestion.id, suggestion)}\n >\n {'Use'}\n </Button>\n </Stack>\n ) : fallback ? (\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {fallback.match\n ? `Recorded as “${fallback.name}” — a company by that name exists.`\n : `Recorded as “${fallback.name}” without a link.`}\n </Typography>\n {fallback.match ? (\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => onChange(fallback.match?.id ?? null, fallback.match)}\n >\n {'Use'}\n </Button>\n ) : onCreate ? (\n <Button size=\"small\" disabled={busy} onClick={() => void create(fallback.name)}>\n {`Create “${fallback.name}”`}\n </Button>\n ) : null}\n </Stack>\n ) : null}\n </Stack>\n )\n}\nCompanyPicker.displayName = 'CompanyPicker'\n\nexport default CompanyPicker\n"],"names":["createResourceUid","CRM_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","crmNewRecordListFields","useFirestore","useFirestoreCollection","useUser","Autocomplete","Button","createFilterOptions","Stack","TextField","Typography","collection","doc","limit","orderBy","query","serverTimestamp","setDoc","useCallback","useMemo","useState","readCrmRecordsQuota","crmVisibleToClause","useCrmScope","companyDraftFields","EMPTY_COMPANY_DRAFT","suggestCompanyForEmail","COMPANY_OPTIONS_LIMIT","useCompanyOptions","props","hostId","org","enabled","firestore","scope","visibleTo","data","status","companies","idField","options","slice","map","row","id","String","name","domain","ready","Boolean","truncated","length","useCreateCompany","user","createTokens","createHostId","uid","result","ownerUid","ok","Error","error","quota","allowed","set","createdByUid","createdAt","updatedAt","CREATE_OPTION_ID","isCreateOption","option","filterCompanies","stringify","create","optionLabel","CompanyPicker","value","onChange","onCreate","label","helperText","disabled","email","fallbackName","creating","setCreating","createError","setCreateError","suggestion","fallback","trim","lower","toLowerCase","match","find","selected","listed","some","company","message","busy","spacing","size","loading","getOptionLabel","isOptionEqualToValue","current","filterOptions","candidates","state","filtered","typed","inputValue","exact","named","push","_event","next","renderInput","params","undefined","direction","sx","alignItems","variant","color","onClick","displayName"],"mappings":"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"}