@agent-native/core 0.123.0 → 0.123.1
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +16 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
- package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
- package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
- package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
- package/corpus/templates/crm/.env.example +5 -0
- package/corpus/templates/crm/AGENTS.md +82 -0
- package/corpus/templates/crm/CHANGELOG.md +25 -0
- package/corpus/templates/crm/DEVELOPING.md +66 -0
- package/corpus/templates/crm/README.md +122 -0
- package/corpus/templates/crm/_gitignore +12 -0
- package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
- package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
- package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
- package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
- package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
- package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
- package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
- package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
- package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
- package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
- package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
- package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
- package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
- package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
- package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
- package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
- package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
- package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
- package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
- package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
- package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
- package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
- package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
- package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
- package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
- package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
- package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
- package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
- package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
- package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
- package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
- package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
- package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
- package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
- package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
- package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
- package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
- package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
- package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
- package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
- package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
- package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
- package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
- package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
- package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
- package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
- package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
- package/corpus/templates/crm/actions/navigate.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
- package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
- package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
- package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
- package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
- package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
- package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
- package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
- package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
- package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
- package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
- package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
- package/corpus/templates/crm/actions/run.ts +7 -0
- package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
- package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
- package/corpus/templates/crm/actions/sync-crm.ts +146 -0
- package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
- package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
- package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
- package/corpus/templates/crm/actions/view-screen.ts +324 -0
- package/corpus/templates/crm/agent-native.app-skill.json +69 -0
- package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
- package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
- package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
- package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
- package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
- package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
- package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
- package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
- package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
- package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
- package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
- package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
- package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
- package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
- package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
- package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
- package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
- package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
- package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
- package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
- package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
- package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
- package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
- package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
- package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
- package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
- package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
- package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
- package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
- package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
- package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/crm/app/design-system.ts +3 -0
- package/corpus/templates/crm/app/entry.client.tsx +19 -0
- package/corpus/templates/crm/app/entry.server.tsx +10 -0
- package/corpus/templates/crm/app/global.css +167 -0
- package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
- package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
- package/corpus/templates/crm/app/i18n/index.ts +34 -0
- package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
- package/corpus/templates/crm/app/lib/navigation.ts +17 -0
- package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
- package/corpus/templates/crm/app/lib/types.ts +172 -0
- package/corpus/templates/crm/app/lib/utils.ts +1 -0
- package/corpus/templates/crm/app/root.tsx +93 -0
- package/corpus/templates/crm/app/routes/_index.tsx +26 -0
- package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
- package/corpus/templates/crm/app/routes/agent.tsx +5 -0
- package/corpus/templates/crm/app/routes/ask.tsx +53 -0
- package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
- package/corpus/templates/crm/app/routes/lists.tsx +228 -0
- package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
- package/corpus/templates/crm/app/routes/people.tsx +41 -0
- package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
- package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
- package/corpus/templates/crm/app/routes/records.tsx +64 -0
- package/corpus/templates/crm/app/routes/settings.tsx +118 -0
- package/corpus/templates/crm/app/routes/setup.tsx +308 -0
- package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
- package/corpus/templates/crm/app/routes/views.tsx +683 -0
- package/corpus/templates/crm/app/routes.ts +19 -0
- package/corpus/templates/crm/app/vite-env.d.ts +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
- package/corpus/templates/crm/components.json +20 -0
- package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
- package/corpus/templates/crm/docs/features/README.md +55 -0
- package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
- package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
- package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
- package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
- package/corpus/templates/crm/learnings.defaults.md +5 -0
- package/corpus/templates/crm/netlify.toml +11 -0
- package/corpus/templates/crm/package.json +61 -0
- package/corpus/templates/crm/public/favicon.svg +1 -0
- package/corpus/templates/crm/public/icon-180.svg +1 -0
- package/corpus/templates/crm/public/manifest.json +11 -0
- package/corpus/templates/crm/react-router.config.ts +7 -0
- package/corpus/templates/crm/server/crm/adapter.ts +32 -0
- package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
- package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
- package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
- package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
- package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
- package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
- package/corpus/templates/crm/server/crm/read-through.ts +207 -0
- package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
- package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
- package/corpus/templates/crm/server/db/index.ts +191 -0
- package/corpus/templates/crm/server/db/schema.ts +647 -0
- package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
- package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
- package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
- package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
- package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
- package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
- package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
- package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
- package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
- package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
- package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
- package/corpus/templates/crm/server/middleware/auth.ts +4 -0
- package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
- package/corpus/templates/crm/server/plugins/auth.ts +14 -0
- package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
- package/corpus/templates/crm/server/plugins/db.ts +738 -0
- package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
- package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
- package/corpus/templates/crm/shared/crm-contract.ts +417 -0
- package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
- package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
- package/corpus/templates/crm/ssr-entry.ts +11 -0
- package/corpus/templates/crm/tsconfig.json +24 -0
- package/corpus/templates/crm/vite.config.ts +15 -0
- package/corpus/templates/crm/vitest.config.ts +5 -0
- package/corpus/templates/design/app/i18n-data.ts +12 -12
- package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +12 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/templates-meta.ts +12 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The record page: attribute panel on the left, tabs in the middle, list
|
|
3
|
+
* memberships and signals on the right.
|
|
4
|
+
*
|
|
5
|
+
* The page reads two actions on purpose. `get-crm-record` is the one that
|
|
6
|
+
* verifies provider read-through permission for a mirrored record and carries
|
|
7
|
+
* evidence, tasks, and relationships; `get-crm-record-page` carries the typed
|
|
8
|
+
* attribute schema, the current bitemporal values, and list memberships. The
|
|
9
|
+
* second does not replace the first.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { setClientAppState } from "@agent-native/core/client/application-state";
|
|
13
|
+
import {
|
|
14
|
+
useActionMutation,
|
|
15
|
+
useActionQuery,
|
|
16
|
+
} from "@agent-native/core/client/hooks";
|
|
17
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
18
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { toast } from "sonner";
|
|
21
|
+
|
|
22
|
+
import { CrmSignalsPanel } from "@/components/crm/CrmSignalsPanel";
|
|
23
|
+
import { AttributePanel } from "@/components/crm/record/AttributePanel";
|
|
24
|
+
import {
|
|
25
|
+
applyEntryValue,
|
|
26
|
+
applyFieldValue,
|
|
27
|
+
rollbackEntryValue,
|
|
28
|
+
rollbackFieldValue,
|
|
29
|
+
type CrmRecordPage,
|
|
30
|
+
type CrmRecordPageListAttribute,
|
|
31
|
+
type RecordTab,
|
|
32
|
+
} from "@/components/crm/record/record-data";
|
|
33
|
+
import { RecordHeader } from "@/components/crm/record/RecordHeader";
|
|
34
|
+
import { RecordLists } from "@/components/crm/record/RecordLists";
|
|
35
|
+
import { RecordTabs } from "@/components/crm/record/RecordTabs";
|
|
36
|
+
import { LoadingRows, SetupEmptyState } from "@/components/crm/Surface";
|
|
37
|
+
import { TAB_ID } from "@/lib/tab-id";
|
|
38
|
+
import type { CrmRecordDetail } from "@/lib/types";
|
|
39
|
+
|
|
40
|
+
import type {
|
|
41
|
+
CrmAttributeDefinition,
|
|
42
|
+
CrmValue,
|
|
43
|
+
} from "../../../shared/crm-contract";
|
|
44
|
+
|
|
45
|
+
const RECORD_APP_STATE_KEY = "crm-record-workspace";
|
|
46
|
+
|
|
47
|
+
export function RecordWorkspace({
|
|
48
|
+
record,
|
|
49
|
+
isLoading,
|
|
50
|
+
onCompleteTask,
|
|
51
|
+
isCompletingTask,
|
|
52
|
+
actions,
|
|
53
|
+
}: {
|
|
54
|
+
record: CrmRecordDetail | undefined;
|
|
55
|
+
isLoading: boolean;
|
|
56
|
+
onCompleteTask: (taskId: string) => void;
|
|
57
|
+
isCompletingTask: boolean;
|
|
58
|
+
actions?: React.ReactNode;
|
|
59
|
+
}) {
|
|
60
|
+
const t = useT();
|
|
61
|
+
const [tab, setTab] = useState<RecordTab>("activity");
|
|
62
|
+
const queryClient = useQueryClient();
|
|
63
|
+
const recordId = record?.id;
|
|
64
|
+
|
|
65
|
+
const pageParams = { recordId };
|
|
66
|
+
const pageQuery = useActionQuery<CrmRecordPage>(
|
|
67
|
+
"get-crm-record-page" as never,
|
|
68
|
+
pageParams as never,
|
|
69
|
+
{ enabled: Boolean(recordId) },
|
|
70
|
+
);
|
|
71
|
+
const pageKey = ["action", "get-crm-record-page", pageParams] as const;
|
|
72
|
+
|
|
73
|
+
const updateRecord = useActionMutation<
|
|
74
|
+
{ mutationId: string; status?: string },
|
|
75
|
+
{
|
|
76
|
+
recordId: string;
|
|
77
|
+
target: "local";
|
|
78
|
+
fields: Record<string, unknown>;
|
|
79
|
+
expectedRemoteRevision?: string;
|
|
80
|
+
}
|
|
81
|
+
>("update-crm-record" as never);
|
|
82
|
+
const updateEntry = useActionMutation<
|
|
83
|
+
unknown,
|
|
84
|
+
{ entryId: string; values: Record<string, unknown> }
|
|
85
|
+
>("update-crm-list-entry" as never);
|
|
86
|
+
|
|
87
|
+
// The agent needs to know which record is open and which tab is showing.
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!recordId) return;
|
|
90
|
+
void setClientAppState(
|
|
91
|
+
`${RECORD_APP_STATE_KEY}:${TAB_ID}`,
|
|
92
|
+
{ recordId, tab },
|
|
93
|
+
{ requestSource: TAB_ID },
|
|
94
|
+
);
|
|
95
|
+
}, [recordId, tab]);
|
|
96
|
+
|
|
97
|
+
async function commitField(
|
|
98
|
+
attribute: CrmAttributeDefinition,
|
|
99
|
+
value: CrmValue,
|
|
100
|
+
) {
|
|
101
|
+
const current = queryClient.getQueryData<CrmRecordPage>(pageKey);
|
|
102
|
+
if (!current) return;
|
|
103
|
+
const { page: optimistic, edit } = applyFieldValue(
|
|
104
|
+
current,
|
|
105
|
+
attribute.apiSlug,
|
|
106
|
+
value,
|
|
107
|
+
{ since: new Date().toISOString(), actorType: "user", actorId: null },
|
|
108
|
+
);
|
|
109
|
+
queryClient.setQueryData(pageKey, optimistic);
|
|
110
|
+
try {
|
|
111
|
+
await updateRecord.mutateAsync({
|
|
112
|
+
recordId: current.record.id,
|
|
113
|
+
target: "local",
|
|
114
|
+
fields: { [attribute.apiSlug]: value },
|
|
115
|
+
...(current.record.remoteRevision
|
|
116
|
+
? { expectedRemoteRevision: current.record.remoteRevision }
|
|
117
|
+
: {}),
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
queryClient.setQueryData<CrmRecordPage>(pageKey, (previous) =>
|
|
121
|
+
rollbackFieldValue(previous ?? optimistic, edit),
|
|
122
|
+
);
|
|
123
|
+
toast.error(
|
|
124
|
+
error instanceof Error ? error.message : t("record.saveFailed"),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function commitEntryValue(
|
|
130
|
+
entryId: string,
|
|
131
|
+
attribute: CrmRecordPageListAttribute,
|
|
132
|
+
value: CrmValue,
|
|
133
|
+
) {
|
|
134
|
+
const current = queryClient.getQueryData<CrmRecordPage>(pageKey);
|
|
135
|
+
if (!current) return;
|
|
136
|
+
const { page: optimistic, previousValue } = applyEntryValue(
|
|
137
|
+
current,
|
|
138
|
+
entryId,
|
|
139
|
+
attribute.apiSlug,
|
|
140
|
+
value,
|
|
141
|
+
);
|
|
142
|
+
queryClient.setQueryData(pageKey, optimistic);
|
|
143
|
+
try {
|
|
144
|
+
await updateEntry.mutateAsync({
|
|
145
|
+
entryId,
|
|
146
|
+
values: { [attribute.apiSlug]: value },
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
queryClient.setQueryData<CrmRecordPage>(pageKey, (previous) =>
|
|
150
|
+
rollbackEntryValue(
|
|
151
|
+
previous ?? optimistic,
|
|
152
|
+
entryId,
|
|
153
|
+
attribute.apiSlug,
|
|
154
|
+
previousValue,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
toast.error(
|
|
158
|
+
error instanceof Error ? error.message : t("record.saveFailed"),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isLoading) return <LoadingRows rows={8} />;
|
|
164
|
+
if (!record)
|
|
165
|
+
return (
|
|
166
|
+
<SetupEmptyState
|
|
167
|
+
title={t("record.unavailableTitle")}
|
|
168
|
+
description={t("record.unavailableDescription")}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const page = pageQuery.data;
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div className="flex min-h-full flex-col">
|
|
176
|
+
<RecordHeader
|
|
177
|
+
record={record}
|
|
178
|
+
recordUrl={page?.recordUrl ?? null}
|
|
179
|
+
recordUrlUnavailableReason={page?.recordUrlUnavailableReason ?? null}
|
|
180
|
+
actions={actions}
|
|
181
|
+
/>
|
|
182
|
+
<div className="flex min-h-0 flex-1 flex-col lg:flex-row">
|
|
183
|
+
{/* A failed panel load is a visible error, not an empty attribute list:
|
|
184
|
+
"this record has no attributes" and "we could not read them" are
|
|
185
|
+
different answers and must not render the same. */}
|
|
186
|
+
{pageQuery.error ? (
|
|
187
|
+
<aside className="w-full border-border/70 p-5 lg:w-[22rem] lg:shrink-0 lg:border-r">
|
|
188
|
+
<p className="text-sm text-destructive">
|
|
189
|
+
{pageQuery.error instanceof Error
|
|
190
|
+
? pageQuery.error.message
|
|
191
|
+
: t("record.panelLoadFailed")}
|
|
192
|
+
</p>
|
|
193
|
+
</aside>
|
|
194
|
+
) : page ? (
|
|
195
|
+
<AttributePanel page={page} onCommit={commitField} />
|
|
196
|
+
) : (
|
|
197
|
+
<aside className="w-full border-border/70 p-5 lg:w-[22rem] lg:shrink-0 lg:border-r">
|
|
198
|
+
<LoadingRows rows={6} />
|
|
199
|
+
</aside>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
<div className="min-w-0 flex-1">
|
|
203
|
+
<RecordTabs
|
|
204
|
+
record={record}
|
|
205
|
+
tab={tab}
|
|
206
|
+
onTabChange={setTab}
|
|
207
|
+
onCompleteTask={onCompleteTask}
|
|
208
|
+
isCompletingTask={isCompletingTask}
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="grid w-full content-start gap-6 border-border/70 p-5 lg:w-[22rem] lg:shrink-0 lg:border-l">
|
|
213
|
+
{page ? (
|
|
214
|
+
<RecordLists page={page} onEntryCommit={commitEntryValue} />
|
|
215
|
+
) : null}
|
|
216
|
+
<section aria-labelledby="crm-record-signals">
|
|
217
|
+
<h2
|
|
218
|
+
id="crm-record-signals"
|
|
219
|
+
className="mb-2 text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground"
|
|
220
|
+
>
|
|
221
|
+
{t("record.signals")}
|
|
222
|
+
</h2>
|
|
223
|
+
<CrmSignalsPanel
|
|
224
|
+
recordId={record.id}
|
|
225
|
+
evidence={record.evidence ?? []}
|
|
226
|
+
/>
|
|
227
|
+
</section>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { DataTable } from "@agent-native/toolkit/dashboard";
|
|
3
|
+
import { IconChartDots } from "@tabler/icons-react";
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
|
|
8
|
+
interface ProgramPreview {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
rowCount?: number;
|
|
11
|
+
columns?: Array<{ name: string; type: string }>;
|
|
12
|
+
sampleRows?: Array<Record<string, unknown>>;
|
|
13
|
+
asOfMs?: number;
|
|
14
|
+
cacheHit?: boolean;
|
|
15
|
+
stale?: boolean;
|
|
16
|
+
truncated?: boolean;
|
|
17
|
+
message?: string;
|
|
18
|
+
lastGoodRun?: {
|
|
19
|
+
rowCount: number;
|
|
20
|
+
columns: Array<{ name: string; type: string }>;
|
|
21
|
+
sampleRows: Array<Record<string, unknown>>;
|
|
22
|
+
truncated: boolean;
|
|
23
|
+
asOfMs: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function SavedViewDataProgram({ data }: { data: unknown }) {
|
|
28
|
+
const viewId = linkedProgramViewId(data);
|
|
29
|
+
const lastRunViewId = useRef<string | undefined>(undefined);
|
|
30
|
+
const run = useActionMutation<ProgramPreview, { viewId: string }>(
|
|
31
|
+
"run-crm-saved-view-program" as never,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!viewId || lastRunViewId.current === viewId) return;
|
|
36
|
+
lastRunViewId.current = viewId;
|
|
37
|
+
run.mutate({ viewId });
|
|
38
|
+
}, [run, viewId]);
|
|
39
|
+
|
|
40
|
+
if (!viewId) return null;
|
|
41
|
+
const preview = run.data?.ok ? run.data : run.data?.lastGoodRun;
|
|
42
|
+
return (
|
|
43
|
+
<section className="mx-5 mt-5 rounded-lg border border-border/70 bg-card p-4 sm:mx-7">
|
|
44
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
45
|
+
<div className="flex items-start gap-3">
|
|
46
|
+
<IconChartDots className="mt-0.5 size-4 text-muted-foreground" />
|
|
47
|
+
<div>
|
|
48
|
+
<h2 className="text-sm font-semibold">Cross-source context</h2>
|
|
49
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
50
|
+
{run.isPending
|
|
51
|
+
? "Running the saved data program…"
|
|
52
|
+
: run.isError
|
|
53
|
+
? "The saved data program is unavailable."
|
|
54
|
+
: run.data?.ok
|
|
55
|
+
? `${run.data.rowCount ?? 0} rows from the linked data program.`
|
|
56
|
+
: run.data?.message ||
|
|
57
|
+
"Showing the last good program result."}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
{preview ? (
|
|
62
|
+
<div className="flex gap-2">
|
|
63
|
+
{preview.truncated ? (
|
|
64
|
+
<Badge variant="outline">Truncated</Badge>
|
|
65
|
+
) : null}
|
|
66
|
+
{run.data?.ok && run.data.cacheHit ? (
|
|
67
|
+
<Badge variant="secondary">Cached</Badge>
|
|
68
|
+
) : null}
|
|
69
|
+
</div>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
{preview?.sampleRows?.length ? (
|
|
73
|
+
<div className="mt-4 overflow-hidden rounded-md border border-border/70">
|
|
74
|
+
<DataTable
|
|
75
|
+
data={preview.sampleRows.slice(0, 5)}
|
|
76
|
+
columns={preview.columns?.slice(0, 8).map((column) => column.name)}
|
|
77
|
+
maxRows={5}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
) : null}
|
|
81
|
+
</section>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function linkedProgramViewId(data: unknown) {
|
|
86
|
+
if (!data || typeof data !== "object") return undefined;
|
|
87
|
+
const appliedView = (data as { appliedView?: unknown }).appliedView;
|
|
88
|
+
if (!appliedView || typeof appliedView !== "object") return undefined;
|
|
89
|
+
const view = appliedView as Record<string, unknown>;
|
|
90
|
+
return typeof view.id === "string" && typeof view.dataProgramId === "string"
|
|
91
|
+
? view.id
|
|
92
|
+
: undefined;
|
|
93
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconArrowUpRight,
|
|
3
|
+
IconDatabase,
|
|
4
|
+
IconDatabaseOff,
|
|
5
|
+
} from "@tabler/icons-react";
|
|
6
|
+
import { Link } from "react-router";
|
|
7
|
+
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
10
|
+
|
|
11
|
+
export function PageHeader({
|
|
12
|
+
eyebrow,
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
actions,
|
|
16
|
+
}: {
|
|
17
|
+
eyebrow?: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
actions?: React.ReactNode;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<header className="flex flex-col gap-4 border-b border-border/70 px-5 py-5 sm:flex-row sm:items-end sm:justify-between sm:px-7">
|
|
24
|
+
<div className="min-w-0">
|
|
25
|
+
{eyebrow ? (
|
|
26
|
+
<p className="mb-1 text-xs font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
|
27
|
+
{eyebrow}
|
|
28
|
+
</p>
|
|
29
|
+
) : null}
|
|
30
|
+
<h1 className="text-xl font-semibold tracking-tight">{title}</h1>
|
|
31
|
+
{description ? (
|
|
32
|
+
<p className="mt-1 max-w-2xl text-sm leading-6 text-muted-foreground">
|
|
33
|
+
{description}
|
|
34
|
+
</p>
|
|
35
|
+
) : null}
|
|
36
|
+
</div>
|
|
37
|
+
{actions ? (
|
|
38
|
+
<div className="flex shrink-0 items-center gap-2">{actions}</div>
|
|
39
|
+
) : null}
|
|
40
|
+
</header>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function LoadingRows({ rows = 5 }: { rows?: number }) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="grid gap-2 p-5 sm:p-7">
|
|
47
|
+
{Array.from({ length: rows }, (_, index) => (
|
|
48
|
+
<Skeleton key={index} className="h-12 w-full" />
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function SetupEmptyState({
|
|
55
|
+
title = "Start a CRM to begin",
|
|
56
|
+
description = "Start with the built-in SQL CRM, or connect a scoped workspace CRM. No provider credentials are stored in this app.",
|
|
57
|
+
onSync,
|
|
58
|
+
isSyncing = false,
|
|
59
|
+
actionLabel,
|
|
60
|
+
}: {
|
|
61
|
+
title?: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
onSync?: () => void;
|
|
64
|
+
isSyncing?: boolean;
|
|
65
|
+
actionLabel?: string;
|
|
66
|
+
}) {
|
|
67
|
+
return (
|
|
68
|
+
<div className="grid min-h-[360px] place-items-center p-6">
|
|
69
|
+
<div className="max-w-sm text-center">
|
|
70
|
+
<div className="mx-auto grid size-10 place-items-center rounded-full bg-muted text-muted-foreground">
|
|
71
|
+
<IconDatabaseOff className="size-5" />
|
|
72
|
+
</div>
|
|
73
|
+
<h2 className="mt-4 text-base font-semibold">{title}</h2>
|
|
74
|
+
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
|
75
|
+
{description}
|
|
76
|
+
</p>
|
|
77
|
+
<div className="mt-5 flex flex-wrap justify-center gap-2">
|
|
78
|
+
<Button asChild className="gap-2">
|
|
79
|
+
<Link to="/setup">
|
|
80
|
+
<IconDatabase className="size-4" /> Start with Native SQL
|
|
81
|
+
</Link>
|
|
82
|
+
</Button>
|
|
83
|
+
<Button asChild variant="outline" className="gap-2">
|
|
84
|
+
<Link to="/settings/connections">
|
|
85
|
+
Open shared connections <IconArrowUpRight className="size-4" />
|
|
86
|
+
</Link>
|
|
87
|
+
</Button>
|
|
88
|
+
{onSync ? (
|
|
89
|
+
<Button onClick={onSync} disabled={isSyncing}>
|
|
90
|
+
{isSyncing ? "Working…" : (actionLabel ?? "Sync recent records")}
|
|
91
|
+
</Button>
|
|
92
|
+
) : null}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { IconArrowRight, IconCheck, IconListCheck } from "@tabler/icons-react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
|
|
4
|
+
import { LoadingRows, SetupEmptyState } from "@/components/crm/Surface";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
normalizeRecords,
|
|
9
|
+
normalizeTasks,
|
|
10
|
+
type CrmOverview,
|
|
11
|
+
} from "@/lib/types";
|
|
12
|
+
|
|
13
|
+
export function WorkOverview({
|
|
14
|
+
overview,
|
|
15
|
+
isLoading,
|
|
16
|
+
}: {
|
|
17
|
+
overview: CrmOverview | undefined;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
}) {
|
|
20
|
+
const tasks = normalizeTasks(overview?.tasks);
|
|
21
|
+
const records = normalizeRecords(overview?.records, "account");
|
|
22
|
+
const focus = overview?.focus ?? [];
|
|
23
|
+
|
|
24
|
+
if (isLoading) return <LoadingRows rows={7} />;
|
|
25
|
+
if (!overview) return <SetupEmptyState />;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="mx-auto grid max-w-6xl gap-8 p-5 sm:p-7 xl:grid-cols-[minmax(0,1fr)_320px]">
|
|
29
|
+
<div className="min-w-0">
|
|
30
|
+
<section>
|
|
31
|
+
<div className="flex items-center justify-between gap-3">
|
|
32
|
+
<div>
|
|
33
|
+
<h2 className="text-sm font-semibold">Today’s work</h2>
|
|
34
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
35
|
+
A focused queue across your CRM workspace.
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
<Button
|
|
39
|
+
asChild
|
|
40
|
+
variant="ghost"
|
|
41
|
+
size="sm"
|
|
42
|
+
className="gap-1.5 text-muted-foreground"
|
|
43
|
+
>
|
|
44
|
+
<Link to="/views">
|
|
45
|
+
Saved views <IconArrowRight className="size-3.5" />
|
|
46
|
+
</Link>
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="mt-3 divide-y divide-border rounded-lg border border-border/70 bg-card">
|
|
50
|
+
{tasks.length ? (
|
|
51
|
+
tasks.map((task) => (
|
|
52
|
+
<div
|
|
53
|
+
key={task.id}
|
|
54
|
+
className="flex min-h-14 items-center gap-3 px-3.5 py-2.5"
|
|
55
|
+
>
|
|
56
|
+
<span className="grid size-5 shrink-0 place-items-center rounded-full border border-border text-muted-foreground">
|
|
57
|
+
<IconCheck className="size-3" />
|
|
58
|
+
</span>
|
|
59
|
+
<div className="min-w-0 flex-1">
|
|
60
|
+
<p className="truncate text-sm font-medium">{task.title}</p>
|
|
61
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
62
|
+
{task.dueAt
|
|
63
|
+
? `Due ${formatDate(task.dueAt)}`
|
|
64
|
+
: "No due date"}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
{task.recordId ? (
|
|
68
|
+
<Button asChild variant="ghost" size="sm">
|
|
69
|
+
<Link
|
|
70
|
+
to={`/records/${encodeURIComponent(task.recordId)}`}
|
|
71
|
+
>
|
|
72
|
+
Open
|
|
73
|
+
</Link>
|
|
74
|
+
</Button>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
))
|
|
78
|
+
) : (
|
|
79
|
+
<QuietEmpty
|
|
80
|
+
icon={<IconListCheck className="size-4" />}
|
|
81
|
+
text="No open CRM tasks right now."
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
</section>
|
|
86
|
+
|
|
87
|
+
<section className="mt-8">
|
|
88
|
+
<div className="flex items-center justify-between">
|
|
89
|
+
<h2 className="text-sm font-semibold">Recently active</h2>
|
|
90
|
+
<Button
|
|
91
|
+
asChild
|
|
92
|
+
variant="ghost"
|
|
93
|
+
size="sm"
|
|
94
|
+
className="gap-1.5 text-muted-foreground"
|
|
95
|
+
>
|
|
96
|
+
<Link to="/accounts">
|
|
97
|
+
Accounts <IconArrowRight className="size-3.5" />
|
|
98
|
+
</Link>
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="mt-3 divide-y divide-border rounded-lg border border-border/70 bg-card">
|
|
102
|
+
{records.length ? (
|
|
103
|
+
records.slice(0, 5).map((record) => (
|
|
104
|
+
<Link
|
|
105
|
+
key={record.id}
|
|
106
|
+
to={`/records/${encodeURIComponent(record.id)}`}
|
|
107
|
+
className="flex items-center gap-3 px-3.5 py-3 transition-colors hover:bg-muted/50"
|
|
108
|
+
>
|
|
109
|
+
<div className="min-w-0 flex-1">
|
|
110
|
+
<p className="truncate text-sm font-medium">
|
|
111
|
+
{record.displayName}
|
|
112
|
+
</p>
|
|
113
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
114
|
+
{record.subtitle ?? record.owner ?? "CRM record"}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
{record.stage ? (
|
|
118
|
+
<Badge variant="secondary" className="font-normal">
|
|
119
|
+
{record.stage}
|
|
120
|
+
</Badge>
|
|
121
|
+
) : null}
|
|
122
|
+
</Link>
|
|
123
|
+
))
|
|
124
|
+
) : (
|
|
125
|
+
<QuietEmpty text="Recent activity will appear as CRM records are added or synced." />
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</section>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<aside className="self-start border-t border-border/70 pt-5 xl:border-l xl:border-t-0 xl:pl-7 xl:pt-0">
|
|
132
|
+
<h2 className="text-sm font-semibold">Focus</h2>
|
|
133
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
134
|
+
Signals worth deciding on.
|
|
135
|
+
</p>
|
|
136
|
+
<div className="mt-4 grid gap-3">
|
|
137
|
+
{focus.length ? (
|
|
138
|
+
focus.map((item) => (
|
|
139
|
+
<div
|
|
140
|
+
key={item.label}
|
|
141
|
+
className="border-b border-border/70 pb-3 last:border-0"
|
|
142
|
+
>
|
|
143
|
+
<p className="text-xs font-medium uppercase tracking-[0.1em] text-muted-foreground">
|
|
144
|
+
{item.label}
|
|
145
|
+
</p>
|
|
146
|
+
<p className="mt-1 text-sm font-medium">{item.value}</p>
|
|
147
|
+
{item.detail ? (
|
|
148
|
+
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
|
149
|
+
{item.detail}
|
|
150
|
+
</p>
|
|
151
|
+
) : null}
|
|
152
|
+
</div>
|
|
153
|
+
))
|
|
154
|
+
) : (
|
|
155
|
+
<p className="text-sm leading-6 text-muted-foreground">
|
|
156
|
+
CRM will surface follow-up signals as records and interactions
|
|
157
|
+
become available.
|
|
158
|
+
</p>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</aside>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function QuietEmpty({ icon, text }: { icon?: React.ReactNode; text: string }) {
|
|
167
|
+
return (
|
|
168
|
+
<div className="flex min-h-20 items-center justify-center gap-2 px-4 text-sm text-muted-foreground">
|
|
169
|
+
{icon}
|
|
170
|
+
{text}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatDate(value: string) {
|
|
176
|
+
const date = new Date(value);
|
|
177
|
+
return Number.isNaN(date.valueOf())
|
|
178
|
+
? value
|
|
179
|
+
: date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
180
|
+
}
|