@aglyn/plugins-crm 1.0.0-beta.230 → 1.0.0-beta.232
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.
- package/package.json +14 -13
- package/src/lib/components/activity-timeline.js +1 -0
- package/src/lib/components/activity-timeline.js.map +1 -1
- package/src/lib/components/assignment-rule-drawer.js +1 -0
- package/src/lib/components/assignment-rule-drawer.js.map +1 -1
- package/src/lib/components/contact-associations-card.js +1 -0
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-duplicates-card.js +1 -0
- package/src/lib/components/contact-duplicates-card.js.map +1 -1
- package/src/lib/components/contact-field-drawer.js +1 -0
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contact-known-by-card.js +1 -0
- package/src/lib/components/contact-known-by-card.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +1 -0
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +1 -0
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.d.ts +1 -13
- package/src/lib/components/crm-next-activity-column.js +1 -19
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/crm-record-header.js +2 -0
- package/src/lib/components/crm-record-header.js.map +1 -1
- package/src/lib/components/deals-section.js +1 -0
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -0
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-history-card.js +1 -0
- package/src/lib/components/lead-history-card.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +2 -14
- package/src/lib/components/lead-status-options.js +1 -13
- package/src/lib/components/lead-status-options.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +1 -0
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.d.ts +1 -2
- package/src/lib/components/lead-unqualify-dialog.js +2 -1
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/linked-deals-card.js +1 -0
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/picklist-values-card.js +1 -0
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/components/recipes-card.js +1 -0
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/reports/activity-card.js +1 -0
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +1 -0
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +1 -0
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +1 -0
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.js +1 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +1 -0
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +1 -0
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +1 -0
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports-section.js +2 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/settings-section.d.ts +18 -0
- package/src/lib/components/settings-section.js +97 -1
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/tasks-calendar.js +1 -0
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +2 -2
- package/src/lib/constants/company-filters.js +1 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +3 -3
- package/src/lib/constants/contact-filters.js +2 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +3 -3
- package/src/lib/constants/deal-filters.js +1 -1
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/constants/field-list-query.d.ts +1 -1
- package/src/lib/constants/field-list-query.js +1 -1
- package/src/lib/constants/field-list-query.js.map +1 -1
- package/src/lib/declarations.server.js +9 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/contacts-list-seed.d.ts +1 -1
- package/src/lib/model/contacts-list-seed.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +4 -4
- package/src/lib/model/crm-list-query.js +2 -2
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +13 -0
- package/src/lib/model/crm-next-activity.js +19 -0
- package/src/lib/model/crm-next-activity.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +6 -5
- package/src/lib/model/lead-filters.js +3 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-status-choices.d.ts +32 -0
- package/src/lib/model/lead-status-choices.js +31 -0
- package/src/lib/model/lead-status-choices.js.map +1 -0
- package/src/lib/model/task-views.d.ts +2 -2
- package/src/lib/model/task-views.js.map +1 -1
- package/src/lib/server/person-records.d.ts +27 -0
- package/src/lib/server/person-records.js +127 -4
- package/src/lib/server/person-records.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/settings-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 canManageOrg,\n type ConsolePluginPageProps,\n CRM_ASSIGNMENT_RULES_MAX,\n CRM_ASSIGNMENT_RULES_PATH,\n CRM_AUTO_CREATE_COMPANIES_PATH,\n CRM_ROUND_ROBIN_POOL_MAX,\n CRM_ROUND_ROBIN_POOL_PATH,\n type CrmAssignmentRule,\n crmHostDefaultOwnerSegments,\n describeAssignmentRule,\n orgAutoCreatesCompanies,\n type OrgRole,\n pluginDocsHelp,\n readCrmAssignmentSettings,\n roundRobinOrder,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiArrowDown, mdiArrowUp, mdiDeleteOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n FormControlLabel,\n FormGroup,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, FieldPath, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport AssignmentRuleDrawer from './assignment-rule-drawer'\nimport EmailTemplatesCard from './email-templates-card'\nimport EmailCaptureCard from './email-capture-card'\nimport RecipesCard from './recipes-card'\nimport SendingAddressesCard from './sending-addresses-card'\nimport SharingRulesCard from './sharing-rules-card'\n\nexport type CrmSettingsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/**\n * Whether the signed-in member may change an org-wide CRM setting, and\n * whether that is known yet.\n *\n * The org document's client branch admits an OWNER or ADMIN and nobody\n * else — `canManageOrg()` in the rules — so the question is the caller's\n * org role, read off their own membership document, which the rules let a\n * member read for themselves. A scoped collaborator, an editor, a viewer:\n * each sees the switch and cannot move it, with the reason beside it,\n * rather than a switch that moves and snaps back with a bare\n * `permission-denied`. `ready` separates \"no\" from \"not yet\", so the\n * control disables with a reason instead of hiding until the read lands.\n */\nexport function useCanManageCrmSettings(orgId: string | undefined): {\n canManage: boolean\n ready: boolean\n} {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = user?.uid ?? ''\n const { data: member, status } = useFirestoreDoc<{ role?: OrgRole }>(\n () => (orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null),\n [firestore, orgId, uid],\n )\n return {\n canManage: canManageOrg(member?.role ?? null),\n ready: Boolean(orgId && uid) && status !== 'loading',\n }\n}\n\nexport interface AutoCreateCompaniesCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Create companies from work email domains\" — the org's one switch over\n * what a capture does with a company nobody has filed yet (AGL-2613).\n *\n * ## What the switch decides, and what it does not\n *\n * A contact captured from `jane@acme.com` is linked to the company at\n * `acme.com` whether this is on or off, provided exactly one such company\n * is visible to the capturing site. The switch decides the case where NO\n * company carries the domain: on, the capture creates one named after the\n * domain and links the contact; off — the default — it creates nothing and\n * the contact waits for a person to file them. Public mailbox domains never\n * create a company either way; a workspace's consumer list is not a list\n * of accounts.\n *\n * ## Written where it is read\n *\n * One dotted-path `update()` onto `orgs/{orgId}`, so the org's other keys\n * are untouched and the map under `crm` can grow a key per setting. The\n * shell's org listener delivers the new value back, which is what the\n * switch reflects; a local copy holds the click only until then.\n */\nexport function AutoCreateCompaniesCard(props: AutoCreateCompaniesCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n\n const stored = orgAutoCreatesCompanies(org as Record<string, unknown> | undefined)\n const [checked, setChecked] = useState(stored)\n const [busy, setBusy] = useState(false)\n // The stored value wins whenever it changes: the click is optimistic, and\n // the org document is what the capture door will actually read.\n useEffect(() => setChecked(stored), [stored])\n\n const handleChange = async (next: boolean) => {\n // The switch is disabled for a member who may not move it; the guard\n // stands anyway, because a disabled control still delivers a change\n // event in some environments and the rules would refuse the write.\n if (!orgId || !canManage) return\n setChecked(next)\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_AUTO_CREATE_COMPANIES_PATH]: next,\n })\n enqueueSnackbar(\n next\n ? 'Companies will be created from work email domains'\n : 'Companies will no longer be created from email domains',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setChecked(stored)\n enqueueSnackbar('The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('crmSettings', { anchor: '#companies' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={checked}\n disabled={!ready || !canManage || busy}\n onChange={(event) => void handleChange(event.target.checked)}\n />\n }\n label=\"Create companies from work email domains\"\n />\n <FormHelperText>\n {'A contact captured with a work email address is linked to the ' +\n 'company whose domain matches it. When this is on and no such ' +\n 'company exists yet, one is created from the domain — acme.com ' +\n 'becomes Acme — and the contact is linked to it. Public mailbox ' +\n 'domains such as gmail.com never create a company.'}\n </FormHelperText>\n {ready && !canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nAutoCreateCompaniesCard.displayName = 'AutoCreateCompaniesCard'\n\n/** The caption every card shows a member who may read but not change it. */\nfunction ManagersOnlyNote(props: { ready: boolean; canManage: boolean }) {\n return props.ready && !props.canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null\n}\nManagersOnlyNote.displayName = 'ManagersOnlyNote'\n\n/** The `value` of the default-owner picker's \"nobody\" entry. */\nconst NO_DEFAULT_OWNER = ''\n\nexport interface AssignmentCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Default owner\" — who gets the records captured on a site when no\n * assignment rule claims them (AGL-2618).\n *\n * Per site, on the org document: the reader is the org-level assignment\n * pass, which reads one document for every site's default, and the writer\n * is the same owner-or-admin the rest of the `crm` map admits. Under a\n * site the card is that site's one picker; at the ORGANIZATION level\n * (AGL-2630) it is one picker per site in the org, because the map is per\n * site and this is the one place all of it is on screen at once. The\n * field is addressed by `FieldPath` segments rather than a dotted string\n * because a host id is a document id and may contain a dot — joined with\n * dots, the write would land beside the setting rather than in it.\n * Clearing a picker deletes its field, so an org that once set a default\n * and unset it reads exactly like one that never did.\n */\nexport function DefaultOwnerCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const mount = useCrmOrgMount()\n const sites = useMemo<ReadonlyArray<{ id: string; name: string | null }>>(\n () =>\n hostId\n ? [{ id: hostId, name: null }]\n : (mount?.hosts ?? []).map((host) => ({ id: host.id, name: host.name })),\n [hostId, mount?.hosts],\n )\n const ready = scopeReady && roleReady && (Boolean(hostId) || Boolean(mount?.hostsReady))\n return (\n <CardDisplay\n header={'Default owner'}\n help={pluginDocsHelp('crmSettings', { anchor: '#default-owner' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {sites.map((site) => (\n <DefaultOwnerPicker\n key={site.id}\n orgId={orgId}\n siteId={site.id}\n siteName={site.name}\n org={org}\n roster={roster}\n canManage={canManage}\n ready={ready}\n />\n ))}\n {!hostId && ready && sites.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet, so there is nobody to route to.'}\n </Typography>\n ) : null}\n <FormHelperText>\n {'Every contact captured on a site — a form, a sign-up, a booking, ' +\n 'an order — that no assignment rule claims is handed to that ' +\n \"site's default owner, and they are notified. A contact added by \" +\n 'hand or imported with an owner keeps the owner you chose.'}\n </FormHelperText>\n {roster.error ? (\n <Typography variant=\"caption\" color=\"error\">\n {roster.error}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nDefaultOwnerCard.displayName = 'DefaultOwnerCard'\n\n/** One site's slot in the org-wide default-owner map, as a picker. */\nfunction DefaultOwnerPicker(props: {\n orgId: string | undefined\n siteId: string\n /** How the site reads; `null` under the site itself, where \"this site\" will do. */\n siteName: string | null\n org?: Partial<AglynOrgBilling> | null\n roster: ReturnType<typeof useOrgMemberDirectory>\n canManage: boolean\n ready: boolean\n}) {\n const { orgId, siteId, siteName, org, roster, canManage, ready } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const siteLabel = siteName ?? 'this site'\n\n const stored =\n readCrmAssignmentSettings(org as Record<string, unknown> | undefined)\n .hostDefaultOwners[siteId] ?? NO_DEFAULT_OWNER\n const [value, setValue] = useState(stored)\n const [busy, setBusy] = useState(false)\n useEffect(() => setValue(stored), [stored])\n\n const handleChange = async (next: string) => {\n if (!orgId || !canManage) return\n setValue(next)\n setBusy(true)\n try {\n await updateDoc(\n doc(firestore, 'orgs', orgId),\n new FieldPath(...crmHostDefaultOwnerSegments(siteId)),\n next || deleteField(),\n )\n enqueueSnackbar(\n next\n ? `New contacts on ${siteLabel} go to ${roster.nameOf(next)} unless a rule says otherwise`\n : `New contacts on ${siteLabel} stay unassigned unless a rule says otherwise`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setValue(stored)\n enqueueSnackbar('The default owner could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n // A stored owner the roster no longer lists is still shown, by uid, so\n // the picker never silently reads \"Nobody\" for a setting that is there.\n const options = useMemo(\n () =>\n stored && !roster.members.some((member) => member.uid === stored)\n ? [...roster.members, { uid: stored, label: `${stored} (former member)` }]\n : roster.members,\n [roster.members, stored],\n )\n return (\n <TextField\n select\n size=\"small\"\n label={siteName ? `Default owner · ${siteName}` : 'Default owner for this site'}\n value={value}\n onChange={(event) => void handleChange(event.target.value)}\n disabled={!ready || !canManage || busy || roster.loading}\n // \"Nobody\" is the empty value, which a select would otherwise\n // render as a blank rather than as the choice it is. The label is\n // shrunk with it: a field that always draws a value has no empty\n // state for the label to sit in, and left to itself it decides from\n // the value alone and paints the label over the choice.\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n sx={{ maxWidth: 360 }}\n >\n <MenuItem value={NO_DEFAULT_OWNER}>{'Nobody — leave unassigned'}</MenuItem>\n {options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nDefaultOwnerPicker.displayName = 'DefaultOwnerPicker'\n\n/**\n * \"Assignment rules\" — the ordered list the capture pass tries first\n * (AGL-2618), with Add rule in a drawer, reorder and delete.\n *\n * ## The whole list is the unit of write\n *\n * The rules are an array on the org document and every edit — a new rule,\n * a move, a delete — writes the whole array by its dotted path. A per-rule\n * write has no address (an array element is not a field), and the list is\n * bounded at fifty, so the write is small. The array the card writes is\n * the array it read off the org prop plus the one edit, so two admins\n * editing at once last-write-wins at the granularity of one edit, which\n * is what a list this short and this rarely edited needs.\n *\n * ## First match wins, so order is the meaning\n *\n * A rule's position is a fact about it — \"bookings go to Kim, and\n * everything else to Sam\" is two rules in that order and nonsense in the\n * other — which is why the reorder controls sit in the first column and\n * the row reads \"1st, 2nd…\" rather than a name. Up and down rather than\n * drag, the way the custom-field list does it: a drag handle needs a\n * pointer, and the reorder is rare.\n */\nexport function AssignmentRulesCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const settings = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const { rules } = settings\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [busy, setBusy] = useState(false)\n\n const writeRules = useCallback(\n async (next: CrmAssignmentRule[], said: string) => {\n if (!orgId || !canManage) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ASSIGNMENT_RULES_PATH]: next,\n })\n enqueueSnackbar(said, { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The rules could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n throw error\n } finally {\n setBusy(false)\n }\n },\n [firestore, orgId, canManage, enqueueSnackbar],\n )\n\n const move = (index: number, by: -1 | 1) => {\n const target = index + by\n if (target < 0 || target >= rules.length) return\n const next = [...rules]\n ;[next[index], next[target]] = [next[target], next[index]]\n void writeRules(next, 'Rule moved').catch(() => undefined)\n }\n const remove = (rule: CrmAssignmentRule) => {\n void writeRules(\n rules.filter((entry) => entry.id !== rule.id),\n 'Rule deleted',\n ).catch(() => undefined)\n }\n const handleAdd = async (rule: CrmAssignmentRule) => {\n await writeRules([...rules, rule], 'Rule added')\n setDrawerOpen(false)\n }\n\n const ready = scopeReady && roleReady\n const atCap = rules.length >= CRM_ASSIGNMENT_RULES_MAX\n const canEdit = ready && canManage && !busy\n const ordinal = (index: number) => {\n const n = index + 1\n const suffix =\n n % 100 >= 11 && n % 100 <= 13\n ? 'th'\n : (['th', 'st', 'nd', 'rd'] as const)[n % 10] ?? 'th'\n return `${n}${suffix}`\n }\n return (\n <CardDisplay\n header={'Assignment rules'}\n help={pluginDocsHelp('crmSettings', { anchor: '#assignment-rules' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n disabled={!canEdit || atCap}\n onClick={() => setDrawerOpen(true)}\n >\n {'Add rule'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Tried in order for every new contact captured on any site in the ' +\n 'workspace; the first rule whose every condition holds assigns the ' +\n 'owner. A contact no rule claims goes to the capturing site’s ' +\n 'default owner, or stays unassigned.'}\n </Typography>\n {rules.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No rules yet. Add one to route new contacts by where they came from.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 120 }}>{'Order'}</TableCell>\n <TableCell>{'When'}</TableCell>\n <TableCell>{'Assign to'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {rules.map((rule, index) => {\n const described = describeAssignmentRule(rule, roster.nameOf)\n return (\n <TableRow key={rule.id} hover>\n <TableCell>\n <Stack direction=\"row\" spacing={0} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ minWidth: 32 }}>\n {ordinal(index)}\n </Typography>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === 0}\n onClick={() => move(index, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === rules.length - 1}\n onClick={() => move(index, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.when}</Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.assign}</Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu\n label={`Rule ${index + 1}`}\n items={[\n {\n key: 'delete',\n label: 'Delete rule',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !canEdit,\n onClick: () => remove(rule),\n },\n ]}\n />\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\n )}\n {atCap ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`A workspace keeps at most ${CRM_ASSIGNMENT_RULES_MAX} rules. Delete one to add another.`}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n <AssignmentRuleDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n members={roster.members}\n membersLoading={roster.loading}\n poolSize={settings.pool.memberUids.length}\n existingIds={rules.map((rule) => rule.id)}\n onSubmit={handleAdd}\n />\n </CardDisplay>\n )\n}\nAssignmentRulesCard.displayName = 'AssignmentRulesCard'\n\n/**\n * \"Round robin\" — the members handed records in turn (AGL-2618).\n *\n * The pool is the roster with a checkbox per member, and its ORDER is the\n * order members were checked: a member checked later joins the end of the\n * rotation, and unchecking removes them without disturbing the others. The\n * pointer — who got the last record — is the server's to move and is only\n * read here, as \"next up\", so an admin can see where the rotation stands\n * without being able to put a thumb on it.\n */\nexport function RoundRobinCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const { pool } = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const [busy, setBusy] = useState(false)\n\n const toggle = async (uid: string, inPool: boolean) => {\n if (!orgId || !canManage) return\n const next = inPool\n ? [...pool.memberUids.filter((entry) => entry !== uid), uid]\n : pool.memberUids.filter((entry) => entry !== uid)\n if (next.length > CRM_ROUND_ROBIN_POOL_MAX) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ROUND_ROBIN_POOL_PATH]: next,\n })\n enqueueSnackbar(\n inPool\n ? `${roster.nameOf(uid)} joined the rotation`\n : `${roster.nameOf(uid)} left the rotation`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The pool could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n const canEdit = ready && canManage && !busy && !roster.loading\n const order = roundRobinOrder(pool.memberUids, pool.lastAssignedUid)\n const nextUp = order[0]\n // The rotation names people the way the checkboxes above do — with the\n // address, since two members can share a display name.\n const rotationName = (uid: string): string => {\n const member = roster.members.find((row) => row.uid === uid)\n return member ? crmMemberPickerLabel(member) : roster.nameOf(uid)\n }\n return (\n <CardDisplay\n header={'Round robin'}\n help={pluginDocsHelp('crmSettings', { anchor: '#round-robin' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The members a round-robin rule, or an automation set to round ' +\n 'robin, hands records to in turn. Each new record goes to the ' +\n 'member after the last one who got a record, wrapping round.'}\n </Typography>\n {roster.loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading the team…'}\n </Typography>\n ) : roster.error ? (\n <Typography variant=\"body2\" color=\"error\">\n {roster.error}\n </Typography>\n ) : (\n <FormGroup>\n {roster.members.map((member) => (\n <FormControlLabel\n key={member.uid}\n control={\n <Checkbox\n size=\"small\"\n checked={pool.memberUids.includes(member.uid)}\n disabled={!canEdit}\n onChange={(event) => void toggle(member.uid, event.target.checked)}\n />\n }\n label={crmMemberPickerLabel(member)}\n />\n ))}\n </FormGroup>\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {pool.memberUids.length === 0\n ? 'Nobody is in the rotation; a round-robin rule is skipped until somebody is.'\n : `Rotation: ${pool.memberUids.map(rotationName).join(' → ')}. Next up: ${rotationName(nextUp)}.`}\n </Typography>\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nRoundRobinCard.displayName = 'RoundRobinCard'\n\n/**\n * `/crm/settings` — what the CRM does on its own, for every site in the\n * workspace (AGL-2613).\n *\n * A stack of cards, one per concern, so a later setting arrives as a card\n * beside this one rather than a field inside it. Every card writes the org\n * document, because a CRM setting is a fact about how the business files\n * people and not about one site; the section is reached from a site's hub\n * or from the organization's (AGL-2630), and writes the same document from\n * either. The one per-site setting — the default owner — is the site's\n * slot in an org-wide map: under a site the card names the site it is for,\n * and at the organization level it lists every site's slot.\n *\n * The Recipes card (AGL-2639) is the one card that mounts at the\n * ORGANIZATION level only. It installs an automation onto a site of the\n * reader's choosing, which is the org's need; under a site the Actions\n * page's own Recipes menu is the door, and a second one here would write\n * the same action by a different route. Last, because the welcome recipe\n * leans on the round-robin pool set up in the card above it.\n *\n * The Email templates card (AGL-2658) is the one card every CRM editor may\n * write to — a template is working material, not policy — and mounts at\n * both levels, listing the site's letters under a site and the whole\n * workspace's from the organization's hub.\n *\n * Your sending addresses (AGL-2975) sits directly under Email capture,\n * because it is where a member who copies that address from an alias looks\n * next: it is the reader's OWN list, at both levels, and writes through the\n * core route rather than the org document.\n */\nexport function CrmSettingsSection(props: CrmSettingsSectionProps) {\n const { hostId, org } = props\n const mount = useCrmOrgMount()\n // The Email capture card (AGL-2657) rotates on the same owner-or-admin\n // bar every other card writes on; the scope and the role are resolved\n // here once and handed down, since the card itself writes no document.\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n return (\n <Stack spacing={3}>\n <AutoCreateCompaniesCard hostId={hostId} org={org} />\n <DefaultOwnerCard hostId={hostId} org={org} />\n <AssignmentRulesCard hostId={hostId} org={org} />\n <RoundRobinCard hostId={hostId} org={org} />\n <EmailTemplatesCard hostId={hostId} org={org} />\n <EmailCaptureCard hostId={hostId} canManage={canManage} ready={scopeReady && roleReady} />\n <SendingAddressesCard orgId={orgId} ready={scopeReady} />\n {/* Sharing rules (AGL-3336) are the organization's: mounted where its\n sites are listed, at the org level only — before Recipes, which\n stays last. */}\n {mount ? (\n <SharingRulesCard\n orgId={orgId ?? null}\n org={org}\n canManage={canManage}\n ready={scopeReady && roleReady}\n />\n ) : null}\n {mount ? <RecipesCard org={org} /> : null}\n </Stack>\n )\n}\nCrmSettingsSection.displayName = 'CrmSettingsSection'\n\nexport default CrmSettingsSection\n"],"names":["canManageOrg","CRM_ASSIGNMENT_RULES_MAX","CRM_ASSIGNMENT_RULES_PATH","CRM_AUTO_CREATE_COMPANIES_PATH","CRM_ROUND_ROBIN_POOL_MAX","CRM_ROUND_ROBIN_POOL_PATH","crmHostDefaultOwnerSegments","describeAssignmentRule","orgAutoCreatesCompanies","pluginDocsHelp","readCrmAssignmentSettings","roundRobinOrder","crmMemberPickerLabel","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","CardDisplay","MdiIcon","SrOnly","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useFirestoreDoc","useUser","Button","Checkbox","FormControlLabel","FormGroup","FormHelperText","IconButton","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","deleteField","doc","FieldPath","updateDoc","useCallback","useEffect","useMemo","useState","useCrmOrgMount","useCrmScope","useOrgMemberDirectory","AssignmentRuleDrawer","EmailTemplatesCard","EmailCaptureCard","RecipesCard","SendingAddressesCard","SharingRulesCard","useCanManageCrmSettings","orgId","firestore","data","user","uid","member","status","canManage","role","ready","Boolean","AutoCreateCompaniesCard","props","hostId","org","enqueueSnackbar","scopeReady","roleReady","stored","checked","setChecked","busy","setBusy","handleChange","next","variant","persist","error","console","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","control","size","disabled","onChange","event","target","label","color","displayName","ManagersOnlyNote","NO_DEFAULT_OWNER","DefaultOwnerCard","roster","mount","sites","id","name","hosts","map","host","hostsReady","site","DefaultOwnerPicker","siteId","siteName","length","siteLabel","hostDefaultOwners","value","setValue","nameOf","options","members","some","select","loading","slotProps","displayEmpty","inputLabel","shrink","sx","maxWidth","AssignmentRulesCard","settings","rules","drawerOpen","setDrawerOpen","writeRules","said","move","index","by","catch","undefined","remove","rule","filter","entry","handleAdd","atCap","canEdit","ordinal","n","suffix","contentBordered","HeaderProps","action","onClick","width","align","described","hover","direction","alignItems","minWidth","path","when","assign","items","key","icon","destructive","open","onClose","membersLoading","poolSize","pool","memberUids","existingIds","onSubmit","RoundRobinCard","toggle","inPool","order","lastAssignedUid","nextUp","rotationName","find","row","includes","join","CrmSettingsSection"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/settings-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 canManageOrg,\n type ConsolePluginPageProps,\n CRM_ASSIGNMENT_RULES_MAX,\n CRM_ASSIGNMENT_RULES_PATH,\n CRM_AUTO_CREATE_COMPANIES_PATH,\n CRM_DEFAULT_RECORD_SCOPE_PATH,\n CRM_ROUND_ROBIN_POOL_MAX,\n CRM_ROUND_ROBIN_POOL_PATH,\n type CrmAssignmentRule,\n crmDefaultScopeOf,\n crmHostDefaultOwnerSegments,\n describeAssignmentRule,\n orgAutoCreatesCompanies,\n type OrgRole,\n pluginDocsHelp,\n readCrmAssignmentSettings,\n roundRobinOrder,\n crmMemberPickerLabel,\n} from '@aglyn/aglyn'\nimport { mdiArrowDown, mdiArrowUp, mdiDeleteOutline } from '@aglyn/shared-data-mdi'\nimport { CardDisplay, MdiIcon, SrOnly } from '@aglyn/shared-ui-jsx'\nimport RowActionsMenu from '@aglyn/shared-ui-jsx/components/row-actions-menu.component'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport {\n Button,\n Checkbox,\n FormControlLabel,\n FormGroup,\n FormHelperText,\n IconButton,\n MenuItem,\n Stack,\n Switch,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { deleteField, doc, FieldPath, updateDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useCrmOrgMount } from '../hooks/use-crm-org-mount'\nimport { useCrmScope } from '../hooks/use-crm-scope'\nimport { useOrgMemberDirectory } from '../hooks/use-org-member-directory'\nimport AssignmentRuleDrawer from './assignment-rule-drawer'\nimport EmailTemplatesCard from './email-templates-card'\nimport EmailCaptureCard from './email-capture-card'\nimport RecipesCard from './recipes-card'\nimport SendingAddressesCard from './sending-addresses-card'\nimport SharingRulesCard from './sharing-rules-card'\n\nexport type CrmSettingsSectionProps = Pick<ConsolePluginPageProps, 'hostId' | 'org'>\n\n/**\n * Whether the signed-in member may change an org-wide CRM setting, and\n * whether that is known yet.\n *\n * The org document's client branch admits an OWNER or ADMIN and nobody\n * else — `canManageOrg()` in the rules — so the question is the caller's\n * org role, read off their own membership document, which the rules let a\n * member read for themselves. A scoped collaborator, an editor, a viewer:\n * each sees the switch and cannot move it, with the reason beside it,\n * rather than a switch that moves and snaps back with a bare\n * `permission-denied`. `ready` separates \"no\" from \"not yet\", so the\n * control disables with a reason instead of hiding until the read lands.\n */\nexport function useCanManageCrmSettings(orgId: string | undefined): {\n canManage: boolean\n ready: boolean\n} {\n const firestore = useFirestore()\n const { data: user } = useUser()\n const uid = user?.uid ?? ''\n const { data: member, status } = useFirestoreDoc<{ role?: OrgRole }>(\n () => (orgId && uid ? doc(firestore, 'orgs', orgId, 'members', uid) : null),\n [firestore, orgId, uid],\n )\n return {\n canManage: canManageOrg(member?.role ?? null),\n ready: Boolean(orgId && uid) && status !== 'loading',\n }\n}\n\nexport interface AutoCreateCompaniesCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Create companies from work email domains\" — the org's one switch over\n * what a capture does with a company nobody has filed yet (AGL-2613).\n *\n * ## What the switch decides, and what it does not\n *\n * A contact captured from `jane@acme.com` is linked to the company at\n * `acme.com` whether this is on or off, provided exactly one such company\n * is visible to the capturing site. The switch decides the case where NO\n * company carries the domain: on, the capture creates one named after the\n * domain and links the contact; off — the default — it creates nothing and\n * the contact waits for a person to file them. Public mailbox domains never\n * create a company either way; a workspace's consumer list is not a list\n * of accounts.\n *\n * ## Written where it is read\n *\n * One dotted-path `update()` onto `orgs/{orgId}`, so the org's other keys\n * are untouched and the map under `crm` can grow a key per setting. The\n * shell's org listener delivers the new value back, which is what the\n * switch reflects; a local copy holds the click only until then.\n */\nexport function AutoCreateCompaniesCard(props: AutoCreateCompaniesCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n\n const stored = orgAutoCreatesCompanies(org as Record<string, unknown> | undefined)\n const [checked, setChecked] = useState(stored)\n const [busy, setBusy] = useState(false)\n // The stored value wins whenever it changes: the click is optimistic, and\n // the org document is what the capture door will actually read.\n useEffect(() => setChecked(stored), [stored])\n\n const handleChange = async (next: boolean) => {\n // The switch is disabled for a member who may not move it; the guard\n // stands anyway, because a disabled control still delivers a change\n // event in some environments and the rules would refuse the write.\n if (!orgId || !canManage) return\n setChecked(next)\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_AUTO_CREATE_COMPANIES_PATH]: next,\n })\n enqueueSnackbar(\n next\n ? 'Companies will be created from work email domains'\n : 'Companies will no longer be created from email domains',\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setChecked(stored)\n enqueueSnackbar('The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n return (\n <CardDisplay\n header={'Companies'}\n help={pluginDocsHelp('crmSettings', { anchor: '#companies' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={checked}\n disabled={!ready || !canManage || busy}\n onChange={(event) => void handleChange(event.target.checked)}\n />\n }\n label=\"Create companies from work email domains\"\n />\n <FormHelperText>\n {'A contact captured with a work email address is linked to the ' +\n 'company whose domain matches it. When this is on and no such ' +\n 'company exists yet, one is created from the domain — acme.com ' +\n 'becomes Acme — and the contact is linked to it. Public mailbox ' +\n 'domains such as gmail.com never create a company.'}\n </FormHelperText>\n {ready && !canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nAutoCreateCompaniesCard.displayName = 'AutoCreateCompaniesCard'\n\n/**\n * \"Default sharing for new records\" (AGL-3662) — what a new contact,\n * company, deal or task starts shared with, set apart from the org's\n * defaults for new datasets and media.\n *\n * Changes nothing that already exists: narrowing an address book from a\n * select would hide records from sites already working them, with no\n * confirmation. Each record's own sharing is where that is decided.\n *\n * Reads `crmDefaultScopeOf`, the same reader the capture door and every CRM\n * creator stamp from, so the control cannot show one thing while records\n * get another — including for an org that has never set it, which inherits\n * the dataset default it shared before the split.\n */\nexport function DefaultRecordSharingCard(props: AutoCreateCompaniesCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n\n const stored =\n crmDefaultScopeOf(org as Record<string, unknown> | undefined) ?? 'host'\n const [value, setValue] = useState<'org' | 'host'>(stored)\n const [busy, setBusy] = useState(false)\n useEffect(() => setValue(stored), [stored])\n\n const handleChange = async (next: 'org' | 'host') => {\n if (!orgId || !canManage) return\n setValue(next)\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_DEFAULT_RECORD_SCOPE_PATH]: next,\n })\n enqueueSnackbar('Default sharing updated', {\n variant: 'success',\n persist: false,\n })\n } catch (error) {\n console.error(error)\n setValue(stored)\n enqueueSnackbar('The setting could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n return (\n <CardDisplay\n header={'Default sharing for new records'}\n help={pluginDocsHelp('crmSettings')}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1} sx={{ maxWidth: 480 }}>\n <TextField\n select\n size=\"small\"\n label=\"New contacts, companies, deals and tasks are shared with\"\n value={value}\n disabled={!ready || !canManage || busy}\n onChange={(event) =>\n void handleChange(event.target.value as 'org' | 'host')\n }\n helperText={\n 'Only affects records created from now on. Datasets and media ' +\n 'have defaults of their own, on the organization Data and ' +\n 'Media pages.'\n }\n >\n <MenuItem value=\"org\">{'All sites'}</MenuItem>\n <MenuItem value=\"host\">\n {'The site they came in on, and its consent group'}\n </MenuItem>\n </TextField>\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nDefaultRecordSharingCard.displayName = 'DefaultRecordSharingCard'\n\n/** The caption every card shows a member who may read but not change it. */\nfunction ManagersOnlyNote(props: { ready: boolean; canManage: boolean }) {\n return props.ready && !props.canManage ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Only a workspace owner or admin can change this.'}\n </Typography>\n ) : null\n}\nManagersOnlyNote.displayName = 'ManagersOnlyNote'\n\n/** The `value` of the default-owner picker's \"nobody\" entry. */\nconst NO_DEFAULT_OWNER = ''\n\nexport interface AssignmentCardProps {\n /** The site the section is read under, or `null` at the organization level. */\n hostId: string | null\n org?: Partial<AglynOrgBilling> | null\n}\n\n/**\n * \"Default owner\" — who gets the records captured on a site when no\n * assignment rule claims them (AGL-2618).\n *\n * Per site, on the org document: the reader is the org-level assignment\n * pass, which reads one document for every site's default, and the writer\n * is the same owner-or-admin the rest of the `crm` map admits. Under a\n * site the card is that site's one picker; at the ORGANIZATION level\n * (AGL-2630) it is one picker per site in the org, because the map is per\n * site and this is the one place all of it is on screen at once. The\n * field is addressed by `FieldPath` segments rather than a dotted string\n * because a host id is a document id and may contain a dot — joined with\n * dots, the write would land beside the setting rather than in it.\n * Clearing a picker deletes its field, so an org that once set a default\n * and unset it reads exactly like one that never did.\n */\nexport function DefaultOwnerCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const mount = useCrmOrgMount()\n const sites = useMemo<ReadonlyArray<{ id: string; name: string | null }>>(\n () =>\n hostId\n ? [{ id: hostId, name: null }]\n : (mount?.hosts ?? []).map((host) => ({ id: host.id, name: host.name })),\n [hostId, mount?.hosts],\n )\n const ready = scopeReady && roleReady && (Boolean(hostId) || Boolean(mount?.hostsReady))\n return (\n <CardDisplay\n header={'Default owner'}\n help={pluginDocsHelp('crmSettings', { anchor: '#default-owner' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {sites.map((site) => (\n <DefaultOwnerPicker\n key={site.id}\n orgId={orgId}\n siteId={site.id}\n siteName={site.name}\n org={org}\n roster={roster}\n canManage={canManage}\n ready={ready}\n />\n ))}\n {!hostId && ready && sites.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'This organization has no sites yet, so there is nobody to route to.'}\n </Typography>\n ) : null}\n <FormHelperText>\n {'Every contact captured on a site — a form, a sign-up, a booking, ' +\n 'an order — that no assignment rule claims is handed to that ' +\n \"site's default owner, and they are notified. A contact added by \" +\n 'hand or imported with an owner keeps the owner you chose.'}\n </FormHelperText>\n {roster.error ? (\n <Typography variant=\"caption\" color=\"error\">\n {roster.error}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nDefaultOwnerCard.displayName = 'DefaultOwnerCard'\n\n/** One site's slot in the org-wide default-owner map, as a picker. */\nfunction DefaultOwnerPicker(props: {\n orgId: string | undefined\n siteId: string\n /** How the site reads; `null` under the site itself, where \"this site\" will do. */\n siteName: string | null\n org?: Partial<AglynOrgBilling> | null\n roster: ReturnType<typeof useOrgMemberDirectory>\n canManage: boolean\n ready: boolean\n}) {\n const { orgId, siteId, siteName, org, roster, canManage, ready } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const siteLabel = siteName ?? 'this site'\n\n const stored =\n readCrmAssignmentSettings(org as Record<string, unknown> | undefined)\n .hostDefaultOwners[siteId] ?? NO_DEFAULT_OWNER\n const [value, setValue] = useState(stored)\n const [busy, setBusy] = useState(false)\n useEffect(() => setValue(stored), [stored])\n\n const handleChange = async (next: string) => {\n if (!orgId || !canManage) return\n setValue(next)\n setBusy(true)\n try {\n await updateDoc(\n doc(firestore, 'orgs', orgId),\n new FieldPath(...crmHostDefaultOwnerSegments(siteId)),\n next || deleteField(),\n )\n enqueueSnackbar(\n next\n ? `New contacts on ${siteLabel} go to ${roster.nameOf(next)} unless a rule says otherwise`\n : `New contacts on ${siteLabel} stay unassigned unless a rule says otherwise`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n setValue(stored)\n enqueueSnackbar('The default owner could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n // A stored owner the roster no longer lists is still shown, by uid, so\n // the picker never silently reads \"Nobody\" for a setting that is there.\n const options = useMemo(\n () =>\n stored && !roster.members.some((member) => member.uid === stored)\n ? [...roster.members, { uid: stored, label: `${stored} (former member)` }]\n : roster.members,\n [roster.members, stored],\n )\n return (\n <TextField\n select\n size=\"small\"\n label={siteName ? `Default owner · ${siteName}` : 'Default owner for this site'}\n value={value}\n onChange={(event) => void handleChange(event.target.value)}\n disabled={!ready || !canManage || busy || roster.loading}\n // \"Nobody\" is the empty value, which a select would otherwise\n // render as a blank rather than as the choice it is. The label is\n // shrunk with it: a field that always draws a value has no empty\n // state for the label to sit in, and left to itself it decides from\n // the value alone and paints the label over the choice.\n slotProps={{ select: { displayEmpty: true }, inputLabel: { shrink: true } }}\n sx={{ maxWidth: 360 }}\n >\n <MenuItem value={NO_DEFAULT_OWNER}>{'Nobody — leave unassigned'}</MenuItem>\n {options.map((member) => (\n <MenuItem key={member.uid} value={member.uid}>\n {crmMemberPickerLabel(member)}\n </MenuItem>\n ))}\n </TextField>\n )\n}\nDefaultOwnerPicker.displayName = 'DefaultOwnerPicker'\n\n/**\n * \"Assignment rules\" — the ordered list the capture pass tries first\n * (AGL-2618), with Add rule in a drawer, reorder and delete.\n *\n * ## The whole list is the unit of write\n *\n * The rules are an array on the org document and every edit — a new rule,\n * a move, a delete — writes the whole array by its dotted path. A per-rule\n * write has no address (an array element is not a field), and the list is\n * bounded at fifty, so the write is small. The array the card writes is\n * the array it read off the org prop plus the one edit, so two admins\n * editing at once last-write-wins at the granularity of one edit, which\n * is what a list this short and this rarely edited needs.\n *\n * ## First match wins, so order is the meaning\n *\n * A rule's position is a fact about it — \"bookings go to Kim, and\n * everything else to Sam\" is two rules in that order and nonsense in the\n * other — which is why the reorder controls sit in the first column and\n * the row reads \"1st, 2nd…\" rather than a name. Up and down rather than\n * drag, the way the custom-field list does it: a drag handle needs a\n * pointer, and the reorder is rare.\n */\nexport function AssignmentRulesCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const settings = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const { rules } = settings\n const [drawerOpen, setDrawerOpen] = useState(false)\n const [busy, setBusy] = useState(false)\n\n const writeRules = useCallback(\n async (next: CrmAssignmentRule[], said: string) => {\n if (!orgId || !canManage) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ASSIGNMENT_RULES_PATH]: next,\n })\n enqueueSnackbar(said, { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The rules could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n throw error\n } finally {\n setBusy(false)\n }\n },\n [firestore, orgId, canManage, enqueueSnackbar],\n )\n\n const move = (index: number, by: -1 | 1) => {\n const target = index + by\n if (target < 0 || target >= rules.length) return\n const next = [...rules]\n ;[next[index], next[target]] = [next[target], next[index]]\n void writeRules(next, 'Rule moved').catch(() => undefined)\n }\n const remove = (rule: CrmAssignmentRule) => {\n void writeRules(\n rules.filter((entry) => entry.id !== rule.id),\n 'Rule deleted',\n ).catch(() => undefined)\n }\n const handleAdd = async (rule: CrmAssignmentRule) => {\n await writeRules([...rules, rule], 'Rule added')\n setDrawerOpen(false)\n }\n\n const ready = scopeReady && roleReady\n const atCap = rules.length >= CRM_ASSIGNMENT_RULES_MAX\n const canEdit = ready && canManage && !busy\n const ordinal = (index: number) => {\n const n = index + 1\n const suffix =\n n % 100 >= 11 && n % 100 <= 13\n ? 'th'\n : (['th', 'st', 'nd', 'rd'] as const)[n % 10] ?? 'th'\n return `${n}${suffix}`\n }\n return (\n <CardDisplay\n header={'Assignment rules'}\n help={pluginDocsHelp('crmSettings', { anchor: '#assignment-rules' })}\n contentGutterX\n contentGutterY\n contentBordered=\"all\"\n HeaderProps={{\n action: (\n <Button\n variant=\"contained\"\n disabled={!canEdit || atCap}\n onClick={() => setDrawerOpen(true)}\n >\n {'Add rule'}\n </Button>\n ),\n }}\n >\n <Stack spacing={2}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Tried in order for every new contact captured on any site in the ' +\n 'workspace; the first rule whose every condition holds assigns the ' +\n 'owner. A contact no rule claims goes to the capturing site’s ' +\n 'default owner, or stays unassigned.'}\n </Typography>\n {rules.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'No rules yet. Add one to route new contacts by where they came from.'}\n </Typography>\n ) : (\n <ScrollTable size=\"small\">\n <TableHead>\n <TableRow>\n <TableCell sx={{ width: 120 }}>{'Order'}</TableCell>\n <TableCell>{'When'}</TableCell>\n <TableCell>{'Assign to'}</TableCell>\n <TableCell align=\"right\" />\n </TableRow>\n </TableHead>\n <TableBody>\n {rules.map((rule, index) => {\n const described = describeAssignmentRule(rule, roster.nameOf)\n return (\n <TableRow key={rule.id} hover>\n <TableCell>\n <Stack direction=\"row\" spacing={0} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ minWidth: 32 }}>\n {ordinal(index)}\n </Typography>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === 0}\n onClick={() => move(index, -1)}\n >\n <MdiIcon path={mdiArrowUp.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} up`}</SrOnly>\n </IconButton>\n <IconButton\n size=\"small\"\n disabled={!canEdit || index === rules.length - 1}\n onClick={() => move(index, 1)}\n >\n <MdiIcon path={mdiArrowDown.path} size={0.7} />\n <SrOnly>{`Move rule ${index + 1} down`}</SrOnly>\n </IconButton>\n </Stack>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.when}</Typography>\n </TableCell>\n <TableCell>\n <Typography variant=\"body2\">{described.assign}</Typography>\n </TableCell>\n <TableCell align=\"right\" sx={{ width: 56 }}>\n <RowActionsMenu\n label={`Rule ${index + 1}`}\n items={[\n {\n key: 'delete',\n label: 'Delete rule',\n icon: <MdiIcon path={mdiDeleteOutline.path} size={0.8} />,\n destructive: true,\n disabled: !canEdit,\n onClick: () => remove(rule),\n },\n ]}\n />\n </TableCell>\n </TableRow>\n )\n })}\n </TableBody>\n </ScrollTable>\n )}\n {atCap ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`A workspace keeps at most ${CRM_ASSIGNMENT_RULES_MAX} rules. Delete one to add another.`}\n </Typography>\n ) : null}\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n <AssignmentRuleDrawer\n open={drawerOpen}\n onClose={() => setDrawerOpen(false)}\n members={roster.members}\n membersLoading={roster.loading}\n poolSize={settings.pool.memberUids.length}\n existingIds={rules.map((rule) => rule.id)}\n onSubmit={handleAdd}\n />\n </CardDisplay>\n )\n}\nAssignmentRulesCard.displayName = 'AssignmentRulesCard'\n\n/**\n * \"Round robin\" — the members handed records in turn (AGL-2618).\n *\n * The pool is the roster with a checkbox per member, and its ORDER is the\n * order members were checked: a member checked later joins the end of the\n * rotation, and unchecking removes them without disturbing the others. The\n * pointer — who got the last record — is the server's to move and is only\n * read here, as \"next up\", so an admin can see where the rotation stands\n * without being able to put a thumb on it.\n */\nexport function RoundRobinCard(props: AssignmentCardProps) {\n const { hostId, org } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n const roster = useOrgMemberDirectory(orgId)\n const { pool } = useMemo(\n () => readCrmAssignmentSettings(org as Record<string, unknown> | undefined),\n [org],\n )\n const [busy, setBusy] = useState(false)\n\n const toggle = async (uid: string, inPool: boolean) => {\n if (!orgId || !canManage) return\n const next = inPool\n ? [...pool.memberUids.filter((entry) => entry !== uid), uid]\n : pool.memberUids.filter((entry) => entry !== uid)\n if (next.length > CRM_ROUND_ROBIN_POOL_MAX) return\n setBusy(true)\n try {\n await updateDoc(doc(firestore, 'orgs', orgId), {\n [CRM_ROUND_ROBIN_POOL_PATH]: next,\n })\n enqueueSnackbar(\n inPool\n ? `${roster.nameOf(uid)} joined the rotation`\n : `${roster.nameOf(uid)} left the rotation`,\n { variant: 'success', persist: false },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('The pool could not be saved', {\n variant: 'error',\n allowDuplicate: true,\n })\n } finally {\n setBusy(false)\n }\n }\n\n const ready = scopeReady && roleReady\n const canEdit = ready && canManage && !busy && !roster.loading\n const order = roundRobinOrder(pool.memberUids, pool.lastAssignedUid)\n const nextUp = order[0]\n // The rotation names people the way the checkboxes above do — with the\n // address, since two members can share a display name.\n const rotationName = (uid: string): string => {\n const member = roster.members.find((row) => row.uid === uid)\n return member ? crmMemberPickerLabel(member) : roster.nameOf(uid)\n }\n return (\n <CardDisplay\n header={'Round robin'}\n help={pluginDocsHelp('crmSettings', { anchor: '#round-robin' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The members a round-robin rule, or an automation set to round ' +\n 'robin, hands records to in turn. Each new record goes to the ' +\n 'member after the last one who got a record, wrapping round.'}\n </Typography>\n {roster.loading ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Loading the team…'}\n </Typography>\n ) : roster.error ? (\n <Typography variant=\"body2\" color=\"error\">\n {roster.error}\n </Typography>\n ) : (\n <FormGroup>\n {roster.members.map((member) => (\n <FormControlLabel\n key={member.uid}\n control={\n <Checkbox\n size=\"small\"\n checked={pool.memberUids.includes(member.uid)}\n disabled={!canEdit}\n onChange={(event) => void toggle(member.uid, event.target.checked)}\n />\n }\n label={crmMemberPickerLabel(member)}\n />\n ))}\n </FormGroup>\n )}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {pool.memberUids.length === 0\n ? 'Nobody is in the rotation; a round-robin rule is skipped until somebody is.'\n : `Rotation: ${pool.memberUids.map(rotationName).join(' → ')}. Next up: ${rotationName(nextUp)}.`}\n </Typography>\n <ManagersOnlyNote ready={ready} canManage={canManage} />\n </Stack>\n </CardDisplay>\n )\n}\nRoundRobinCard.displayName = 'RoundRobinCard'\n\n/**\n * `/crm/settings` — what the CRM does on its own, for every site in the\n * workspace (AGL-2613).\n *\n * A stack of cards, one per concern, so a later setting arrives as a card\n * beside this one rather than a field inside it. Every card writes the org\n * document, because a CRM setting is a fact about how the business files\n * people and not about one site; the section is reached from a site's hub\n * or from the organization's (AGL-2630), and writes the same document from\n * either. The one per-site setting — the default owner — is the site's\n * slot in an org-wide map: under a site the card names the site it is for,\n * and at the organization level it lists every site's slot.\n *\n * The Recipes card (AGL-2639) is the one card that mounts at the\n * ORGANIZATION level only. It installs an automation onto a site of the\n * reader's choosing, which is the org's need; under a site the Actions\n * page's own Recipes menu is the door, and a second one here would write\n * the same action by a different route. Last, because the welcome recipe\n * leans on the round-robin pool set up in the card above it.\n *\n * The Email templates card (AGL-2658) is the one card every CRM editor may\n * write to — a template is working material, not policy — and mounts at\n * both levels, listing the site's letters under a site and the whole\n * workspace's from the organization's hub.\n *\n * Your sending addresses (AGL-2975) sits directly under Email capture,\n * because it is where a member who copies that address from an alias looks\n * next: it is the reader's OWN list, at both levels, and writes through the\n * core route rather than the org document.\n */\nexport function CrmSettingsSection(props: CrmSettingsSectionProps) {\n const { hostId, org } = props\n const mount = useCrmOrgMount()\n // The Email capture card (AGL-2657) rotates on the same owner-or-admin\n // bar every other card writes on; the scope and the role are resolved\n // here once and handed down, since the card itself writes no document.\n const { orgId, ready: scopeReady } = useCrmScope({ hostId, org })\n const { canManage, ready: roleReady } = useCanManageCrmSettings(orgId)\n return (\n <Stack spacing={3}>\n <DefaultRecordSharingCard hostId={hostId} org={org} />\n <AutoCreateCompaniesCard hostId={hostId} org={org} />\n <DefaultOwnerCard hostId={hostId} org={org} />\n <AssignmentRulesCard hostId={hostId} org={org} />\n <RoundRobinCard hostId={hostId} org={org} />\n <EmailTemplatesCard hostId={hostId} org={org} />\n <EmailCaptureCard hostId={hostId} canManage={canManage} ready={scopeReady && roleReady} />\n <SendingAddressesCard orgId={orgId} ready={scopeReady} />\n {/* Sharing rules (AGL-3336) are the organization's: mounted where its\n sites are listed, at the org level only — before Recipes, which\n stays last. */}\n {mount ? (\n <SharingRulesCard\n orgId={orgId ?? null}\n org={org}\n canManage={canManage}\n ready={scopeReady && roleReady}\n />\n ) : null}\n {mount ? <RecipesCard org={org} /> : null}\n </Stack>\n )\n}\nCrmSettingsSection.displayName = 'CrmSettingsSection'\n\nexport default CrmSettingsSection\n"],"names":["canManageOrg","CRM_ASSIGNMENT_RULES_MAX","CRM_ASSIGNMENT_RULES_PATH","CRM_AUTO_CREATE_COMPANIES_PATH","CRM_DEFAULT_RECORD_SCOPE_PATH","CRM_ROUND_ROBIN_POOL_MAX","CRM_ROUND_ROBIN_POOL_PATH","crmDefaultScopeOf","crmHostDefaultOwnerSegments","describeAssignmentRule","orgAutoCreatesCompanies","pluginDocsHelp","readCrmAssignmentSettings","roundRobinOrder","crmMemberPickerLabel","mdiArrowDown","mdiArrowUp","mdiDeleteOutline","CardDisplay","MdiIcon","SrOnly","RowActionsMenu","ScrollTable","useSnackbar","useFirestore","useFirestoreDoc","useUser","Button","Checkbox","FormControlLabel","FormGroup","FormHelperText","IconButton","MenuItem","Stack","Switch","TableBody","TableCell","TableHead","TableRow","TextField","Typography","deleteField","doc","FieldPath","updateDoc","useCallback","useEffect","useMemo","useState","useCrmOrgMount","useCrmScope","useOrgMemberDirectory","AssignmentRuleDrawer","EmailTemplatesCard","EmailCaptureCard","RecipesCard","SendingAddressesCard","SharingRulesCard","useCanManageCrmSettings","orgId","firestore","data","user","uid","member","status","canManage","role","ready","Boolean","AutoCreateCompaniesCard","props","hostId","org","enqueueSnackbar","scopeReady","roleReady","stored","checked","setChecked","busy","setBusy","handleChange","next","variant","persist","error","console","allowDuplicate","header","help","anchor","contentGutterX","contentGutterY","spacing","control","size","disabled","onChange","event","target","label","color","displayName","DefaultRecordSharingCard","value","setValue","sx","maxWidth","select","helperText","ManagersOnlyNote","NO_DEFAULT_OWNER","DefaultOwnerCard","roster","mount","sites","id","name","hosts","map","host","hostsReady","site","DefaultOwnerPicker","siteId","siteName","length","siteLabel","hostDefaultOwners","nameOf","options","members","some","loading","slotProps","displayEmpty","inputLabel","shrink","AssignmentRulesCard","settings","rules","drawerOpen","setDrawerOpen","writeRules","said","move","index","by","catch","undefined","remove","rule","filter","entry","handleAdd","atCap","canEdit","ordinal","n","suffix","contentBordered","HeaderProps","action","onClick","width","align","described","hover","direction","alignItems","minWidth","path","when","assign","items","key","icon","destructive","open","onClose","membersLoading","poolSize","pool","memberUids","existingIds","onSubmit","RoundRobinCard","toggle","inPool","order","lastAssignedUid","nextUp","rotationName","find","row","includes","join","CrmSettingsSection"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/tasks-calendar.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 { type CrmTaskPicklists, crmTaskPicklistLabels } from '@aglyn/aglyn'\nimport { mdiChevronLeft, mdiChevronRight } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Box, Button, ButtonBase, IconButton, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport type { CrmTaskRow } from '../hooks/use-crm-tasks'\nimport {\n bucketTasksByCalendarDay,\n CALENDAR_DAY_LABELS,\n CALENDAR_WEEK_DAYS,\n shiftCalendarMonth,\n taskCalendarMonth,\n} from '../model/task-calendar'\n\n/** How many tasks a square lists before folding the rest into a count. */\nconst TASKS_PER_DAY = 3\n\nexport interface TasksCalendarProps {\n /** The window the list is holding — the calendar reads no more than it. */\n tasks: readonly CrmTaskRow[]\n /** Open one task's drawer, the same drawer a row opens. */\n onOpen: (task: CrmTaskRow) => void\n /** The list's window filled, so neither surface is showing everything. */\n truncated?: boolean\n /** The org's task lists, which name each entry's type and status on hover (AGL-3517). */\n picklists?: CrmTaskPicklists\n}\n\n/**\n * A MONTH OF TASKS, BY DUE DATE (AGL-2662).\n *\n * The Tasks list answers \"what is on my plate\"; this answers \"what does\n * next week look like\", which a list ordered by due date can only imply.\n *\n * ## Drawn over the list's own window, and never over a second read\n *\n * The rows are the ones the section already holds. A calendar that fetched\n * its own month would be a second listener over the same collection, would\n * disagree with the list whenever the two windows differed, and would put\n * a read behind every page of the arrows. What it costs instead is honesty:\n * the grid can only place what the window carries, so the tasks due in\n * other months are counted under it rather than implied to be absent.\n *\n * ## Hand-built rather than a calendar dependency\n *\n * Seven columns of MUI boxes and the local-day arithmetic in\n * `task-calendar.ts`. The one calendar package already installed\n * (`@mui/x-date-pickers`) draws a date PICKER — a grid whose squares are\n * choices, not content — so it answers a different question, and a\n * scheduling library for one surface is a dependency the whole console\n * would carry.\n */\nexport function TasksCalendar(props: TasksCalendarProps) {\n const { tasks, onOpen, truncated, picklists } = props\n /** \"Call Ada · Call · In Progress\" — the entry's type and status on hover. */\n const hoverText = (task: CrmTaskRow) => {\n if (!picklists) return task.title\n const labels = crmTaskPicklistLabels(task, picklists)\n return `${task.title} · ${labels.type} · ${labels.status}`\n }\n /*\n * The clock is read ONCE, at mount. A component that read `Date.now()`\n * per render would move the today square under a reader who left the tab\n * open across midnight, without any state having changed — and the grid\n * is a pure function of this, so what is drawn can be pinned.\n */\n const [nowMs] = useState(() => Date.now())\n const [anchorMs, setAnchorMs] = useState(() => nowMs)\n\n const month = useMemo(\n () => taskCalendarMonth(anchorMs, nowMs),\n [anchorMs, nowMs],\n )\n const buckets = useMemo(\n () => bucketTasksByCalendarDay(tasks, (task) => task.dueAtMs, month),\n [tasks, month],\n )\n const label = useMemo(\n () =>\n month.monthDate.toLocaleDateString(undefined, {\n month: 'long',\n year: 'numeric',\n }),\n [month],\n )\n\n return (\n <Stack spacing={1.5}>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <IconButton\n size=\"small\"\n aria-label=\"Previous month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, -1))}\n >\n <MdiIcon path={mdiChevronLeft.path} size={0.9} />\n </IconButton>\n <Typography variant=\"subtitle1\" sx={{ minWidth: 160, textAlign: 'center' }}>\n {label}\n </Typography>\n <IconButton\n size=\"small\"\n aria-label=\"Next month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, 1))}\n >\n <MdiIcon path={mdiChevronRight.path} size={0.9} />\n </IconButton>\n <Button size=\"small\" onClick={() => setAnchorMs(nowMs)}>\n {'Today'}\n </Button>\n </Stack>\n\n <Box sx={{ overflowX: 'auto' }}>\n <Box sx={{ minWidth: 640 }}>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n mb: 0.5,\n }}\n >\n {CALENDAR_DAY_LABELS.map((day) => (\n <Typography\n key={day}\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ textAlign: 'center' }}\n >\n {day}\n </Typography>\n ))}\n </Box>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n }}\n >\n {month.weeks.flat().map((day) => {\n const held = buckets.byDay.get(day.startMs) ?? []\n const shown = held.slice(0, TASKS_PER_DAY)\n const folded = held.length - shown.length\n return (\n <Box\n key={day.startMs}\n sx={{\n minHeight: 96,\n p: 0.75,\n borderRadius: 1,\n border: 1,\n borderColor: day.isToday ? 'primary.main' : 'divider',\n bgcolor: day.inMonth ? 'background.paper' : 'action.hover',\n }}\n >\n <Typography\n variant=\"caption\"\n sx={{\n display: 'block',\n mb: 0.5,\n fontWeight: day.isToday ? 'fontWeightBold' : undefined,\n color: day.inMonth ? 'text.primary' : 'text.disabled',\n }}\n >\n {day.date}\n </Typography>\n <Stack spacing={0.25}>\n {shown.map((task) => (\n <ButtonBase\n key={task.$id}\n onClick={() => onOpen(task)}\n title={hoverText(task)}\n sx={{\n justifyContent: 'flex-start',\n width: '100%',\n px: 0.5,\n py: 0.25,\n borderRadius: 0.5,\n bgcolor: 'action.selected',\n '&:hover': { bgcolor: 'action.focus' },\n }}\n >\n <Typography\n variant=\"caption\"\n noWrap\n sx={{\n width: '100%',\n textAlign: 'left',\n textDecoration:\n task.status === 'done' ? 'line-through' : undefined,\n color:\n task.status === 'done'\n ? 'text.secondary'\n : day.endMs <= nowMs\n ? 'error.main'\n : 'text.primary',\n }}\n >\n {task.title}\n </Typography>\n </ButtonBase>\n ))}\n {folded > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${folded} more`}\n </Typography>\n ) : null}\n </Stack>\n </Box>\n )\n })}\n </Box>\n </Box>\n </Box>\n\n {/*\n What the grid could not place. Said plainly, because a month with\n nothing on it reads as \"nothing is due\" and the honest reading here\n is often \"the window's tasks are somewhere else\".\n */}\n {buckets.undated.length || buckets.elsewhere.length || truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n buckets.elsewhere.length\n ? `${buckets.elsewhere.length} due outside this month`\n : '',\n buckets.undated.length\n ? `${buckets.undated.length} with no due date`\n : '',\n truncated ? 'and the view is showing only its first page' : '',\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nTasksCalendar.displayName = 'TasksCalendar'\n\nexport default TasksCalendar\n"],"names":["crmTaskPicklistLabels","mdiChevronLeft","mdiChevronRight","MdiIcon","Box","Button","ButtonBase","IconButton","Stack","Typography","useMemo","useState","bucketTasksByCalendarDay","CALENDAR_DAY_LABELS","CALENDAR_WEEK_DAYS","shiftCalendarMonth","taskCalendarMonth","TASKS_PER_DAY","TasksCalendar","props","tasks","onOpen","truncated","picklists","hoverText","task","title","labels","type","status","nowMs","Date","now","anchorMs","setAnchorMs","month","buckets","dueAtMs","label","monthDate","toLocaleDateString","undefined","year","spacing","direction","sx","alignItems","flexWrap","rowGap","size","aria-label","onClick","current","path","variant","minWidth","textAlign","overflowX","display","gridTemplateColumns","gap","mb","map","day","color","weeks","flat","held","byDay","get","startMs","shown","slice","folded","length","minHeight","p","borderRadius","border","borderColor","isToday","bgcolor","inMonth","fontWeight","date","justifyContent","width","px","py","noWrap","textDecoration","endMs","$id","undated","elsewhere","filter","Boolean","join","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAEA,SAAgCA,qBAAqB,QAAQ,eAAc;AAC3E,SAASC,cAAc,EAAEC,eAAe,QAAQ,yBAAwB;AACxE,SAASC,OAAO,QAAQ,uBAAsB;AAC9C,SAASC,GAAG,EAAEC,MAAM,EAAEC,UAAU,EAAEC,UAAU,EAAEC,KAAK,EAAEC,UAAU,QAAQ,gBAAe;AACtF,SAASC,OAAO,EAAEC,QAAQ,QAAQ,QAAO;AAEzC,SACEC,wBAAwB,EACxBC,mBAAmB,EACnBC,kBAAkB,EAClBC,kBAAkB,EAClBC,iBAAiB,QACZ,4BAAwB;AAE/B,wEAAwE,GACxE,MAAMC,gBAAgB;AAatB;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,SAASC,cAAcC,KAAyB;IACrD,MAAM,EAAEC,KAAK,EAAEC,MAAM,EAAEC,SAAS,EAAEC,SAAS,EAAE,GAAGJ;IAChD,4EAA4E,GAC5E,MAAMK,YAAY,CAACC;QACjB,IAAI,CAACF,WAAW,OAAOE,KAAKC,KAAK;QACjC,MAAMC,SAAS3B,sBAAsByB,MAAMF;QAC3C,OAAO,GAAGE,KAAKC,KAAK,CAAC,GAAG,EAAEC,OAAOC,IAAI,CAAC,GAAG,EAAED,OAAOE,MAAM,EAAE;IAC5D;IACA;;;;;GAKC,GACD,MAAM,CAACC,MAAM,GAAGnB,SAAS,IAAMoB,KAAKC,GAAG;IACvC,MAAM,CAACC,UAAUC,YAAY,GAAGvB,SAAS,IAAMmB;IAE/C,MAAMK,QAAQzB,QACZ,IAAMM,kBAAkBiB,UAAUH,QAClC;QAACG;QAAUH;KAAM;IAEnB,MAAMM,UAAU1B,QACd,IAAME,yBAAyBQ,OAAO,CAACK,OAASA,KAAKY,OAAO,EAAEF,QAC9D;QAACf;QAAOe;KAAM;IAEhB,MAAMG,QAAQ5B,QACZ,IACEyB,MAAMI,SAAS,CAACC,kBAAkB,CAACC,WAAW;YAC5CN,OAAO;YACPO,MAAM;QACR,IACF;QAACP;KAAM;IAGT,qBACE,MAAC3B;QAAMmC,SAAS;;0BACd,MAACnC;gBACCoC,WAAU;gBACVD,SAAS;gBACTE,IAAI;oBAAEC,YAAY;oBAAUC,UAAU;oBAAQC,QAAQ;gBAAE;;kCAExD,KAACzC;wBACC0C,MAAK;wBACLC,cAAW;wBACXC,SAAS,IAAMjB,YAAY,CAACkB,UAAYrC,mBAAmBqC,SAAS,CAAC;kCAErE,cAAA,KAACjD;4BAAQkD,MAAMpD,eAAeoD,IAAI;4BAAEJ,MAAM;;;kCAE5C,KAACxC;wBAAW6C,SAAQ;wBAAYT,IAAI;4BAAEU,UAAU;4BAAKC,WAAW;wBAAS;kCACtElB;;kCAEH,KAAC/B;wBACC0C,MAAK;wBACLC,cAAW;wBACXC,SAAS,IAAMjB,YAAY,CAACkB,UAAYrC,mBAAmBqC,SAAS;kCAEpE,cAAA,KAACjD;4BAAQkD,MAAMnD,gBAAgBmD,IAAI;4BAAEJ,MAAM;;;kCAE7C,KAAC5C;wBAAO4C,MAAK;wBAAQE,SAAS,IAAMjB,YAAYJ;kCAC7C;;;;0BAIL,KAAC1B;gBAAIyC,IAAI;oBAAEY,WAAW;gBAAO;0BAC3B,cAAA,MAACrD;oBAAIyC,IAAI;wBAAEU,UAAU;oBAAI;;sCACvB,KAACnD;4BACCyC,IAAI;gCACFa,SAAS;gCACTC,qBAAqB,CAAC,OAAO,EAAE7C,mBAAmB,MAAM,CAAC;gCACzD8C,KAAK;gCACLC,IAAI;4BACN;sCAEChD,oBAAoBiD,GAAG,CAAC,CAACC,oBACxB,KAACtD;oCAEC6C,SAAQ;oCACRU,OAAM;oCACNnB,IAAI;wCAAEW,WAAW;oCAAS;8CAEzBO;mCALIA;;sCASX,KAAC3D;4BACCyC,IAAI;gCACFa,SAAS;gCACTC,qBAAqB,CAAC,OAAO,EAAE7C,mBAAmB,MAAM,CAAC;gCACzD8C,KAAK;4BACP;sCAECzB,MAAM8B,KAAK,CAACC,IAAI,GAAGJ,GAAG,CAAC,CAACC;oCACV3B;gCAAb,MAAM+B,QAAO/B,qBAAAA,QAAQgC,KAAK,CAACC,GAAG,CAACN,IAAIO,OAAO,aAA7BlC,qBAAkC,EAAE;gCACjD,MAAMmC,QAAQJ,KAAKK,KAAK,CAAC,GAAGvD;gCAC5B,MAAMwD,SAASN,KAAKO,MAAM,GAAGH,MAAMG,MAAM;gCACzC,qBACE,MAACtE;oCAECyC,IAAI;wCACF8B,WAAW;wCACXC,GAAG;wCACHC,cAAc;wCACdC,QAAQ;wCACRC,aAAahB,IAAIiB,OAAO,GAAG,iBAAiB;wCAC5CC,SAASlB,IAAImB,OAAO,GAAG,qBAAqB;oCAC9C;;sDAEA,KAACzE;4CACC6C,SAAQ;4CACRT,IAAI;gDACFa,SAAS;gDACTG,IAAI;gDACJsB,YAAYpB,IAAIiB,OAAO,GAAG,mBAAmBvC;gDAC7CuB,OAAOD,IAAImB,OAAO,GAAG,iBAAiB;4CACxC;sDAECnB,IAAIqB,IAAI;;sDAEX,MAAC5E;4CAAMmC,SAAS;;gDACb4B,MAAMT,GAAG,CAAC,CAACrC,qBACV,KAACnB;wDAEC6C,SAAS,IAAM9B,OAAOI;wDACtBC,OAAOF,UAAUC;wDACjBoB,IAAI;4DACFwC,gBAAgB;4DAChBC,OAAO;4DACPC,IAAI;4DACJC,IAAI;4DACJX,cAAc;4DACdI,SAAS;4DACT,WAAW;gEAAEA,SAAS;4DAAe;wDACvC;kEAEA,cAAA,KAACxE;4DACC6C,SAAQ;4DACRmC,MAAM;4DACN5C,IAAI;gEACFyC,OAAO;gEACP9B,WAAW;gEACXkC,gBACEjE,KAAKI,MAAM,KAAK,SAAS,iBAAiBY;gEAC5CuB,OACEvC,KAAKI,MAAM,KAAK,SACZ,mBACAkC,IAAI4B,KAAK,IAAI7D,QACX,eACA;4DACV;sEAECL,KAAKC,KAAK;;uDA7BRD,KAAKmE,GAAG;gDAiChBnB,SAAS,kBACR,KAAChE;oDAAW6C,SAAQ;oDAAUU,OAAM;8DACjC,CAAC,CAAC,EAAES,OAAO,KAAK,CAAC;qDAElB;;;;mCA7DDV,IAAIO,OAAO;4BAiEtB;;;;;YAULlC,QAAQyD,OAAO,CAACnB,MAAM,IAAItC,QAAQ0D,SAAS,CAACpB,MAAM,IAAIpD,0BACrD,KAACb;gBAAW6C,SAAQ;gBAAUU,OAAM;0BACjC;oBACC5B,QAAQ0D,SAAS,CAACpB,MAAM,GACpB,GAAGtC,QAAQ0D,SAAS,CAACpB,MAAM,CAAC,uBAAuB,CAAC,GACpD;oBACJtC,QAAQyD,OAAO,CAACnB,MAAM,GAClB,GAAGtC,QAAQyD,OAAO,CAACnB,MAAM,CAAC,iBAAiB,CAAC,GAC5C;oBACJpD,YAAY,gDAAgD;iBAC7D,CACEyE,MAAM,CAACC,SACPC,IAAI,CAAC;iBAER;;;AAGV;AACA/E,cAAcgF,WAAW,GAAG;AAE5B,eAAehF,cAAa"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/components/tasks-calendar.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 { type CrmTaskPicklists, crmTaskPicklistLabels } from '@aglyn/aglyn'\nimport { mdiChevronLeft, mdiChevronRight } from '@aglyn/shared-data-mdi'\nimport { MdiIcon } from '@aglyn/shared-ui-jsx'\nimport { Box, Button, ButtonBase, IconButton, Stack, Typography } from '@mui/material'\nimport { useMemo, useState } from 'react'\nimport type { CrmTaskRow } from '../hooks/use-crm-tasks'\nimport {\n bucketTasksByCalendarDay,\n CALENDAR_DAY_LABELS,\n CALENDAR_WEEK_DAYS,\n shiftCalendarMonth,\n taskCalendarMonth,\n} from '../model/task-calendar'\n\n/** How many tasks a square lists before folding the rest into a count. */\nconst TASKS_PER_DAY = 3\n\nexport interface TasksCalendarProps {\n /** The window the list is holding — the calendar reads no more than it. */\n tasks: readonly CrmTaskRow[]\n /** Open one task's drawer, the same drawer a row opens. */\n onOpen: (task: CrmTaskRow) => void\n /** The list's window filled, so neither surface is showing everything. */\n truncated?: boolean\n /** The org's task lists, which name each entry's type and status on hover (AGL-3517). */\n picklists?: CrmTaskPicklists\n}\n\n/**\n * A MONTH OF TASKS, BY DUE DATE (AGL-2662).\n *\n * The Tasks list answers \"what is on my plate\"; this answers \"what does\n * next week look like\", which a list ordered by due date can only imply.\n *\n * ## Drawn over the list's own window, and never over a second read\n *\n * The rows are the ones the section already holds. A calendar that fetched\n * its own month would be a second listener over the same collection, would\n * disagree with the list whenever the two windows differed, and would put\n * a read behind every page of the arrows. What it costs instead is honesty:\n * the grid can only place what the window carries, so the tasks due in\n * other months are counted under it rather than implied to be absent.\n *\n * ## Hand-built rather than a calendar dependency\n *\n * Seven columns of MUI boxes and the local-day arithmetic in\n * `task-calendar.ts`. The one calendar package already installed\n * (`@mui/x-date-pickers`) draws a date PICKER — a grid whose squares are\n * choices, not content — so it answers a different question, and a\n * scheduling library for one surface is a dependency the whole console\n * would carry.\n */\nexport function TasksCalendar(props: TasksCalendarProps) {\n const { tasks, onOpen, truncated, picklists } = props\n /** \"Call Ada · Call · In Progress\" — the entry's type and status on hover. */\n const hoverText = (task: CrmTaskRow) => {\n if (!picklists) return task.title\n const labels = crmTaskPicklistLabels(task, picklists)\n return `${task.title} · ${labels.type} · ${labels.status}`\n }\n /*\n * The clock is read ONCE, at mount. A component that read `Date.now()`\n * per render would move the today square under a reader who left the tab\n * open across midnight, without any state having changed — and the grid\n * is a pure function of this, so what is drawn can be pinned.\n */\n const [nowMs] = useState(() => Date.now())\n const [anchorMs, setAnchorMs] = useState(() => nowMs)\n\n const month = useMemo(\n () => taskCalendarMonth(anchorMs, nowMs),\n [anchorMs, nowMs],\n )\n const buckets = useMemo(\n () => bucketTasksByCalendarDay(tasks, (task) => task.dueAtMs, month),\n [tasks, month],\n )\n const label = useMemo(\n () =>\n month.monthDate.toLocaleDateString(undefined, {\n month: 'long',\n year: 'numeric',\n }),\n [month],\n )\n\n return (\n <Stack spacing={1.5}>\n <Stack\n useFlexGap\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', flexWrap: 'wrap', rowGap: 1 }}\n >\n <IconButton\n size=\"small\"\n aria-label=\"Previous month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, -1))}\n >\n <MdiIcon path={mdiChevronLeft.path} size={0.9} />\n </IconButton>\n <Typography variant=\"subtitle1\" sx={{ minWidth: 160, textAlign: 'center' }}>\n {label}\n </Typography>\n <IconButton\n size=\"small\"\n aria-label=\"Next month\"\n onClick={() => setAnchorMs((current) => shiftCalendarMonth(current, 1))}\n >\n <MdiIcon path={mdiChevronRight.path} size={0.9} />\n </IconButton>\n <Button size=\"small\" onClick={() => setAnchorMs(nowMs)}>\n {'Today'}\n </Button>\n </Stack>\n\n <Box sx={{ overflowX: 'auto' }}>\n <Box sx={{ minWidth: 640 }}>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n mb: 0.5,\n }}\n >\n {CALENDAR_DAY_LABELS.map((day) => (\n <Typography\n key={day}\n variant=\"caption\"\n color=\"text.secondary\"\n sx={{ textAlign: 'center' }}\n >\n {day}\n </Typography>\n ))}\n </Box>\n <Box\n sx={{\n display: 'grid',\n gridTemplateColumns: `repeat(${CALENDAR_WEEK_DAYS}, 1fr)`,\n gap: 0.5,\n }}\n >\n {month.weeks.flat().map((day) => {\n const held = buckets.byDay.get(day.startMs) ?? []\n const shown = held.slice(0, TASKS_PER_DAY)\n const folded = held.length - shown.length\n return (\n <Box\n key={day.startMs}\n sx={{\n minHeight: 96,\n p: 0.75,\n borderRadius: 1,\n border: 1,\n borderColor: day.isToday ? 'primary.main' : 'divider',\n bgcolor: day.inMonth ? 'background.paper' : 'action.hover',\n }}\n >\n <Typography\n variant=\"caption\"\n sx={{\n display: 'block',\n mb: 0.5,\n fontWeight: day.isToday ? 'fontWeightBold' : undefined,\n color: day.inMonth ? 'text.primary' : 'text.disabled',\n }}\n >\n {day.date}\n </Typography>\n <Stack spacing={0.25}>\n {shown.map((task) => (\n <ButtonBase\n key={task.$id}\n onClick={() => onOpen(task)}\n title={hoverText(task)}\n sx={{\n justifyContent: 'flex-start',\n width: '100%',\n px: 0.5,\n py: 0.25,\n borderRadius: 0.5,\n bgcolor: 'action.selected',\n '&:hover': { bgcolor: 'action.focus' },\n }}\n >\n <Typography\n variant=\"caption\"\n noWrap\n sx={{\n width: '100%',\n textAlign: 'left',\n textDecoration:\n task.status === 'done' ? 'line-through' : undefined,\n color:\n task.status === 'done'\n ? 'text.secondary'\n : day.endMs <= nowMs\n ? 'error.main'\n : 'text.primary',\n }}\n >\n {task.title}\n </Typography>\n </ButtonBase>\n ))}\n {folded > 0 ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`+${folded} more`}\n </Typography>\n ) : null}\n </Stack>\n </Box>\n )\n })}\n </Box>\n </Box>\n </Box>\n\n {/*\n What the grid could not place. Said plainly, because a month with\n nothing on it reads as \"nothing is due\" and the honest reading here\n is often \"the window's tasks are somewhere else\".\n */}\n {buckets.undated.length || buckets.elsewhere.length || truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {[\n buckets.elsewhere.length\n ? `${buckets.elsewhere.length} due outside this month`\n : '',\n buckets.undated.length\n ? `${buckets.undated.length} with no due date`\n : '',\n truncated ? 'and the view is showing only its first page' : '',\n ]\n .filter(Boolean)\n .join(' · ')}\n </Typography>\n ) : null}\n </Stack>\n )\n}\nTasksCalendar.displayName = 'TasksCalendar'\n\nexport default TasksCalendar\n"],"names":["crmTaskPicklistLabels","mdiChevronLeft","mdiChevronRight","MdiIcon","Box","Button","ButtonBase","IconButton","Stack","Typography","useMemo","useState","bucketTasksByCalendarDay","CALENDAR_DAY_LABELS","CALENDAR_WEEK_DAYS","shiftCalendarMonth","taskCalendarMonth","TASKS_PER_DAY","TasksCalendar","props","tasks","onOpen","truncated","picklists","hoverText","task","title","labels","type","status","nowMs","Date","now","anchorMs","setAnchorMs","month","buckets","dueAtMs","label","monthDate","toLocaleDateString","undefined","year","spacing","useFlexGap","direction","sx","alignItems","flexWrap","rowGap","size","aria-label","onClick","current","path","variant","minWidth","textAlign","overflowX","display","gridTemplateColumns","gap","mb","map","day","color","weeks","flat","held","byDay","get","startMs","shown","slice","folded","length","minHeight","p","borderRadius","border","borderColor","isToday","bgcolor","inMonth","fontWeight","date","justifyContent","width","px","py","noWrap","textDecoration","endMs","$id","undated","elsewhere","filter","Boolean","join","displayName"],"mappings":"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"}
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { ListFilterField } from '@aglyn/shared-
|
|
18
|
-
import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-
|
|
17
|
+
import type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
18
|
+
import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
19
19
|
export declare const COMPANY_LIST_FILTER_FIELDS: readonly ListFilterField[];
|
|
20
20
|
/** The picklist columns the list filters by, each with the list its choices come from. */
|
|
21
21
|
export declare const COMPANY_PICKLIST_FILTERS: readonly [{
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import { CRM_NEXT_ACTIVITY_FILTER_FIELD } from "../
|
|
16
|
+
*/ import { CRM_NEXT_ACTIVITY_FILTER_FIELD } from "../model/crm-next-activity.js";
|
|
17
17
|
import { CRM_LIST_SEARCH } from "../model/crm-list-query.js";
|
|
18
18
|
/*
|
|
19
19
|
* Companies (`orgs/{orgId}/companies`, read scoped to a host) — every clause
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/company-filters.ts"],"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\nimport type { ListFilterField } from '@aglyn/shared-
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/company-filters.ts"],"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\nimport type { ListFilterField } from '@aglyn/shared-util-tools/list-query/list-filter'\n\nimport { CRM_NEXT_ACTIVITY_FILTER_FIELD } from '../model/crm-next-activity'\nimport type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport { CRM_LIST_SEARCH } from '../model/crm-list-query'\n\n/*\n * Companies (`orgs/{orgId}/companies`, read scoped to a host) — every clause\n * and the search word on the list's one query, newest change first\n * (AGL-3321).\n *\n * The search box reads `searchTokens`, the word prefixes of the name and\n * the domain every company writer stamps (`crmCompanyListFields`). Under a\n * site the scope clause (`visibleTo array-contains-any`) is the query's one\n * array clause, and the search folds into it (`scopedSearchTokens`) for a\n * reader who may drop it.\n *\n * The name is also an exact match and a PREFIX range over `nameLower` — the\n * list's one range, which then orders the list by name. The owner is a\n * choice from the roster, and \"No next activity\" asks `nextTaskAtMs ==\n * null`. Each equality rides one `(field, updatedAt DESC)` composite, and\n * one `(field, nameLower ASC)` beside it for the prefix.\n *\n * Type, Industry and Rating (AGL-3514) are choices from the org's lists,\n * asked of the KEY each writer stores beside the label (`typeKey`,\n * `industryKey`, `ratingKey` — see `crmCompanyListFields`): a choice's\n * value is the key, its caption the label, so the query compares what\n * the record keyed rather than how the label happens to be cased.\n */\nexport const COMPANY_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n presence: 'always',\n operators: ['equals', 'startsWith'],\n },\n {\n column: 'ownerUid',\n kind: 'exact',\n path: 'ownerUid',\n operators: ['equals', 'isAnyOf'],\n },\n CRM_NEXT_ACTIVITY_FILTER_FIELD,\n { column: 'type', kind: 'exact', path: 'typeKey', operators: ['equals', 'isAnyOf'] },\n { column: 'industry', kind: 'exact', path: 'industryKey', operators: ['equals', 'isAnyOf'] },\n { column: 'rating', kind: 'exact', path: 'ratingKey', operators: ['equals', 'isAnyOf'] },\n]\n\n/** The picklist columns the list filters by, each with the list its choices come from. */\nexport const COMPANY_PICKLIST_FILTERS = [\n { column: 'type', picklistId: 'accountType', header: 'Type' },\n { column: 'industry', picklistId: 'industry', header: 'Industry' },\n { column: 'rating', picklistId: 'rating', header: 'Rating' },\n] as const\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the name, beside the scope clause — the prefix range the\n * `(field, nameLower)` composites already serve.\n */\nexport const COMPANY_PREFIX_SEARCH = {\n field: COMPANY_LIST_FILTER_FIELDS[0],\n notice: 'Search matches the start of a company’s name for access limited to specific sites.',\n}\n\n/** The list's orders: newest change first, and by name under a prefix. */\nexport const COMPANY_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc' },\n { path: 'nameLower', direction: 'asc' },\n]\n\nexport const COMPANY_LIST_DECLARATION: ListQueryDeclaration = {\n fields: COMPANY_LIST_FILTER_FIELDS,\n sorts: COMPANY_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n"],"names":["CRM_NEXT_ACTIVITY_FILTER_FIELD","CRM_LIST_SEARCH","COMPANY_LIST_FILTER_FIELDS","column","kind","path","lowerPath","presence","operators","COMPANY_PICKLIST_FILTERS","picklistId","header","COMPANY_PREFIX_SEARCH","field","notice","COMPANY_LIST_SORTS","direction","COMPANY_LIST_DECLARATION","fields","sorts","search"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID,SAASA,8BAA8B,QAAQ,gCAA4B;AAE3E,SAASC,eAAe,QAAQ,6BAAyB;AAEzD;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,MAAMC,6BAAyD;IACpE;QACEC,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNC,WAAW;QACXC,UAAU;QACVC,WAAW;YAAC;YAAU;SAAa;IACrC;IACA;QACEL,QAAQ;QACRC,MAAM;QACNC,MAAM;QACNG,WAAW;YAAC;YAAU;SAAU;IAClC;IACAR;IACA;QAAEG,QAAQ;QAAQC,MAAM;QAASC,MAAM;QAAWG,WAAW;YAAC;YAAU;SAAU;IAAC;IACnF;QAAEL,QAAQ;QAAYC,MAAM;QAASC,MAAM;QAAeG,WAAW;YAAC;YAAU;SAAU;IAAC;IAC3F;QAAEL,QAAQ;QAAUC,MAAM;QAASC,MAAM;QAAaG,WAAW;YAAC;YAAU;SAAU;IAAC;CACxF,CAAA;AAED,wFAAwF,GACxF,OAAO,MAAMC,2BAA2B;IACtC;QAAEN,QAAQ;QAAQO,YAAY;QAAeC,QAAQ;IAAO;IAC5D;QAAER,QAAQ;QAAYO,YAAY;QAAYC,QAAQ;IAAW;IACjE;QAAER,QAAQ;QAAUO,YAAY;QAAUC,QAAQ;IAAS;CAC5D,CAAS;AAEV;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB;IACnCC,OAAOX,0BAA0B,CAAC,EAAE;IACpCY,QAAQ;AACV,EAAC;AAED,wEAAwE,GACxE,OAAO,MAAMC,qBAA+C;IAC1D;QAAEV,MAAM;QAAaW,WAAW;IAAO;IACvC;QAAEX,MAAM;QAAaW,WAAW;IAAM;CACvC,CAAA;AAED,OAAO,MAAMC,2BAAiD;IAC5DC,QAAQhB;IACRiB,OAAOJ;IACPK,QAAQnB;AACV,EAAC"}
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type ContactFieldDefinition } from '@aglyn/aglyn';
|
|
18
|
-
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-
|
|
19
|
-
import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-
|
|
17
|
+
import { type ContactFieldDefinition } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
19
|
+
import type { ListQueryDeclaration, ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
20
20
|
import { type CrmClauseAsked } from '../model/crm-list-query';
|
|
21
21
|
/** The column a contact's lead source filters as (AGL-3511). */
|
|
22
22
|
export declare const CONTACT_LEAD_SOURCE_COLUMN = "leadSource";
|
|
@@ -14,7 +14,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { CRM_CONTACT_VIEW_FIELDS, CRM_FACET_KEY_ANY_GROUP, crmContactCustomColumn, crmFacetKey, crmFacetKeyValue
|
|
17
|
+
*/ import { CRM_CONTACT_VIEW_FIELDS, CRM_FACET_KEY_ANY_GROUP, crmContactCustomColumn, crmFacetKey, crmFacetKeyValue } from "@aglyn/aglyn/app-utils/crm";
|
|
18
|
+
import { EMAIL_STATE_STATUSES } from "@aglyn/aglyn/app-utils/email-state";
|
|
18
19
|
import { CRM_LIST_SEARCH, crmAnyOf, crmClauseValues } from "../model/crm-list-query.js";
|
|
19
20
|
/*==========================================
|
|
20
21
|
* CONTACTS (`orgs/{orgId}/contacts`, read scoped to a host) — WHAT THE
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/contact-filters.ts"],"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\nimport {\n type ContactFieldDefinition,\n CRM_CONTACT_VIEW_FIELDS,\n CRM_FACET_KEY_ANY_GROUP,\n type CrmFacetKeyField,\n crmContactCustomColumn,\n crmFacetKey,\n crmFacetKeyValue,\n EMAIL_STATE_STATUSES,\n} from '@aglyn/aglyn'\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-ui-jsx/const/list-filter'\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n} from '../model/crm-list-query'\n\n/*==========================================\n * CONTACTS (`orgs/{orgId}/contacts`, read scoped to a host) — WHAT THE\n * PANEL OFFERS, AND WHAT THE QUERY ASKS (AGL-3321).\n *\n * Every clause and the search word are on the list's one Firestore query,\n * newest change first, paged by it. A contact is one row shared by every\n * holder, and most of what a holder filters by — the owner, the stage, the\n * sources, the company, the tags, a custom value, whether the person has\n * bought — lives on that holder's FACET, a path per holder no index can\n * reach. So every writer stamps `facetKeys` (`crmContactFacetKeys` in\n * `@aglyn/aglyn`): `{group}:{field}={value}` for each holder's value,\n * `{group}:{field}` for \"has one\", and the same under `*` for any holder.\n * Under a site a facet clause asks the viewing holder's key; at the\n * organization level, any holder's.\n *\n * What one query holds decides the rest:\n *\n * - ONE array clause. The scope clause (`visibleTo array-contains-any`),\n * the search (`searchTokens`), a name word (`nameTokens`), a form\n * (`formIds`) and a facet clause (`facetKeys`) all are one. The search\n * folds into the scope clause for a reader who may drop it; a facet or\n * form clause stands in the scope clause's place for that reader, since\n * only a record the viewing holder keeps carries its key. Two of them\n * at once is refused by name.\n * - ONE range. `updatedAt`, the field the list is ordered by, composes\n * with anything; Created and a name or address prefix order the list by\n * their own field and so stand alone beside the scope clause\n * (`contactSoloClause`), served by the `(visibleTo, field)` composites.\n * - equalities (the address, the site, the verdict, \"No next activity\")\n * compose freely, one `(field, updatedAt DESC)` composite each.\n *\n * The per-holder FIGURES (orders, lifetime value) live on each holder's\n * facet, where no range reaches — a top-level `ordersCount` was never\n * written — so they offer \"is not empty\" alone: \"has bought\".\n *\n * A collaborator's search is the start of the name (`CONTACT_PREFIX_SEARCH`).\n *=========================================*/\n\n/**\n * A word of the canonical name (`nameTokens`), the whole of it, or its start\n * or end — `nameLower`, which every contact carries, and `nameReversed`.\n */\nconst NAME_FIELD: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n reversedPath: 'nameReversed',\n operators: ['contains', 'equals', 'startsWith', 'endsWith'],\n}\n\n/**\n * The address. `normalizeContactEmail` lower-cases before every write, so the\n * stored value IS its own normalized key.\n */\nconst EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals', 'startsWith'],\n}\n\n/** When the person was first captured — a range the list is then ordered by. */\nconst CREATED_FIELD: ListFilterField = {\n column: CRM_CONTACT_VIEW_FIELDS.createdAt,\n kind: 'date',\n path: 'createdAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\n/**\n * The forms a person came in through — the top-level `formIds` mirror of the\n * interactions' `formId` (AGL-2612), matched whole and as typed because a\n * form id is minted with mixed case. The form's own page links here with its\n * id; nobody types one.\n */\nconst FORM_FIELD: ListFilterField = {\n column: 'formIds',\n kind: 'text',\n path: 'formIds',\n tokensPath: 'formIds',\n verbatimTokens: true,\n operators: ['contains'],\n}\n\n/** The column a contact's lead source filters as (AGL-3511). */\nexport const CONTACT_LEAD_SOURCE_COLUMN = 'leadSource'\n\n/** What the Filters panel offers on a contact, in the grid's grammar. */\nexport const CONTACT_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n {\n // A holder's tags, matched whole and case-insensitively.\n column: CRM_CONTACT_VIEW_FIELDS.tags,\n kind: 'exact',\n path: 'tags',\n operators: ['contains', 'isAnyOf', 'isNotEmpty'],\n },\n FORM_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n // Per-holder figures: \"has bought\", never a range across holders.\n {\n column: CRM_CONTACT_VIEW_FIELDS.orders,\n kind: 'number',\n path: 'ordersCount',\n operators: ['isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.ltv,\n kind: 'number',\n path: 'ltvCents',\n operators: ['isNotEmpty'],\n },\n CREATED_FIELD,\n {\n column: CRM_CONTACT_VIEW_FIELDS.updatedAt,\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n /*\n * \"No next activity\" (AGL-2661): `nextTaskAtMs == null`, which every\n * contact carries from its creation until a task is scheduled against it.\n */\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n /*\n * THE FACET FIELDS (AGL-2617). The values are picked, not typed — a uid\n * from the roster, a stage from the fixed list, a source from its labels,\n * a company from the picker — so `equals` and `isAnyOf` are what they\n * offer; the section supplies the choices. The column names are the\n * contract `CRM_CONTACT_VIEW_FIELDS` states, because the dynamic-list\n * translator reads a view by them. The paths are the flattened ROW'S.\n */\n {\n column: CRM_CONTACT_VIEW_FIELDS.owner,\n kind: 'exact',\n path: 'ownerUid',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.stage,\n kind: 'exact',\n path: 'lifecycleStage',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n // The `sources` presence map, matched on its keys — see `keysOf`.\n column: CRM_CONTACT_VIEW_FIELDS.source,\n kind: 'exact',\n path: 'sources',\n keysOf: true,\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.company,\n kind: 'exact',\n path: 'companyId',\n operators: ['equals', 'isNotEmpty'],\n },\n /*\n * A holder's lead source (AGL-3511), picked from the org's Lead source\n * values and asked through its `leadSource` facet key — the label as the\n * picklist compares it.\n */\n {\n column: CONTACT_LEAD_SOURCE_COLUMN,\n kind: 'exact',\n path: 'leadSource',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n /*\n * The verdict on the address (AGL-3245), asked through `emailStatus` —\n * `none` for a person nothing has been said about, which is what \"is\n * empty\" asks.\n */\n {\n column: 'emailState',\n kind: 'exact',\n path: 'emailState.status',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n },\n]\n\n/**\n * One filter field per active custom contact field (AGL-2617). A custom\n * value lives under the holder's `custom` map, so it is asked through the\n * holder's facet key: equality on a picked, typed or ticked value, and\n * \"is not empty\". Its column is the one the table shows it under —\n * `crmContactCustomColumn(key)` — so a clause and a column agree on the name.\n */\nexport function contactCustomFilterFields(\n definitions: readonly Pick<\n ContactFieldDefinition,\n 'key' | 'type' | 'retiredAt'\n >[],\n): ListFilterField[] {\n return definitions\n .filter((definition) => !definition.retiredAt)\n .map((definition): ListFilterField => {\n const column = crmContactCustomColumn(definition.key)\n const path = `custom.${definition.key}`\n switch (definition.type) {\n case 'number':\n return {\n column,\n kind: 'number',\n path,\n operators: ['=', 'isNotEmpty'],\n }\n case 'date':\n return { column, kind: 'date', path, operators: ['isNotEmpty'] }\n case 'checkbox':\n return { column, kind: 'boolean', path }\n case 'select':\n return {\n column,\n kind: 'exact',\n path,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n }\n default:\n return {\n column,\n kind: 'text',\n path,\n lowerPath: path,\n operators: ['equals', 'isNotEmpty'],\n }\n }\n })\n}\n\n/** The custom fields' headers, keyed by the column each filters as. */\nexport function contactCustomFilterHeaders(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[],\n): Record<string, string> {\n return Object.fromEntries(\n definitions.map((definition) => [\n crmContactCustomColumn(definition.key),\n definition.label || definition.key,\n ]),\n )\n}\n\n/**\n * How every filterable contact field reads — on a chip, in the add-filter\n * picker, and as the header of a filter-only hidden column.\n */\nexport const CONTACT_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Contact',\n email: 'Email',\n tags: 'Tags',\n formIds: 'Form ID',\n hostId: 'Site ID',\n ordersCount: 'Orders',\n ltvCents: 'Lifetime value (cents)',\n createdAt: 'Created',\n updatedAt: 'Updated',\n nextTaskAtMs: 'Next activity',\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'Owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'Stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'Source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'Company',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'Lead source',\n emailState: 'Email',\n}\n\n/*------------------------------------------\n * THE QUERY THE CONTACTS LIST RUNS.\n *-----------------------------------------*/\n\n/** The list's one order: newest change first. */\nexport const CONTACT_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc' },\n]\n\n/** The stored array a facet clause is asked of. */\nexport const CONTACT_FACET_KEYS_PATH = 'facetKeys'\n\n/** The fields the Contacts query asks — each a stored field, as named. */\nexport const CONTACT_QUERY_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n FORM_FIELD,\n CREATED_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n {\n column: 'emailStatus',\n kind: 'exact',\n path: 'emailStatus',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CONTACT_FACET_KEYS_PATH,\n kind: 'exact',\n path: CONTACT_FACET_KEYS_PATH,\n tokensPath: CONTACT_FACET_KEYS_PATH,\n operators: ['contains', 'isAnyOf'],\n },\n]\n\nexport const CONTACT_LIST_DECLARATION: ListQueryDeclaration = {\n fields: CONTACT_QUERY_FIELDS,\n sorts: CONTACT_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The grid fields a stored clause passes to the query unchanged. */\nconst AS_STORED = new Set([\n 'name',\n 'email',\n 'formIds',\n 'hostId',\n 'createdAt',\n 'updatedAt',\n 'nextTaskAtMs',\n])\n\n/**\n * The ranges that stand ALONE beside the scope clause: Created and a name\n * or address prefix each order the list by their own field, which the\n * `(visibleTo, field)` composites serve and nothing beside them. Updated is\n * not one: the list is ordered by it already, so it composes freely.\n */\nexport function contactSoloClause(clause: ListFilterRequest): boolean {\n if (clause.field === 'createdAt') return true\n if (clause.field === 'name') return clause.op === 'startsWith' || clause.op === 'endsWith'\n return clause.field === 'email' && clause.op === 'startsWith'\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the contact's name, beside the scope clause.\n */\nexport const CONTACT_PREFIX_SEARCH = {\n field: NAME_FIELD,\n notice: 'Search matches the start of a contact’s name for access limited to specific sites.',\n}\n\n/** The facet field each grid column is keyed under. */\nconst FACET_FIELDS: Readonly<Record<string, CrmFacetKeyField>> = {\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'company',\n [CRM_CONTACT_VIEW_FIELDS.tags]: 'tag',\n [CRM_CONTACT_VIEW_FIELDS.orders]: 'orders',\n [CRM_CONTACT_VIEW_FIELDS.ltv]: 'ltv',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'leadSource',\n}\n\n/** Who is asking: the viewing holder, and whether their query may drop the scope clause. */\nexport interface ContactQueryReader {\n /** The viewing holder's group id under a site; `null` at the organization level. */\n groupId: string | null\n /**\n * Whether the reader's query may stand a facet or form clause in the\n * scope clause's place (`useCrmFoldsScope`) — always at the organization\n * level, where there is no scope clause.\n */\n foldsScope: boolean\n}\n\n/** Why a reader whose access is some sites cannot filter by a holder's field. */\nconst LIMITED_ACCESS =\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it'\n\n/** The facet field a grid column names, or null for one that is not a facet field. */\nfunction facetFieldOf(column: string): CrmFacetKeyField | null {\n if (FACET_FIELDS[column]) return FACET_FIELDS[column]\n if (column.startsWith(CRM_CONTACT_VIEW_FIELDS.custom)) {\n return `custom.${column.slice(CRM_CONTACT_VIEW_FIELDS.custom.length)}`\n }\n return null\n}\n\n/**\n * One grid clause as the clause the Contacts query asks — see the block at\n * the top. A clause a query cannot serve for this reader is refused by\n * name, never matched over the rows a page loaded.\n */\nexport function contactQueryClause(\n clause: ListFilterRequest,\n reader: ContactQueryReader,\n): CrmClauseAsked {\n if (AS_STORED.has(clause.field)) {\n if (clause.field === 'formIds' && !reader.foldsScope)\n return { refused: LIMITED_ACCESS }\n return { field: clause.field, op: clause.op, value: clause.value }\n }\n if (clause.field === 'emailState') {\n if (clause.op === 'isEmpty')\n return { field: 'emailStatus', op: 'equals', value: 'none' }\n if (clause.op === 'isNotEmpty')\n return crmAnyOf('emailStatus', EMAIL_STATE_STATUSES)\n const values = crmClauseValues(clause)\n if (\n values.some(\n (value) => !(EMAIL_STATE_STATUSES as readonly string[]).includes(value),\n )\n ) {\n return { refused: 'pick one of the choices' }\n }\n return crmAnyOf('emailStatus', values)\n }\n const facet = facetFieldOf(clause.field)\n if (!facet) return { refused: 'this list does not filter by that' }\n if (!reader.foldsScope) return { refused: LIMITED_ACCESS }\n const group = reader.groupId ?? CRM_FACET_KEY_ANY_GROUP\n if (clause.op === 'isNotEmpty') {\n return {\n field: CONTACT_FACET_KEYS_PATH,\n op: 'contains',\n value: crmFacetKey(group, facet),\n }\n }\n const keys: string[] = []\n for (const value of crmClauseValues(clause)) {\n const key = crmFacetKeyValue(clause.op === '=' ? Number(value) : value)\n if (key === null) return { refused: 'no value yet' }\n keys.push(crmFacetKey(group, facet, key))\n }\n const asked = crmAnyOf(CONTACT_FACET_KEYS_PATH, keys)\n // One key is an `array-contains`; several, an `array-contains-any`.\n return 'refused' in asked || asked.op === 'isAnyOf'\n ? asked\n : { ...asked, op: 'contains' }\n}\n\n/**\n * Whether an asked clause stands in for the scope clause: a facet key of\n * the viewing holder, which only a record that holder keeps carries, or a\n * form, which only its site's captures carry.\n */\nexport function contactClauseImpliesScope(groupId: string | null) {\n return (clause: ListFilterRequest): boolean => {\n if (clause.field === 'formIds') return true\n if (clause.field !== CONTACT_FACET_KEYS_PATH || !groupId) return false\n return crmClauseValues(clause).every((key) => key.startsWith(`${groupId}:`))\n }\n}\n\n/**\n * The query shapes the Contacts list can send, as declarations the index\n * pin enumerates (AGL-3321): the list's own, without its solo ranges, whose\n * every equality rides `(field, updatedAt DESC)`; and each solo range beside\n * the scope clause alone.\n */\nexport function contactIndexShapes(): ListQueryDeclaration[] {\n const soloOps = new Set(['startsWith', 'endsWith'])\n const main: ListQueryDeclaration = {\n ...CONTACT_LIST_DECLARATION,\n fields: CONTACT_QUERY_FIELDS.filter((field) => field !== CREATED_FIELD).map((field) =>\n field.operators\n ? { ...field, operators: field.operators.filter((op) => !soloOps.has(op)) }\n : field,\n ),\n }\n const solo: ListQueryDeclaration = {\n fields: [\n { ...NAME_FIELD, operators: ['startsWith', 'endsWith'] },\n { ...EMAIL_FIELD, operators: ['startsWith'] },\n CREATED_FIELD,\n ],\n sorts: [],\n }\n return [main, solo]\n}\n"],"names":["CRM_CONTACT_VIEW_FIELDS","CRM_FACET_KEY_ANY_GROUP","crmContactCustomColumn","crmFacetKey","crmFacetKeyValue","EMAIL_STATE_STATUSES","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","NAME_FIELD","column","kind","path","lowerPath","tokensPath","reversedPath","operators","EMAIL_FIELD","presence","CREATED_FIELD","createdAt","FORM_FIELD","verbatimTokens","CONTACT_LEAD_SOURCE_COLUMN","CONTACT_LIST_FILTER_FIELDS","tags","orders","ltv","updatedAt","storedAs","owner","stage","source","keysOf","company","contactCustomFilterFields","definitions","filter","definition","retiredAt","map","key","type","contactCustomFilterHeaders","Object","fromEntries","label","CONTACT_LIST_FILTER_HEADERS","name","email","formIds","hostId","ordersCount","ltvCents","nextTaskAtMs","emailState","CONTACT_LIST_SORTS","direction","CONTACT_FACET_KEYS_PATH","CONTACT_QUERY_FIELDS","CONTACT_LIST_DECLARATION","fields","sorts","search","AS_STORED","Set","contactSoloClause","clause","field","op","CONTACT_PREFIX_SEARCH","notice","FACET_FIELDS","LIMITED_ACCESS","facetFieldOf","startsWith","custom","slice","length","contactQueryClause","reader","has","foldsScope","refused","value","values","some","includes","facet","group","groupId","keys","Number","push","asked","contactClauseImpliesScope","every","contactIndexShapes","soloOps","main","solo"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/constants/contact-filters.ts"],"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\nimport {\n type ContactFieldDefinition,\n CRM_CONTACT_VIEW_FIELDS,\n CRM_FACET_KEY_ANY_GROUP,\n type CrmFacetKeyField,\n crmContactCustomColumn,\n crmFacetKey,\n crmFacetKeyValue,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { EMAIL_STATE_STATUSES } from '@aglyn/aglyn/app-utils/email-state'\nimport type {\n ListFilterField,\n ListFilterRequest,\n} from '@aglyn/shared-util-tools/list-query/list-filter'\nimport type {\n ListQueryDeclaration,\n ListQuerySort,\n} from '@aglyn/shared-util-tools/list-query/list-query-plan'\nimport {\n CRM_LIST_SEARCH,\n type CrmClauseAsked,\n crmAnyOf,\n crmClauseValues,\n} from '../model/crm-list-query'\n\n/*==========================================\n * CONTACTS (`orgs/{orgId}/contacts`, read scoped to a host) — WHAT THE\n * PANEL OFFERS, AND WHAT THE QUERY ASKS (AGL-3321).\n *\n * Every clause and the search word are on the list's one Firestore query,\n * newest change first, paged by it. A contact is one row shared by every\n * holder, and most of what a holder filters by — the owner, the stage, the\n * sources, the company, the tags, a custom value, whether the person has\n * bought — lives on that holder's FACET, a path per holder no index can\n * reach. So every writer stamps `facetKeys` (`crmContactFacetKeys` in\n * `@aglyn/aglyn`): `{group}:{field}={value}` for each holder's value,\n * `{group}:{field}` for \"has one\", and the same under `*` for any holder.\n * Under a site a facet clause asks the viewing holder's key; at the\n * organization level, any holder's.\n *\n * What one query holds decides the rest:\n *\n * - ONE array clause. The scope clause (`visibleTo array-contains-any`),\n * the search (`searchTokens`), a name word (`nameTokens`), a form\n * (`formIds`) and a facet clause (`facetKeys`) all are one. The search\n * folds into the scope clause for a reader who may drop it; a facet or\n * form clause stands in the scope clause's place for that reader, since\n * only a record the viewing holder keeps carries its key. Two of them\n * at once is refused by name.\n * - ONE range. `updatedAt`, the field the list is ordered by, composes\n * with anything; Created and a name or address prefix order the list by\n * their own field and so stand alone beside the scope clause\n * (`contactSoloClause`), served by the `(visibleTo, field)` composites.\n * - equalities (the address, the site, the verdict, \"No next activity\")\n * compose freely, one `(field, updatedAt DESC)` composite each.\n *\n * The per-holder FIGURES (orders, lifetime value) live on each holder's\n * facet, where no range reaches — a top-level `ordersCount` was never\n * written — so they offer \"is not empty\" alone: \"has bought\".\n *\n * A collaborator's search is the start of the name (`CONTACT_PREFIX_SEARCH`).\n *=========================================*/\n\n/**\n * A word of the canonical name (`nameTokens`), the whole of it, or its start\n * or end — `nameLower`, which every contact carries, and `nameReversed`.\n */\nconst NAME_FIELD: ListFilterField = {\n column: 'name',\n kind: 'text',\n path: 'name',\n lowerPath: 'nameLower',\n tokensPath: 'nameTokens',\n reversedPath: 'nameReversed',\n operators: ['contains', 'equals', 'startsWith', 'endsWith'],\n}\n\n/**\n * The address. `normalizeContactEmail` lower-cases before every write, so the\n * stored value IS its own normalized key.\n */\nconst EMAIL_FIELD: ListFilterField = {\n column: 'email',\n kind: 'text',\n path: 'email',\n lowerPath: 'email',\n presence: 'always',\n operators: ['equals', 'startsWith'],\n}\n\n/** When the person was first captured — a range the list is then ordered by. */\nconst CREATED_FIELD: ListFilterField = {\n column: CRM_CONTACT_VIEW_FIELDS.createdAt,\n kind: 'date',\n path: 'createdAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n}\n\n/**\n * The forms a person came in through — the top-level `formIds` mirror of the\n * interactions' `formId` (AGL-2612), matched whole and as typed because a\n * form id is minted with mixed case. The form's own page links here with its\n * id; nobody types one.\n */\nconst FORM_FIELD: ListFilterField = {\n column: 'formIds',\n kind: 'text',\n path: 'formIds',\n tokensPath: 'formIds',\n verbatimTokens: true,\n operators: ['contains'],\n}\n\n/** The column a contact's lead source filters as (AGL-3511). */\nexport const CONTACT_LEAD_SOURCE_COLUMN = 'leadSource'\n\n/** What the Filters panel offers on a contact, in the grid's grammar. */\nexport const CONTACT_LIST_FILTER_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n {\n // A holder's tags, matched whole and case-insensitively.\n column: CRM_CONTACT_VIEW_FIELDS.tags,\n kind: 'exact',\n path: 'tags',\n operators: ['contains', 'isAnyOf', 'isNotEmpty'],\n },\n FORM_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n // Per-holder figures: \"has bought\", never a range across holders.\n {\n column: CRM_CONTACT_VIEW_FIELDS.orders,\n kind: 'number',\n path: 'ordersCount',\n operators: ['isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.ltv,\n kind: 'number',\n path: 'ltvCents',\n operators: ['isNotEmpty'],\n },\n CREATED_FIELD,\n {\n column: CRM_CONTACT_VIEW_FIELDS.updatedAt,\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n /*\n * \"No next activity\" (AGL-2661): `nextTaskAtMs == null`, which every\n * contact carries from its creation until a task is scheduled against it.\n */\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n /*\n * THE FACET FIELDS (AGL-2617). The values are picked, not typed — a uid\n * from the roster, a stage from the fixed list, a source from its labels,\n * a company from the picker — so `equals` and `isAnyOf` are what they\n * offer; the section supplies the choices. The column names are the\n * contract `CRM_CONTACT_VIEW_FIELDS` states, because the dynamic-list\n * translator reads a view by them. The paths are the flattened ROW'S.\n */\n {\n column: CRM_CONTACT_VIEW_FIELDS.owner,\n kind: 'exact',\n path: 'ownerUid',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.stage,\n kind: 'exact',\n path: 'lifecycleStage',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n {\n // The `sources` presence map, matched on its keys — see `keysOf`.\n column: CRM_CONTACT_VIEW_FIELDS.source,\n kind: 'exact',\n path: 'sources',\n keysOf: true,\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CRM_CONTACT_VIEW_FIELDS.company,\n kind: 'exact',\n path: 'companyId',\n operators: ['equals', 'isNotEmpty'],\n },\n /*\n * A holder's lead source (AGL-3511), picked from the org's Lead source\n * values and asked through its `leadSource` facet key — the label as the\n * picklist compares it.\n */\n {\n column: CONTACT_LEAD_SOURCE_COLUMN,\n kind: 'exact',\n path: 'leadSource',\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n },\n /*\n * The verdict on the address (AGL-3245), asked through `emailStatus` —\n * `none` for a person nothing has been said about, which is what \"is\n * empty\" asks.\n */\n {\n column: 'emailState',\n kind: 'exact',\n path: 'emailState.status',\n presence: 'nullable',\n operators: ['equals', 'isAnyOf', 'isEmpty', 'isNotEmpty'],\n },\n]\n\n/**\n * One filter field per active custom contact field (AGL-2617). A custom\n * value lives under the holder's `custom` map, so it is asked through the\n * holder's facet key: equality on a picked, typed or ticked value, and\n * \"is not empty\". Its column is the one the table shows it under —\n * `crmContactCustomColumn(key)` — so a clause and a column agree on the name.\n */\nexport function contactCustomFilterFields(\n definitions: readonly Pick<\n ContactFieldDefinition,\n 'key' | 'type' | 'retiredAt'\n >[],\n): ListFilterField[] {\n return definitions\n .filter((definition) => !definition.retiredAt)\n .map((definition): ListFilterField => {\n const column = crmContactCustomColumn(definition.key)\n const path = `custom.${definition.key}`\n switch (definition.type) {\n case 'number':\n return {\n column,\n kind: 'number',\n path,\n operators: ['=', 'isNotEmpty'],\n }\n case 'date':\n return { column, kind: 'date', path, operators: ['isNotEmpty'] }\n case 'checkbox':\n return { column, kind: 'boolean', path }\n case 'select':\n return {\n column,\n kind: 'exact',\n path,\n operators: ['equals', 'isAnyOf', 'isNotEmpty'],\n }\n default:\n return {\n column,\n kind: 'text',\n path,\n lowerPath: path,\n operators: ['equals', 'isNotEmpty'],\n }\n }\n })\n}\n\n/** The custom fields' headers, keyed by the column each filters as. */\nexport function contactCustomFilterHeaders(\n definitions: readonly Pick<ContactFieldDefinition, 'key' | 'label'>[],\n): Record<string, string> {\n return Object.fromEntries(\n definitions.map((definition) => [\n crmContactCustomColumn(definition.key),\n definition.label || definition.key,\n ]),\n )\n}\n\n/**\n * How every filterable contact field reads — on a chip, in the add-filter\n * picker, and as the header of a filter-only hidden column.\n */\nexport const CONTACT_LIST_FILTER_HEADERS: Readonly<Record<string, string>> = {\n name: 'Contact',\n email: 'Email',\n tags: 'Tags',\n formIds: 'Form ID',\n hostId: 'Site ID',\n ordersCount: 'Orders',\n ltvCents: 'Lifetime value (cents)',\n createdAt: 'Created',\n updatedAt: 'Updated',\n nextTaskAtMs: 'Next activity',\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'Owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'Stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'Source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'Company',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'Lead source',\n emailState: 'Email',\n}\n\n/*------------------------------------------\n * THE QUERY THE CONTACTS LIST RUNS.\n *-----------------------------------------*/\n\n/** The list's one order: newest change first. */\nexport const CONTACT_LIST_SORTS: readonly ListQuerySort[] = [\n { path: 'updatedAt', direction: 'desc' },\n]\n\n/** The stored array a facet clause is asked of. */\nexport const CONTACT_FACET_KEYS_PATH = 'facetKeys'\n\n/** The fields the Contacts query asks — each a stored field, as named. */\nexport const CONTACT_QUERY_FIELDS: readonly ListFilterField[] = [\n NAME_FIELD,\n EMAIL_FIELD,\n FORM_FIELD,\n CREATED_FIELD,\n { column: 'hostId', kind: 'exact', path: 'hostId', presence: 'always' },\n {\n column: 'updatedAt',\n kind: 'date',\n path: 'updatedAt',\n presence: 'always',\n operators: ['is', 'after', 'onOrAfter', 'before', 'onOrBefore'],\n },\n {\n column: 'nextTaskAtMs',\n kind: 'date',\n path: 'nextTaskAtMs',\n presence: 'nullable',\n storedAs: 'millis',\n operators: ['isEmpty'],\n },\n {\n column: 'emailStatus',\n kind: 'exact',\n path: 'emailStatus',\n operators: ['equals', 'isAnyOf'],\n },\n {\n column: CONTACT_FACET_KEYS_PATH,\n kind: 'exact',\n path: CONTACT_FACET_KEYS_PATH,\n tokensPath: CONTACT_FACET_KEYS_PATH,\n operators: ['contains', 'isAnyOf'],\n },\n]\n\nexport const CONTACT_LIST_DECLARATION: ListQueryDeclaration = {\n fields: CONTACT_QUERY_FIELDS,\n sorts: CONTACT_LIST_SORTS,\n search: CRM_LIST_SEARCH,\n}\n\n/** The grid fields a stored clause passes to the query unchanged. */\nconst AS_STORED = new Set([\n 'name',\n 'email',\n 'formIds',\n 'hostId',\n 'createdAt',\n 'updatedAt',\n 'nextTaskAtMs',\n])\n\n/**\n * The ranges that stand ALONE beside the scope clause: Created and a name\n * or address prefix each order the list by their own field, which the\n * `(visibleTo, field)` composites serve and nothing beside them. Updated is\n * not one: the list is ordered by it already, so it composes freely.\n */\nexport function contactSoloClause(clause: ListFilterRequest): boolean {\n if (clause.field === 'createdAt') return true\n if (clause.field === 'name') return clause.op === 'startsWith' || clause.op === 'endsWith'\n return clause.field === 'email' && clause.op === 'startsWith'\n}\n\n/**\n * A collaborator's search (see `prefixSearch` in `useCrmListQuery`): the\n * start of the contact's name, beside the scope clause.\n */\nexport const CONTACT_PREFIX_SEARCH = {\n field: NAME_FIELD,\n notice: 'Search matches the start of a contact’s name for access limited to specific sites.',\n}\n\n/** The facet field each grid column is keyed under. */\nconst FACET_FIELDS: Readonly<Record<string, CrmFacetKeyField>> = {\n [CRM_CONTACT_VIEW_FIELDS.owner]: 'owner',\n [CRM_CONTACT_VIEW_FIELDS.stage]: 'stage',\n [CRM_CONTACT_VIEW_FIELDS.source]: 'source',\n [CRM_CONTACT_VIEW_FIELDS.company]: 'company',\n [CRM_CONTACT_VIEW_FIELDS.tags]: 'tag',\n [CRM_CONTACT_VIEW_FIELDS.orders]: 'orders',\n [CRM_CONTACT_VIEW_FIELDS.ltv]: 'ltv',\n [CONTACT_LEAD_SOURCE_COLUMN]: 'leadSource',\n}\n\n/** Who is asking: the viewing holder, and whether their query may drop the scope clause. */\nexport interface ContactQueryReader {\n /** The viewing holder's group id under a site; `null` at the organization level. */\n groupId: string | null\n /**\n * Whether the reader's query may stand a facet or form clause in the\n * scope clause's place (`useCrmFoldsScope`) — always at the organization\n * level, where there is no scope clause.\n */\n foldsScope: boolean\n}\n\n/** Why a reader whose access is some sites cannot filter by a holder's field. */\nconst LIMITED_ACCESS =\n 'your access is limited to specific sites, which this filter cannot be combined with — an organization administrator can use it'\n\n/** The facet field a grid column names, or null for one that is not a facet field. */\nfunction facetFieldOf(column: string): CrmFacetKeyField | null {\n if (FACET_FIELDS[column]) return FACET_FIELDS[column]\n if (column.startsWith(CRM_CONTACT_VIEW_FIELDS.custom)) {\n return `custom.${column.slice(CRM_CONTACT_VIEW_FIELDS.custom.length)}`\n }\n return null\n}\n\n/**\n * One grid clause as the clause the Contacts query asks — see the block at\n * the top. A clause a query cannot serve for this reader is refused by\n * name, never matched over the rows a page loaded.\n */\nexport function contactQueryClause(\n clause: ListFilterRequest,\n reader: ContactQueryReader,\n): CrmClauseAsked {\n if (AS_STORED.has(clause.field)) {\n if (clause.field === 'formIds' && !reader.foldsScope)\n return { refused: LIMITED_ACCESS }\n return { field: clause.field, op: clause.op, value: clause.value }\n }\n if (clause.field === 'emailState') {\n if (clause.op === 'isEmpty')\n return { field: 'emailStatus', op: 'equals', value: 'none' }\n if (clause.op === 'isNotEmpty')\n return crmAnyOf('emailStatus', EMAIL_STATE_STATUSES)\n const values = crmClauseValues(clause)\n if (\n values.some(\n (value) => !(EMAIL_STATE_STATUSES as readonly string[]).includes(value),\n )\n ) {\n return { refused: 'pick one of the choices' }\n }\n return crmAnyOf('emailStatus', values)\n }\n const facet = facetFieldOf(clause.field)\n if (!facet) return { refused: 'this list does not filter by that' }\n if (!reader.foldsScope) return { refused: LIMITED_ACCESS }\n const group = reader.groupId ?? CRM_FACET_KEY_ANY_GROUP\n if (clause.op === 'isNotEmpty') {\n return {\n field: CONTACT_FACET_KEYS_PATH,\n op: 'contains',\n value: crmFacetKey(group, facet),\n }\n }\n const keys: string[] = []\n for (const value of crmClauseValues(clause)) {\n const key = crmFacetKeyValue(clause.op === '=' ? Number(value) : value)\n if (key === null) return { refused: 'no value yet' }\n keys.push(crmFacetKey(group, facet, key))\n }\n const asked = crmAnyOf(CONTACT_FACET_KEYS_PATH, keys)\n // One key is an `array-contains`; several, an `array-contains-any`.\n return 'refused' in asked || asked.op === 'isAnyOf'\n ? asked\n : { ...asked, op: 'contains' }\n}\n\n/**\n * Whether an asked clause stands in for the scope clause: a facet key of\n * the viewing holder, which only a record that holder keeps carries, or a\n * form, which only its site's captures carry.\n */\nexport function contactClauseImpliesScope(groupId: string | null) {\n return (clause: ListFilterRequest): boolean => {\n if (clause.field === 'formIds') return true\n if (clause.field !== CONTACT_FACET_KEYS_PATH || !groupId) return false\n return crmClauseValues(clause).every((key) => key.startsWith(`${groupId}:`))\n }\n}\n\n/**\n * The query shapes the Contacts list can send, as declarations the index\n * pin enumerates (AGL-3321): the list's own, without its solo ranges, whose\n * every equality rides `(field, updatedAt DESC)`; and each solo range beside\n * the scope clause alone.\n */\nexport function contactIndexShapes(): ListQueryDeclaration[] {\n const soloOps = new Set(['startsWith', 'endsWith'])\n const main: ListQueryDeclaration = {\n ...CONTACT_LIST_DECLARATION,\n fields: CONTACT_QUERY_FIELDS.filter((field) => field !== CREATED_FIELD).map((field) =>\n field.operators\n ? { ...field, operators: field.operators.filter((op) => !soloOps.has(op)) }\n : field,\n ),\n }\n const solo: ListQueryDeclaration = {\n fields: [\n { ...NAME_FIELD, operators: ['startsWith', 'endsWith'] },\n { ...EMAIL_FIELD, operators: ['startsWith'] },\n CREATED_FIELD,\n ],\n sorts: [],\n }\n return [main, solo]\n}\n"],"names":["CRM_CONTACT_VIEW_FIELDS","CRM_FACET_KEY_ANY_GROUP","crmContactCustomColumn","crmFacetKey","crmFacetKeyValue","EMAIL_STATE_STATUSES","CRM_LIST_SEARCH","crmAnyOf","crmClauseValues","NAME_FIELD","column","kind","path","lowerPath","tokensPath","reversedPath","operators","EMAIL_FIELD","presence","CREATED_FIELD","createdAt","FORM_FIELD","verbatimTokens","CONTACT_LEAD_SOURCE_COLUMN","CONTACT_LIST_FILTER_FIELDS","tags","orders","ltv","updatedAt","storedAs","owner","stage","source","keysOf","company","contactCustomFilterFields","definitions","filter","definition","retiredAt","map","key","type","contactCustomFilterHeaders","Object","fromEntries","label","CONTACT_LIST_FILTER_HEADERS","name","email","formIds","hostId","ordersCount","ltvCents","nextTaskAtMs","emailState","CONTACT_LIST_SORTS","direction","CONTACT_FACET_KEYS_PATH","CONTACT_QUERY_FIELDS","CONTACT_LIST_DECLARATION","fields","sorts","search","AS_STORED","Set","contactSoloClause","clause","field","op","CONTACT_PREFIX_SEARCH","notice","FACET_FIELDS","LIMITED_ACCESS","facetFieldOf","startsWith","custom","slice","length","contactQueryClause","reader","has","foldsScope","refused","value","values","some","includes","facet","group","groupId","keys","Number","push","asked","contactClauseImpliesScope","every","contactIndexShapes","soloOps","main","solo"],"mappings":"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"}
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-
|
|
18
|
-
import type { ListGridFilterCodec } from '@aglyn/shared-
|
|
19
|
-
import type { ListQueryDeclaration, ListQueryFilter, ListQuerySort } from '@aglyn/shared-
|
|
17
|
+
import type { ListFilterField, ListFilterRequest } from '@aglyn/shared-util-tools/list-query/list-filter';
|
|
18
|
+
import type { ListGridFilterCodec } from '@aglyn/shared-util-tools/list-query/list-filter-codecs';
|
|
19
|
+
import type { ListQueryDeclaration, ListQueryFilter, ListQuerySort } from '@aglyn/shared-util-tools/list-query/list-query-plan';
|
|
20
20
|
import { type CrmViewFilterClause } from '@aglyn/aglyn/app-utils/crm';
|
|
21
21
|
import { type CrmClauseAsked } from '../model/crm-list-query';
|
|
22
22
|
/** The table's one order: newest change first. */
|