@open-mercato/core 0.9.1-develop.7477.1.64ba865101 → 0.9.1-develop.7497.1.543ea44c40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/business_rules/data/validators.js +8 -3
  3. package/dist/modules/business_rules/data/validators.js.map +2 -2
  4. package/dist/modules/catalog/api/dictionaries/[key]/route.js +35 -7
  5. package/dist/modules/catalog/api/dictionaries/[key]/route.js.map +2 -2
  6. package/dist/modules/communication_channels/lib/email-mime.js +1 -2
  7. package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
  8. package/dist/modules/configs/cli.js +8 -2
  9. package/dist/modules/configs/cli.js.map +2 -2
  10. package/dist/modules/customers/api/companies/[id]/people/route.js +3 -0
  11. package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
  12. package/dist/modules/customers/api/companies/route.js +3 -3
  13. package/dist/modules/customers/api/companies/route.js.map +2 -2
  14. package/dist/modules/customers/api/deals/[id]/companies/route.js +3 -0
  15. package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
  16. package/dist/modules/customers/api/deals/[id]/people/route.js +3 -0
  17. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  18. package/dist/modules/customers/api/dictionaries/kind-settings/route.js +6 -0
  19. package/dist/modules/customers/api/dictionaries/kind-settings/route.js.map +2 -2
  20. package/dist/modules/customers/api/labels/route.js +10 -1
  21. package/dist/modules/customers/api/labels/route.js.map +2 -2
  22. package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js +6 -0
  23. package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js.map +2 -2
  24. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -0
  25. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
  26. package/dist/modules/customers/api/people/[id]/companies/route.js +6 -0
  27. package/dist/modules/customers/api/people/[id]/companies/route.js.map +2 -2
  28. package/dist/modules/customers/api/people/route.js +3 -3
  29. package/dist/modules/customers/api/people/route.js.map +2 -2
  30. package/dist/modules/customers/commands/deals.js +2 -0
  31. package/dist/modules/customers/commands/deals.js.map +2 -2
  32. package/dist/modules/customers/components/ActivityTypeEditor.js +11 -5
  33. package/dist/modules/customers/components/ActivityTypeEditor.js.map +2 -2
  34. package/dist/modules/customers/components/detail/InlineEditors.js +7 -3
  35. package/dist/modules/customers/components/detail/InlineEditors.js.map +2 -2
  36. package/dist/modules/customers/components/detail/PersonHighlights.js +1 -1
  37. package/dist/modules/customers/components/detail/PersonHighlights.js.map +2 -2
  38. package/dist/modules/customers/components/detail/PlannedActivitiesSection.js +4 -5
  39. package/dist/modules/customers/components/detail/PlannedActivitiesSection.js.map +2 -2
  40. package/dist/modules/data_sync/di.js.map +2 -2
  41. package/dist/modules/data_sync/lib/sync-schedule-service.js +60 -27
  42. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
  43. package/dist/modules/messages/commands/messages.js +10 -7
  44. package/dist/modules/messages/commands/messages.js.map +2 -2
  45. package/dist/modules/messages/lib/constants.js +3 -1
  46. package/dist/modules/messages/lib/constants.js.map +2 -2
  47. package/dist/modules/messages/subscribers/message-notification.js +2 -1
  48. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  49. package/dist/modules/sales/api/documents/factory.js +3 -4
  50. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  51. package/dist/modules/sales/commands/payments.js +9 -1
  52. package/dist/modules/sales/commands/payments.js.map +2 -2
  53. package/package.json +7 -7
  54. package/src/modules/business_rules/data/validators.ts +12 -3
  55. package/src/modules/catalog/api/dictionaries/[key]/route.ts +49 -7
  56. package/src/modules/communication_channels/lib/email-mime.ts +1 -2
  57. package/src/modules/configs/cli.ts +10 -1
  58. package/src/modules/customers/api/companies/[id]/people/route.ts +3 -0
  59. package/src/modules/customers/api/companies/route.ts +6 -6
  60. package/src/modules/customers/api/deals/[id]/companies/route.ts +3 -0
  61. package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
  62. package/src/modules/customers/api/dictionaries/kind-settings/route.ts +6 -0
  63. package/src/modules/customers/api/labels/route.ts +10 -1
  64. package/src/modules/customers/api/people/[id]/companies/[linkId]/route.ts +6 -0
  65. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +3 -0
  66. package/src/modules/customers/api/people/[id]/companies/route.ts +6 -0
  67. package/src/modules/customers/api/people/route.ts +6 -6
  68. package/src/modules/customers/commands/deals.ts +2 -0
  69. package/src/modules/customers/components/ActivityTypeEditor.tsx +11 -5
  70. package/src/modules/customers/components/detail/InlineEditors.tsx +8 -4
  71. package/src/modules/customers/components/detail/PersonHighlights.tsx +4 -1
  72. package/src/modules/customers/components/detail/PlannedActivitiesSection.tsx +4 -5
  73. package/src/modules/customers/i18n/de.json +2 -0
  74. package/src/modules/customers/i18n/en.json +2 -0
  75. package/src/modules/customers/i18n/es.json +2 -0
  76. package/src/modules/customers/i18n/ko.json +2 -0
  77. package/src/modules/customers/i18n/pl.json +2 -0
  78. package/src/modules/data_sync/di.ts +1 -0
  79. package/src/modules/data_sync/lib/sync-schedule-service.ts +80 -28
  80. package/src/modules/messages/commands/messages.ts +10 -7
  81. package/src/modules/messages/lib/constants.ts +6 -0
  82. package/src/modules/messages/subscribers/message-notification.ts +2 -0
  83. package/src/modules/sales/api/documents/factory.ts +6 -7
  84. package/src/modules/sales/commands/payments.ts +10 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/PersonHighlights.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Building2, Loader2, Pencil, X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { CompanySelectField } from '../formConfig'\nimport {\n InlineTextEditor,\n InlineDictionaryEditor,\n InlineNextInteractionEditor,\n type InlineFieldProps,\n type NextInteractionPayload,\n} from './InlineEditors'\n\ntype PersonHighlightsPerson = {\n id: string\n displayName: string\n primaryEmail?: string | null\n primaryPhone?: string | null\n status?: string | null\n nextInteractionAt?: string | null\n nextInteractionName?: string | null\n nextInteractionRefId?: string | null\n nextInteractionIcon?: string | null\n nextInteractionColor?: string | null\n organizationId?: string | null\n}\n\ntype PersonHighlightsProfile = {\n id?: string\n companyEntityId?: string | null\n} | null\n\ntype PersonHighlightsValidators = {\n email: NonNullable<InlineFieldProps['validator']>\n phone: NonNullable<InlineFieldProps['validator']>\n displayName: NonNullable<InlineFieldProps['validator']>\n}\n\nexport type PersonHighlightsProps = {\n person: PersonHighlightsPerson\n profile: PersonHighlightsProfile\n validators: PersonHighlightsValidators\n onDisplayNameSave: (value: string | null) => Promise<void>\n onPrimaryEmailSave: (value: string | null) => Promise<void>\n onPrimaryPhoneSave: (value: string | null) => Promise<void>\n onStatusSave: (value: string | null) => Promise<void>\n onNextInteractionSave: (value: NextInteractionPayload | null) => Promise<void>\n onDelete: () => void\n isDeleting: boolean\n onCompanySave: (companyId: string | null) => Promise<void>\n utilityActions?: React.ReactNode\n}\n\ntype CompanyInfo = { id: string; name: string }\n\nexport function PersonHighlights({\n person,\n profile,\n validators,\n onDisplayNameSave,\n onPrimaryEmailSave,\n onPrimaryPhoneSave,\n onStatusSave,\n onNextInteractionSave,\n onDelete,\n isDeleting,\n onCompanySave,\n utilityActions,\n}: PersonHighlightsProps) {\n const router = useRouter()\n const t = useT()\n const runMutation = React.useCallback(async (operation: () => Promise<void>) => operation(), [])\n const [editingCompany, setEditingCompany] = React.useState(false)\n const [companyDraftId, setCompanyDraftId] = React.useState<string>('')\n const [company, setCompany] = React.useState<CompanyInfo | null>(null)\n const [companyLoading, setCompanyLoading] = React.useState(false)\n const [companyError, setCompanyError] = React.useState<string | null>(null)\n const [companySaving, setCompanySaving] = React.useState(false)\n const companyHref = React.useMemo(\n () => (company ? `/backend/customers/companies/${encodeURIComponent(company.id)}` : null),\n [company],\n )\n const isCompanyInteractive = !editingCompany && !companyLoading && !companyError && Boolean(companyHref)\n\n const navigateToCompany = React.useCallback(() => {\n if (!companyHref) return\n router.push(companyHref)\n }, [companyHref])\n\n const handleCompanyClick = React.useCallback(\n (event: React.MouseEvent<HTMLDivElement>) => {\n if (!isCompanyInteractive || !companyHref) return\n const target = event.target as HTMLElement\n if (target.closest('button')) return\n if (event.defaultPrevented) return\n if (event.button !== 0) return\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {\n if (typeof window !== 'undefined') {\n window.open(companyHref, '_blank', 'noopener,noreferrer')\n }\n return\n }\n navigateToCompany()\n },\n [companyHref, isCompanyInteractive, navigateToCompany],\n )\n\n const handleCompanyKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (!isCompanyInteractive) return\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n navigateToCompany()\n }\n },\n [isCompanyInteractive, navigateToCompany],\n )\n\n const activeCompanyId = profile?.companyEntityId ?? null\n const historyFallbackId =\n profile?.id && profile.id !== person.id ? profile.id : undefined\n\n const loadCompany = React.useCallback(async (companyId: string | null) => {\n if (!companyId) {\n setCompany(null)\n return\n }\n setCompanyLoading(true)\n setCompanyError(null)\n try {\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/companies?id=${encodeURIComponent(companyId)}`,\n undefined,\n { errorMessage: t('customers.people.detail.company.loadError', 'Unable to load company information.') },\n )\n const items = Array.isArray(payload?.items) ? (payload.items as Array<Record<string, unknown>>) : []\n const item = items.find((entry) => {\n if (!entry) return false\n const rawId = (entry as Record<string, unknown>).id\n if (typeof rawId === 'string') return rawId === companyId\n if (typeof rawId === 'number') return String(rawId) === companyId\n return false\n }) as Record<string, unknown> | undefined\n if (item && typeof item.display_name === 'string') {\n setCompany({ id: companyId, name: item.display_name })\n } else if (item && typeof item.displayName === 'string') {\n setCompany({ id: companyId, name: item.displayName })\n } else {\n setCompany({ id: companyId, name: t('customers.people.detail.company.unknown', 'Unknown company') })\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customers.people.detail.company.loadError', 'Unable to load company.')\n setCompanyError(message)\n setCompany(null)\n } finally {\n setCompanyLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n setCompanyDraftId(activeCompanyId ?? '')\n loadCompany(activeCompanyId).catch(() => {})\n }, [activeCompanyId, loadCompany])\n\n const handleCompanySave = React.useCallback(async () => {\n if (companySaving) return\n setCompanySaving(true)\n setCompanyError(null)\n const nextId = companyDraftId.trim()\n try {\n await runMutation(async () => {\n await onCompanySave(nextId.length ? nextId : null)\n })\n await loadCompany(nextId.length ? nextId : null)\n setEditingCompany(false)\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : t('customers.people.detail.company.saveError', 'Unable to update company.')\n setCompanyError(message)\n } finally {\n setCompanySaving(false)\n }\n }, [companyDraftId, companySaving, loadCompany, onCompanySave, runMutation, t])\n\n const handleCompanyClear = React.useCallback(async () => {\n if (companySaving) return\n setCompanySaving(true)\n setCompanyError(null)\n try {\n await runMutation(async () => {\n await onCompanySave(null)\n })\n await loadCompany(null)\n setCompanyDraftId('')\n setEditingCompany(false)\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : t('customers.people.detail.company.saveError', 'Unable to update company.')\n setCompanyError(message)\n } finally {\n setCompanySaving(false)\n }\n }, [companySaving, loadCompany, onCompanySave, runMutation, t])\n\n const companyPanel = (\n <div\n className={cn(\n 'group rounded-lg border bg-muted/30 p-3',\n isCompanyInteractive\n ? 'cursor-pointer transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n : null,\n )}\n role={isCompanyInteractive ? 'link' : undefined}\n tabIndex={isCompanyInteractive ? 0 : undefined}\n onClick={handleCompanyClick}\n onKeyDown={handleCompanyKeyDown}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex-1 space-y-1\">\n <p className=\"flex items-center gap-2 text-xs uppercase tracking-wide text-muted-foreground\">\n <Building2 aria-hidden className=\"h-3.5 w-3.5\" />\n {t('customers.people.detail.company.label', 'Company')}\n </p>\n {editingCompany ? (\n <div\n className=\"space-y-3\"\n onKeyDown={(event) => {\n if (event.key === 'Escape') {\n event.preventDefault()\n if (!companySaving) {\n setEditingCompany(false)\n setCompanyError(null)\n setCompanyDraftId(activeCompanyId ?? '')\n }\n }\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n if (!companySaving) {\n void handleCompanySave()\n }\n }\n }}\n >\n <CompanySelectField\n value={companyDraftId || undefined}\n onChange={(next) => setCompanyDraftId(next ?? '')}\n labels={{\n placeholder: t('customers.people.form.company.placeholder'),\n addLabel: t('customers.people.form.company.add'),\n addPrompt: t('customers.people.form.company.prompt'),\n dialogTitle: t('customers.people.form.company.dialogTitle'),\n inputLabel: t('customers.people.form.company.inputLabel'),\n inputPlaceholder: t('customers.people.form.company.inputPlaceholder'),\n emptyError: t('customers.people.form.dictionary.errorRequired'),\n cancelLabel: t('customers.people.form.dictionary.cancel'),\n saveLabel: t('customers.people.form.dictionary.save'),\n errorLoad: t('customers.people.form.dictionary.errorLoad'),\n errorSave: t('customers.people.form.dictionary.error'),\n loadingLabel: t('customers.people.form.company.loading'),\n }}\n />\n {companyError ? <p className=\"text-xs text-status-error-text\">{companyError}</p> : null}\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button type=\"button\" size=\"sm\" onClick={handleCompanySave} disabled={companySaving}>\n {companySaving ? <Loader2 className=\"mr-2 h-3.5 w-3.5 animate-spin\" /> : null}\n {t('ui.forms.actions.save')}\n </Button>\n <Button type=\"button\" size=\"sm\" variant=\"ghost\" onClick={() => {\n setEditingCompany(false)\n setCompanyError(null)\n setCompanyDraftId(activeCompanyId ?? '')\n }} disabled={companySaving}>\n {t('ui.forms.actions.cancel')}\n </Button>\n <Button type=\"button\" size=\"sm\" variant=\"secondary\" onClick={handleCompanyClear} disabled={companySaving}>\n {t('ui.forms.actions.clear')}\n </Button>\n </div>\n </div>\n ) : (\n <div className=\"flex items-center gap-2 text-sm\">\n {companyLoading ? (\n <span className=\"inline-flex items-center gap-2 text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {t('customers.people.detail.company.loading', 'Loading company\u2026')}\n </span>\n ) : company ? (\n <span className=\"text-primary transition group-hover:underline\">\n {t('customers.people.detail.company.current', undefined, { company: company.name })}\n </span>\n ) : companyError ? (\n <span className=\"text-xs text-status-error-text\">{companyError}</span>\n ) : (\n <span className=\"text-muted-foreground\">\n {t('customers.people.detail.company.empty', 'No company assigned')}\n </span>\n )}\n </div>\n )}\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => {\n if (companySaving) return\n setEditingCompany((prev) => !prev)\n setCompanyError(null)\n setCompanyDraftId(activeCompanyId ?? '')\n }}\n className={\n editingCompany\n ? 'opacity-100'\n : 'opacity-0 transition-opacity duration-150 group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100'\n }\n >\n {editingCompany ? <X className=\"h-4 w-4\" /> : <Pencil className=\"h-4 w-4\" />}\n <span className=\"sr-only\">\n {editingCompany ? t('ui.forms.actions.cancel') : t('ui.forms.actions.edit')}\n </span>\n </Button>\n </div>\n </div>\n )\n\n return (\n <div className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/customers/people\"\n backLabel={t('customers.people.detail.actions.backToList')}\n utilityActions={(\n <>\n {utilityActions}\n <VersionHistoryAction\n config={{\n resourceKind: 'customers.person',\n resourceId: person.id,\n resourceIdFallback: historyFallbackId,\n organizationId: person.organizationId ?? undefined,\n }}\n t={t}\n />\n </>\n )}\n title={\n <InlineTextEditor\n label={t('customers.people.form.displayName.label')}\n value={person.displayName}\n placeholder={t('customers.people.form.displayName.placeholder')}\n emptyLabel={t('customers.people.detail.noValue')}\n validator={validators.displayName}\n onSave={onDisplayNameSave}\n hideLabel\n variant=\"plain\"\n activateOnClick\n triggerClassName=\"opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100\"\n containerClassName=\"max-w-full\"\n />\n }\n onDelete={() => {\n onDelete()\n }}\n isDeleting={isDeleting}\n deleteLabel={t('customers.people.list.actions.delete')}\n />\n\n {companyPanel}\n\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-4\">\n <InlineTextEditor\n label={t('customers.people.detail.highlights.primaryEmail')}\n value={person.primaryEmail || ''}\n placeholder={t('customers.people.form.primaryEmail')}\n emptyLabel={t('customers.people.detail.noValue')}\n type=\"email\"\n validator={validators.email}\n recordId={person.id}\n activateOnClick\n onSave={onPrimaryEmailSave}\n />\n <InlineTextEditor\n label={t('customers.people.detail.highlights.primaryPhone')}\n value={person.primaryPhone || ''}\n placeholder={t('customers.people.form.primaryPhone')}\n emptyLabel={t('customers.people.detail.noValue')}\n type=\"tel\"\n validator={validators.phone}\n recordId={person.id}\n activateOnClick\n onSave={onPrimaryPhoneSave}\n />\n <InlineDictionaryEditor\n label={t('customers.people.detail.highlights.status')}\n value={person.status ?? null}\n emptyLabel={t('customers.people.detail.noValue')}\n activateOnClick\n onSave={onStatusSave}\n kind=\"statuses\"\n />\n <InlineNextInteractionEditor\n label={t('customers.people.detail.highlights.nextInteraction')}\n valueAt={person.nextInteractionAt || null}\n valueName={person.nextInteractionName || null}\n valueRefId={person.nextInteractionRefId || null}\n valueIcon={person.nextInteractionIcon || null}\n valueColor={person.nextInteractionColor || null}\n emptyLabel={t('customers.people.detail.noValue')}\n onSave={onNextInteractionSave}\n activateOnClick\n />\n </div>\n </div>\n )\n}\n\nexport default PersonHighlights\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Building2, Loader2, Pencil, X } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { FormHeader } from '@open-mercato/ui/backend/forms'\nimport { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { CompanySelectField } from '../formConfig'\nimport {\n InlineTextEditor,\n InlineDictionaryEditor,\n InlineNextInteractionEditor,\n type InlineFieldProps,\n type NextInteractionPayload,\n} from './InlineEditors'\n\ntype PersonHighlightsPerson = {\n id: string\n displayName: string\n primaryEmail?: string | null\n primaryPhone?: string | null\n status?: string | null\n nextInteractionAt?: string | null\n nextInteractionName?: string | null\n nextInteractionRefId?: string | null\n nextInteractionIcon?: string | null\n nextInteractionColor?: string | null\n organizationId?: string | null\n}\n\ntype PersonHighlightsProfile = {\n id?: string\n companyEntityId?: string | null\n} | null\n\ntype PersonHighlightsValidators = {\n email: NonNullable<InlineFieldProps['validator']>\n phone: NonNullable<InlineFieldProps['validator']>\n displayName: NonNullable<InlineFieldProps['validator']>\n}\n\nexport type PersonHighlightsProps = {\n person: PersonHighlightsPerson\n profile: PersonHighlightsProfile\n validators: PersonHighlightsValidators\n onDisplayNameSave: (value: string | null) => Promise<void>\n onPrimaryEmailSave: (value: string | null) => Promise<void>\n onPrimaryPhoneSave: (value: string | null) => Promise<void>\n onStatusSave: (value: string | null) => Promise<void>\n onNextInteractionSave: (value: NextInteractionPayload | null) => Promise<void>\n onDelete: () => void\n isDeleting: boolean\n onCompanySave: (companyId: string | null) => Promise<void>\n utilityActions?: React.ReactNode\n}\n\ntype CompanyInfo = { id: string; name: string }\n\nexport function PersonHighlights({\n person,\n profile,\n validators,\n onDisplayNameSave,\n onPrimaryEmailSave,\n onPrimaryPhoneSave,\n onStatusSave,\n onNextInteractionSave,\n onDelete,\n isDeleting,\n onCompanySave,\n utilityActions,\n}: PersonHighlightsProps) {\n const router = useRouter()\n const t = useT()\n const runMutation = React.useCallback(async (operation: () => Promise<void>) => operation(), [])\n const [editingCompany, setEditingCompany] = React.useState(false)\n const [companyDraftId, setCompanyDraftId] = React.useState<string>('')\n const [company, setCompany] = React.useState<CompanyInfo | null>(null)\n const [companyLoading, setCompanyLoading] = React.useState(false)\n const [companyError, setCompanyError] = React.useState<string | null>(null)\n const [companySaving, setCompanySaving] = React.useState(false)\n const companyHref = React.useMemo(\n () => (company ? `/backend/customers/companies/${encodeURIComponent(company.id)}` : null),\n [company],\n )\n const isCompanyInteractive = !editingCompany && !companyLoading && !companyError && Boolean(companyHref)\n\n const navigateToCompany = React.useCallback(() => {\n if (!companyHref) return\n router.push(companyHref)\n }, [companyHref])\n\n const handleCompanyClick = React.useCallback(\n (event: React.MouseEvent<HTMLDivElement>) => {\n if (!isCompanyInteractive || !companyHref) return\n const target = event.target as HTMLElement\n if (target.closest('button')) return\n if (event.defaultPrevented) return\n if (event.button !== 0) return\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {\n if (typeof window !== 'undefined') {\n window.open(companyHref, '_blank', 'noopener,noreferrer')\n }\n return\n }\n navigateToCompany()\n },\n [companyHref, isCompanyInteractive, navigateToCompany],\n )\n\n const handleCompanyKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (!isCompanyInteractive) return\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n navigateToCompany()\n }\n },\n [isCompanyInteractive, navigateToCompany],\n )\n\n const activeCompanyId = profile?.companyEntityId ?? null\n const historyFallbackId =\n profile?.id && profile.id !== person.id ? profile.id : undefined\n\n const loadCompany = React.useCallback(async (companyId: string | null) => {\n if (!companyId) {\n setCompany(null)\n return\n }\n setCompanyLoading(true)\n setCompanyError(null)\n try {\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/customers/companies?id=${encodeURIComponent(companyId)}`,\n undefined,\n { errorMessage: t('customers.people.detail.company.loadError', 'Unable to load company information.') },\n )\n const items = Array.isArray(payload?.items) ? (payload.items as Array<Record<string, unknown>>) : []\n const item = items.find((entry) => {\n if (!entry) return false\n const rawId = (entry as Record<string, unknown>).id\n if (typeof rawId === 'string') return rawId === companyId\n if (typeof rawId === 'number') return String(rawId) === companyId\n return false\n }) as Record<string, unknown> | undefined\n if (item && typeof item.display_name === 'string') {\n setCompany({ id: companyId, name: item.display_name })\n } else if (item && typeof item.displayName === 'string') {\n setCompany({ id: companyId, name: item.displayName })\n } else {\n setCompany({ id: companyId, name: t('customers.people.detail.company.unknown', 'Unknown company') })\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : t('customers.people.detail.company.loadError', 'Unable to load company.')\n setCompanyError(message)\n setCompany(null)\n } finally {\n setCompanyLoading(false)\n }\n }, [t])\n\n React.useEffect(() => {\n setCompanyDraftId(activeCompanyId ?? '')\n loadCompany(activeCompanyId).catch(() => {})\n }, [activeCompanyId, loadCompany])\n\n const handleCompanySave = React.useCallback(async () => {\n if (companySaving) return\n setCompanySaving(true)\n setCompanyError(null)\n const nextId = companyDraftId.trim()\n try {\n await runMutation(async () => {\n await onCompanySave(nextId.length ? nextId : null)\n })\n await loadCompany(nextId.length ? nextId : null)\n setEditingCompany(false)\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : t('customers.people.detail.company.saveError', 'Unable to update company.')\n setCompanyError(message)\n } finally {\n setCompanySaving(false)\n }\n }, [companyDraftId, companySaving, loadCompany, onCompanySave, runMutation, t])\n\n const handleCompanyClear = React.useCallback(async () => {\n if (companySaving) return\n setCompanySaving(true)\n setCompanyError(null)\n try {\n await runMutation(async () => {\n await onCompanySave(null)\n })\n await loadCompany(null)\n setCompanyDraftId('')\n setEditingCompany(false)\n } catch (err) {\n const message =\n err instanceof Error\n ? err.message\n : t('customers.people.detail.company.saveError', 'Unable to update company.')\n setCompanyError(message)\n } finally {\n setCompanySaving(false)\n }\n }, [companySaving, loadCompany, onCompanySave, runMutation, t])\n\n const companyPanel = (\n <div\n className={cn(\n 'group rounded-lg border bg-muted/30 p-3',\n isCompanyInteractive\n ? 'cursor-pointer transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n : null,\n )}\n role={isCompanyInteractive ? 'link' : undefined}\n tabIndex={isCompanyInteractive ? 0 : undefined}\n onClick={handleCompanyClick}\n onKeyDown={handleCompanyKeyDown}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex-1 space-y-1\">\n <p className=\"flex items-center gap-2 text-xs uppercase tracking-wide text-muted-foreground\">\n <Building2 aria-hidden className=\"h-3.5 w-3.5\" />\n {t('customers.people.detail.company.label', 'Company')}\n </p>\n {editingCompany ? (\n <div\n className=\"space-y-3\"\n onKeyDown={(event) => {\n if (event.key === 'Escape') {\n event.preventDefault()\n if (!companySaving) {\n setEditingCompany(false)\n setCompanyError(null)\n setCompanyDraftId(activeCompanyId ?? '')\n }\n }\n if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {\n event.preventDefault()\n if (!companySaving) {\n void handleCompanySave()\n }\n }\n }}\n >\n <CompanySelectField\n value={companyDraftId || undefined}\n onChange={(next) => setCompanyDraftId(next ?? '')}\n labels={{\n placeholder: t('customers.people.form.company.placeholder'),\n addLabel: t('customers.people.form.company.add'),\n addPrompt: t('customers.people.form.company.prompt'),\n dialogTitle: t('customers.people.form.company.dialogTitle'),\n inputLabel: t('customers.people.form.company.inputLabel'),\n inputPlaceholder: t('customers.people.form.company.inputPlaceholder'),\n emptyError: t('customers.people.form.dictionary.errorRequired'),\n cancelLabel: t('customers.people.form.dictionary.cancel'),\n saveLabel: t('customers.people.form.dictionary.save'),\n errorLoad: t('customers.people.form.dictionary.errorLoad'),\n errorSave: t('customers.people.form.dictionary.error'),\n loadingLabel: t('customers.people.form.company.loading'),\n }}\n />\n {companyError ? <p className=\"text-xs text-status-error-text\">{companyError}</p> : null}\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button type=\"button\" size=\"sm\" onClick={handleCompanySave} disabled={companySaving}>\n {companySaving ? <Loader2 className=\"mr-2 h-3.5 w-3.5 animate-spin\" /> : null}\n {t('ui.forms.actions.save')}\n </Button>\n <Button type=\"button\" size=\"sm\" variant=\"ghost\" onClick={() => {\n setEditingCompany(false)\n setCompanyError(null)\n setCompanyDraftId(activeCompanyId ?? '')\n }} disabled={companySaving}>\n {t('ui.forms.actions.cancel')}\n </Button>\n <Button type=\"button\" size=\"sm\" variant=\"secondary\" onClick={handleCompanyClear} disabled={companySaving}>\n {t('ui.forms.actions.clear')}\n </Button>\n </div>\n </div>\n ) : (\n <div className=\"flex items-center gap-2 text-sm\">\n {companyLoading ? (\n <span className=\"inline-flex items-center gap-2 text-muted-foreground\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n {t('customers.people.detail.company.loading', 'Loading company\u2026')}\n </span>\n ) : company ? (\n <span className=\"text-primary transition group-hover:underline\">\n {t('customers.people.detail.company.current', undefined, { company: company.name })}\n </span>\n ) : companyError ? (\n <span className=\"text-xs text-status-error-text\">{companyError}</span>\n ) : (\n <span className=\"text-muted-foreground\">\n {t('customers.people.detail.company.empty', 'No company assigned')}\n </span>\n )}\n </div>\n )}\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => {\n if (companySaving) return\n setEditingCompany((prev) => !prev)\n setCompanyError(null)\n setCompanyDraftId(activeCompanyId ?? '')\n }}\n className={\n editingCompany\n ? 'opacity-100'\n // The enclosing panel only activates once a company is assigned, and then it\n // navigates to that company instead of opening this editor \u2014 so without the\n // coarse-pointer reveal the Company field cannot be set or changed on touch.\n : 'opacity-0 transition-opacity duration-150 group-hover:opacity-100 hover:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100'\n }\n >\n {editingCompany ? <X className=\"h-4 w-4\" /> : <Pencil className=\"h-4 w-4\" />}\n <span className=\"sr-only\">\n {editingCompany ? t('ui.forms.actions.cancel') : t('ui.forms.actions.edit')}\n </span>\n </Button>\n </div>\n </div>\n )\n\n return (\n <div className=\"space-y-6\">\n <FormHeader\n mode=\"detail\"\n backHref=\"/backend/customers/people\"\n backLabel={t('customers.people.detail.actions.backToList')}\n utilityActions={(\n <>\n {utilityActions}\n <VersionHistoryAction\n config={{\n resourceKind: 'customers.person',\n resourceId: person.id,\n resourceIdFallback: historyFallbackId,\n organizationId: person.organizationId ?? undefined,\n }}\n t={t}\n />\n </>\n )}\n title={\n <InlineTextEditor\n label={t('customers.people.form.displayName.label')}\n value={person.displayName}\n placeholder={t('customers.people.form.displayName.placeholder')}\n emptyLabel={t('customers.people.detail.noValue')}\n validator={validators.displayName}\n onSave={onDisplayNameSave}\n hideLabel\n variant=\"plain\"\n activateOnClick\n triggerClassName=\"opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100\"\n containerClassName=\"max-w-full\"\n />\n }\n onDelete={() => {\n onDelete()\n }}\n isDeleting={isDeleting}\n deleteLabel={t('customers.people.list.actions.delete')}\n />\n\n {companyPanel}\n\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-4\">\n <InlineTextEditor\n label={t('customers.people.detail.highlights.primaryEmail')}\n value={person.primaryEmail || ''}\n placeholder={t('customers.people.form.primaryEmail')}\n emptyLabel={t('customers.people.detail.noValue')}\n type=\"email\"\n validator={validators.email}\n recordId={person.id}\n activateOnClick\n onSave={onPrimaryEmailSave}\n />\n <InlineTextEditor\n label={t('customers.people.detail.highlights.primaryPhone')}\n value={person.primaryPhone || ''}\n placeholder={t('customers.people.form.primaryPhone')}\n emptyLabel={t('customers.people.detail.noValue')}\n type=\"tel\"\n validator={validators.phone}\n recordId={person.id}\n activateOnClick\n onSave={onPrimaryPhoneSave}\n />\n <InlineDictionaryEditor\n label={t('customers.people.detail.highlights.status')}\n value={person.status ?? null}\n emptyLabel={t('customers.people.detail.noValue')}\n activateOnClick\n onSave={onStatusSave}\n kind=\"statuses\"\n />\n <InlineNextInteractionEditor\n label={t('customers.people.detail.highlights.nextInteraction')}\n valueAt={person.nextInteractionAt || null}\n valueName={person.nextInteractionName || null}\n valueRefId={person.nextInteractionRefId || null}\n valueIcon={person.nextInteractionIcon || null}\n valueColor={person.nextInteractionColor || null}\n emptyLabel={t('customers.people.detail.noValue')}\n onSave={onNextInteractionSave}\n activateOnClick\n />\n </div>\n </div>\n )\n}\n\nexport default PersonHighlights\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -139,7 +139,7 @@ function PlannedActivitiesSection({ activities, onComplete, onSchedule, onEdit,
139
139
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
140
140
  /* @__PURE__ */ jsx("span", { className: "text-sm font-medium block truncate", children: activity.title ?? activity.body ?? activity.interactionType }),
141
141
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
142
- /* @__PURE__ */ jsx("span", { children: activity.scheduledAt ? formatScheduledDate(activity.scheduledAt) : "" }),
142
+ /* @__PURE__ */ jsx("span", { children: activity.scheduledAt ? formatScheduledDate(activity.scheduledAt, t) : "" }),
143
143
  activity.authorName && /* @__PURE__ */ jsxs(Fragment, { children: [
144
144
  /* @__PURE__ */ jsx("span", { children: "\xB7" }),
145
145
  /* @__PURE__ */ jsx("span", { children: activity.authorName })
@@ -200,17 +200,16 @@ function formatRelativeOverdue(isoString, t) {
200
200
  return "";
201
201
  }
202
202
  }
203
- function formatScheduledDate(isoString) {
203
+ function formatScheduledDate(isoString, t) {
204
204
  try {
205
205
  const date = new Date(isoString);
206
- const now = /* @__PURE__ */ new Date();
207
- const tomorrow = new Date(now);
206
+ const tomorrow = /* @__PURE__ */ new Date();
208
207
  tomorrow.setDate(tomorrow.getDate() + 1);
209
208
  const dayName = date.toLocaleDateString(void 0, { weekday: "short" });
210
209
  const dateStr = date.toLocaleDateString(void 0, { day: "numeric", month: "short" });
211
210
  const timeStr = date.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
212
211
  if (isSameDay(date, tomorrow)) {
213
- return `Tomorrow ${timeStr}`;
212
+ return t("customers.timeline.planned.tomorrow", "Tomorrow {{time}}", { time: timeStr });
214
213
  }
215
214
  return `${dayName}, ${dateStr} \xB7 ${timeStr}`;
216
215
  } catch {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/customers/components/detail/PlannedActivitiesSection.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, Clock, AlertCircle, CalendarClock, Check, MoreHorizontal, Plus } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport type { InteractionSummary } from './types'\nimport { isSameDay } from '../../lib/localDay'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\ninterface PlannedActivitiesSectionProps {\n activities: InteractionSummary[]\n onComplete?: (id: string) => void\n onSchedule?: () => void\n onEdit?: (activity: InteractionSummary) => void\n onCancel?: (id: string) => void\n}\n\nexport function PlannedActivitiesSection({ activities, onComplete, onSchedule, onEdit, onCancel }: PlannedActivitiesSectionProps) {\n const t = useT()\n\n if (activities.length === 0) return null\n\n const now = new Date()\n\n const classified = activities.map((activity) => {\n const scheduledDate = activity.scheduledAt ? new Date(activity.scheduledAt) : null\n const isOverdue = scheduledDate ? scheduledDate < now : false\n return { ...activity, isOverdue }\n })\n\n const overdue = classified.filter((a) => a.isOverdue)\n const upcoming = classified.filter((a) => !a.isOverdue)\n\n return (\n <div className=\"rounded-lg border bg-card\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-4 py-3 border-b\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n <CalendarClock className=\"size-4 text-muted-foreground\" />\n {t('customers.timeline.planned.title', 'Planned activities')}\n <span className=\"rounded-full bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground\">\n {activities.length}\n </span>\n {overdue.length > 0 && (\n <span className=\"flex items-center gap-1 text-xs font-medium text-destructive\">\n <AlertCircle className=\"size-3\" />\n {overdue.length} {t('customers.timeline.planned.overdueCount', 'overdue')}\n </span>\n )}\n </div>\n {onSchedule && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"h-7 text-xs\" onClick={onSchedule}>\n <Plus className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.schedule', 'Schedule')}\n </Button>\n )}\n </div>\n\n {/* Items */}\n <div className=\"divide-y\">\n {overdue.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 border-l-2 border-l-destructive px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-destructive/10 shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-destructive\" /> : <AlertCircle className=\"size-4 text-destructive\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex items-center gap-1 font-medium text-destructive\">\n {t('customers.timeline.planned.overdue', 'Overdue')}\n </span>\n <span>\n {activity.scheduledAt ? formatRelativeOverdue(activity.scheduledAt, t) : ''}\n </span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n {onComplete && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-7 shrink-0 text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onComplete(activity.id)\n }}\n >\n <Check className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.markDone', 'Mark done')}\n </Button>\n )}\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n\n {upcoming.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-muted-foreground\" /> : <Clock className=\"size-4 text-muted-foreground\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{activity.scheduledAt ? formatScheduledDate(activity.scheduledAt) : ''}</span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\nfunction formatRelativeOverdue(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n const diffMs = now.getTime() - date.getTime()\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))\n if (diffDays <= 0) return t('customers.timeline.planned.overdueToday', 'due today')\n if (diffDays === 1) return t('customers.timeline.planned.overdueSince', 'since yesterday')\n return t('customers.timeline.planned.overdueDays', 'overdue {{days}} days', { days: diffDays })\n } catch {\n return ''\n }\n}\n\nfunction formatScheduledDate(isoString: string): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n const tomorrow = new Date(now)\n tomorrow.setDate(tomorrow.getDate() + 1)\n\n const dayName = date.toLocaleDateString(undefined, { weekday: 'short' })\n const dateStr = date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n\n if (isSameDay(date, tomorrow)) {\n return `Tomorrow ${timeStr}`\n }\n return `${dayName}, ${dateStr} \u00B7 ${timeStr}`\n } catch {\n return isoString\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { Phone, Mail, Users, StickyNote, Clock, AlertCircle, CalendarClock, Check, MoreHorizontal, Plus } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT, type TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport type { InteractionSummary } from './types'\nimport { isSameDay } from '../../lib/localDay'\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\ninterface PlannedActivitiesSectionProps {\n activities: InteractionSummary[]\n onComplete?: (id: string) => void\n onSchedule?: () => void\n onEdit?: (activity: InteractionSummary) => void\n onCancel?: (id: string) => void\n}\n\nexport function PlannedActivitiesSection({ activities, onComplete, onSchedule, onEdit, onCancel }: PlannedActivitiesSectionProps) {\n const t = useT()\n\n if (activities.length === 0) return null\n\n const now = new Date()\n\n const classified = activities.map((activity) => {\n const scheduledDate = activity.scheduledAt ? new Date(activity.scheduledAt) : null\n const isOverdue = scheduledDate ? scheduledDate < now : false\n return { ...activity, isOverdue }\n })\n\n const overdue = classified.filter((a) => a.isOverdue)\n const upcoming = classified.filter((a) => !a.isOverdue)\n\n return (\n <div className=\"rounded-lg border bg-card\">\n {/* Header */}\n <div className=\"flex items-center justify-between px-4 py-3 border-b\">\n <div className=\"flex items-center gap-2 text-sm font-medium\">\n <CalendarClock className=\"size-4 text-muted-foreground\" />\n {t('customers.timeline.planned.title', 'Planned activities')}\n <span className=\"rounded-full bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground\">\n {activities.length}\n </span>\n {overdue.length > 0 && (\n <span className=\"flex items-center gap-1 text-xs font-medium text-destructive\">\n <AlertCircle className=\"size-3\" />\n {overdue.length} {t('customers.timeline.planned.overdueCount', 'overdue')}\n </span>\n )}\n </div>\n {onSchedule && (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"h-7 text-xs\" onClick={onSchedule}>\n <Plus className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.schedule', 'Schedule')}\n </Button>\n )}\n </div>\n\n {/* Items */}\n <div className=\"divide-y\">\n {overdue.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 border-l-2 border-l-destructive px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-destructive/10 shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-destructive\" /> : <AlertCircle className=\"size-4 text-destructive\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex items-center gap-1 font-medium text-destructive\">\n {t('customers.timeline.planned.overdue', 'Overdue')}\n </span>\n <span>\n {activity.scheduledAt ? formatRelativeOverdue(activity.scheduledAt, t) : ''}\n </span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n {onComplete && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"h-7 shrink-0 text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onComplete(activity.id)\n }}\n >\n <Check className=\"mr-1 size-3\" />\n {t('customers.timeline.planned.markDone', 'Mark done')}\n </Button>\n )}\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n\n {upcoming.map((activity) => {\n const TypeIcon = TYPE_ICONS[activity.interactionType]\n return (\n <div\n key={activity.id}\n className=\"flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-accent/50 transition-colors\"\n onClick={() => onEdit?.(activity)}\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => { if (e.key === 'Enter') onEdit?.(activity) }}\n >\n <div className=\"flex items-center justify-center size-8 rounded-full bg-muted shrink-0\">\n {TypeIcon ? <TypeIcon className=\"size-4 text-muted-foreground\" /> : <Clock className=\"size-4 text-muted-foreground\" />}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-medium block truncate\">\n {activity.title ?? activity.body ?? activity.interactionType}\n </span>\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <span>{activity.scheduledAt ? formatScheduledDate(activity.scheduledAt, t) : ''}</span>\n {activity.authorName && (\n <>\n <span>\u00B7</span>\n <span>{activity.authorName}</span>\n </>\n )}\n </div>\n </div>\n <Popover>\n <PopoverTrigger asChild>\n <IconButton type=\"button\" variant=\"ghost\" size=\"xs\" aria-label={t('customers.timeline.more', 'More')} onClick={(e) => e.stopPropagation()}>\n <MoreHorizontal className=\"size-3.5\" />\n </IconButton>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-40 p-1\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs\"\n onClick={(event) => {\n event.stopPropagation()\n onEdit?.(activity)\n }}\n >\n {t('customers.timeline.planned.reschedule', 'Reschedule')}\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full justify-start text-xs text-destructive hover:text-destructive\"\n onClick={(event) => {\n event.stopPropagation()\n onCancel?.(activity.id)\n }}\n >\n {t('customers.timeline.planned.cancel', 'Cancel')}\n </Button>\n </PopoverContent>\n </Popover>\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\nfunction formatRelativeOverdue(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const now = new Date()\n const diffMs = now.getTime() - date.getTime()\n const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))\n if (diffDays <= 0) return t('customers.timeline.planned.overdueToday', 'due today')\n if (diffDays === 1) return t('customers.timeline.planned.overdueSince', 'since yesterday')\n return t('customers.timeline.planned.overdueDays', 'overdue {{days}} days', { days: diffDays })\n } catch {\n return ''\n }\n}\n\nfunction formatScheduledDate(isoString: string, t: TranslateFn): string {\n try {\n const date = new Date(isoString)\n const tomorrow = new Date()\n tomorrow.setDate(tomorrow.getDate() + 1)\n\n const dayName = date.toLocaleDateString(undefined, { weekday: 'short' })\n const dateStr = date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n\n if (isSameDay(date, tomorrow)) {\n return t('customers.timeline.planned.tomorrow', 'Tomorrow {{time}}', { time: timeStr })\n }\n return `${dayName}, ${dateStr} \u00B7 ${timeStr}`\n } catch {\n return isoString\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/data_sync/di.ts"],
4
- "sourcesContent": ["import { asFunction, asValue } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CredentialsService } from '../integrations/lib/credentials-service'\nimport type { IntegrationLogService } from '../integrations/lib/log-service'\nimport type { IntegrationStateService } from '../integrations/lib/state-service'\nimport type { ProgressService } from '../progress/lib/progressService'\nimport { SyncCursor, SyncMapping, SyncRun, SyncSchedule } from './data/entities'\nimport { createExternalIdMappingService } from './lib/id-mapping'\nimport { createSyncRunService } from './lib/sync-run-service'\nimport { createSyncScheduleService } from './lib/sync-schedule-service'\nimport { createSyncEngine } from './lib/sync-engine'\n\ntype Cradle = {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n integrationLogService: IntegrationLogService\n integrationStateService: IntegrationStateService\n progressService: ProgressService\n schedulerService?: {\n register: (registration: Record<string, unknown>) => Promise<void>\n unregister: (scheduleId: string) => Promise<void>\n }\n}\n\nexport function register(container: AppContainer) {\n container.register({\n externalIdMappingService: asFunction(({ em }: Cradle) => createExternalIdMappingService(em)).scoped().proxy(),\n dataSyncRunService: asFunction(({ em }: Cradle) => createSyncRunService(em)).scoped().proxy(),\n dataSyncScheduleService: asFunction(({ em, schedulerService }: Cradle) => createSyncScheduleService(em, schedulerService)).scoped().proxy(),\n dataSyncEngine: asFunction(({ em, dataSyncRunService, integrationCredentialsService, integrationLogService, integrationStateService, progressService }: Cradle & {\n dataSyncRunService: ReturnType<typeof createSyncRunService>\n }) => createSyncEngine({\n em,\n syncRunService: dataSyncRunService,\n integrationCredentialsService,\n integrationLogService,\n integrationStateService,\n progressService,\n })).scoped().proxy(),\n\n SyncRun: asValue(SyncRun),\n SyncCursor: asValue(SyncCursor),\n SyncMapping: asValue(SyncMapping),\n SyncSchedule: asValue(SyncSchedule),\n })\n}\n"],
5
- "mappings": "AAAA,SAAS,YAAY,eAAe;AAOpC,SAAS,YAAY,aAAa,SAAS,oBAAoB;AAC/D,SAAS,sCAAsC;AAC/C,SAAS,4BAA4B;AACrC,SAAS,iCAAiC;AAC1C,SAAS,wBAAwB;AAc1B,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,0BAA0B,WAAW,CAAC,EAAE,GAAG,MAAc,+BAA+B,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAC5G,oBAAoB,WAAW,CAAC,EAAE,GAAG,MAAc,qBAAqB,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAC5F,yBAAyB,WAAW,CAAC,EAAE,IAAI,iBAAiB,MAAc,0BAA0B,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAC1I,gBAAgB,WAAW,CAAC,EAAE,IAAI,oBAAoB,+BAA+B,uBAAuB,yBAAyB,gBAAgB,MAE/I,iBAAiB;AAAA,MACrB;AAAA,MACA,gBAAgB;AAAA,MAChB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAEnB,SAAS,QAAQ,OAAO;AAAA,IACxB,YAAY,QAAQ,UAAU;AAAA,IAC9B,aAAa,QAAQ,WAAW;AAAA,IAChC,cAAc,QAAQ,YAAY;AAAA,EACpC,CAAC;AACH;",
4
+ "sourcesContent": ["import { asFunction, asValue } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { CredentialsService } from '../integrations/lib/credentials-service'\nimport type { IntegrationLogService } from '../integrations/lib/log-service'\nimport type { IntegrationStateService } from '../integrations/lib/state-service'\nimport type { ProgressService } from '../progress/lib/progressService'\nimport { SyncCursor, SyncMapping, SyncRun, SyncSchedule } from './data/entities'\nimport { createExternalIdMappingService } from './lib/id-mapping'\nimport { createSyncRunService } from './lib/sync-run-service'\nimport { createSyncScheduleService } from './lib/sync-schedule-service'\nimport { createSyncEngine } from './lib/sync-engine'\n\ntype Cradle = {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n integrationLogService: IntegrationLogService\n integrationStateService: IntegrationStateService\n progressService: ProgressService\n schedulerService?: {\n register: (registration: Record<string, unknown>) => Promise<void>\n unregister: (scheduleId: string) => Promise<void>\n exists?: (scheduleId: string) => Promise<boolean>\n }\n}\n\nexport function register(container: AppContainer) {\n container.register({\n externalIdMappingService: asFunction(({ em }: Cradle) => createExternalIdMappingService(em)).scoped().proxy(),\n dataSyncRunService: asFunction(({ em }: Cradle) => createSyncRunService(em)).scoped().proxy(),\n dataSyncScheduleService: asFunction(({ em, schedulerService }: Cradle) => createSyncScheduleService(em, schedulerService)).scoped().proxy(),\n dataSyncEngine: asFunction(({ em, dataSyncRunService, integrationCredentialsService, integrationLogService, integrationStateService, progressService }: Cradle & {\n dataSyncRunService: ReturnType<typeof createSyncRunService>\n }) => createSyncEngine({\n em,\n syncRunService: dataSyncRunService,\n integrationCredentialsService,\n integrationLogService,\n integrationStateService,\n progressService,\n })).scoped().proxy(),\n\n SyncRun: asValue(SyncRun),\n SyncCursor: asValue(SyncCursor),\n SyncMapping: asValue(SyncMapping),\n SyncSchedule: asValue(SyncSchedule),\n })\n}\n"],
5
+ "mappings": "AAAA,SAAS,YAAY,eAAe;AAOpC,SAAS,YAAY,aAAa,SAAS,oBAAoB;AAC/D,SAAS,sCAAsC;AAC/C,SAAS,4BAA4B;AACrC,SAAS,iCAAiC;AAC1C,SAAS,wBAAwB;AAe1B,SAAS,SAAS,WAAyB;AAChD,YAAU,SAAS;AAAA,IACjB,0BAA0B,WAAW,CAAC,EAAE,GAAG,MAAc,+BAA+B,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAC5G,oBAAoB,WAAW,CAAC,EAAE,GAAG,MAAc,qBAAqB,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAC5F,yBAAyB,WAAW,CAAC,EAAE,IAAI,iBAAiB,MAAc,0BAA0B,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAC1I,gBAAgB,WAAW,CAAC,EAAE,IAAI,oBAAoB,+BAA+B,uBAAuB,yBAAyB,gBAAgB,MAE/I,iBAAiB;AAAA,MACrB;AAAA,MACA,gBAAgB;AAAA,MAChB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM;AAAA,IAEnB,SAAS,QAAQ,OAAO;AAAA,IACxB,YAAY,QAAQ,UAAU;AAAA,IAC9B,aAAa,QAAQ,WAAW;AAAA,IAChC,cAAc,QAAQ,YAAY;AAAA,EACpC,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,10 @@ import { createLogger } from "@open-mercato/shared/lib/logger";
5
5
  import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
6
6
  import { SyncSchedule } from "../data/entities.js";
7
7
  const logger = createLogger("data_sync").child({ component: "sync-schedule-service" });
8
+ async function scheduledJobExists(scheduler, scheduledJobId) {
9
+ if (!scheduler.exists) return false;
10
+ return scheduler.exists(scheduledJobId);
11
+ }
8
12
  function createSyncScheduleService(em, schedulerService) {
9
13
  function requireScheduler() {
10
14
  if (!schedulerService) {
@@ -91,7 +95,9 @@ function createSyncScheduleService(em, schedulerService) {
91
95
  }
92
96
  const id = existing?.id ?? randomUUID();
93
97
  const scheduledJobId = existing?.scheduledJobId ?? id;
94
- await requireScheduler().register({
98
+ const scheduler = requireScheduler();
99
+ const mintsRegistration = existing ? !existing.scheduledJobId && !await scheduledJobExists(scheduler, scheduledJobId) : true;
100
+ await scheduler.register({
95
101
  id: scheduledJobId,
96
102
  name: buildScheduleName(input),
97
103
  description: buildScheduleDescription(input),
@@ -112,33 +118,60 @@ function createSyncScheduleService(em, schedulerService) {
112
118
  sourceModule: "data_sync",
113
119
  isEnabled: input.isEnabled
114
120
  });
115
- const row = existing ?? em.create(SyncSchedule, {
116
- id,
117
- integrationId: input.integrationId,
118
- entityType: input.entityType,
119
- direction: input.direction,
120
- scheduleType: input.scheduleType,
121
- scheduleValue: input.scheduleValue,
122
- timezone: input.timezone,
123
- fullSync: input.fullSync,
124
- isEnabled: input.isEnabled,
125
- organizationId: scope.organizationId,
126
- tenantId: scope.tenantId
127
- });
128
- row.integrationId = input.integrationId;
129
- row.entityType = input.entityType;
130
- row.direction = input.direction;
131
- row.scheduleType = input.scheduleType;
132
- row.scheduleValue = input.scheduleValue;
133
- row.timezone = input.timezone;
134
- row.fullSync = input.fullSync;
135
- row.isEnabled = input.isEnabled;
136
- row.scheduledJobId = scheduledJobId;
137
- if (!existing) {
138
- em.persist(row);
121
+ try {
122
+ const row = existing ?? em.create(SyncSchedule, {
123
+ id,
124
+ integrationId: input.integrationId,
125
+ entityType: input.entityType,
126
+ direction: input.direction,
127
+ scheduleType: input.scheduleType,
128
+ scheduleValue: input.scheduleValue,
129
+ timezone: input.timezone,
130
+ fullSync: input.fullSync,
131
+ isEnabled: input.isEnabled,
132
+ organizationId: scope.organizationId,
133
+ tenantId: scope.tenantId
134
+ });
135
+ row.integrationId = input.integrationId;
136
+ row.entityType = input.entityType;
137
+ row.direction = input.direction;
138
+ row.scheduleType = input.scheduleType;
139
+ row.scheduleValue = input.scheduleValue;
140
+ row.timezone = input.timezone;
141
+ row.fullSync = input.fullSync;
142
+ row.isEnabled = input.isEnabled;
143
+ row.scheduledJobId = scheduledJobId;
144
+ if (!existing) {
145
+ em.persist(row);
146
+ }
147
+ await em.flush();
148
+ return row;
149
+ } catch (error) {
150
+ if (mintsRegistration) {
151
+ try {
152
+ await scheduler.unregister(scheduledJobId);
153
+ } catch (compensationError) {
154
+ logger.error("Failed to unregister the scheduled job after a schedule write failure", {
155
+ scheduledJobId,
156
+ scheduleId: id,
157
+ organizationId: scope.organizationId,
158
+ tenantId: scope.tenantId,
159
+ err: compensationError
160
+ });
161
+ getTelemetryRuntime()?.reportError(compensationError, {
162
+ module: "data_sync",
163
+ code: "data_sync.schedule_save_compensation_failed",
164
+ attributes: {
165
+ scheduledJobId,
166
+ scheduleId: id,
167
+ organizationId: scope.organizationId,
168
+ tenantId: scope.tenantId
169
+ }
170
+ });
171
+ }
172
+ }
173
+ throw error;
139
174
  }
140
- await em.flush();
141
- return row;
142
175
  },
143
176
  async deleteSchedule(id, scope, container, expectedUpdatedAt) {
144
177
  const row = await getById(id, scope);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/sync-schedule-service.ts"],
4
- "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { findAndCountWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { enforceCommandOptimisticLockWithGuards, enforceRecordGoneIsConflict } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { SyncSchedule } from '../data/entities'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-schedule-service' })\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n}\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'organization'\n organizationId: string\n tenantId: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload: Record<string, unknown>\n requireFeature?: string\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n unregister: (scheduleId: string) => Promise<void>\n}\n\nexport function createSyncScheduleService(em: EntityManager, schedulerService?: SchedulerServiceLike) {\n function requireScheduler(): SchedulerServiceLike {\n if (!schedulerService) {\n throw new Error('Scheduler module is not available')\n }\n return schedulerService\n }\n\n function buildScheduleName(row: { integrationId: string; entityType: string; direction: 'import' | 'export' }): string {\n return `Data sync: ${row.integrationId} ${row.entityType} ${row.direction}`\n }\n\n function buildScheduleDescription(row: { integrationId: string; entityType: string; direction: 'import' | 'export' }): string {\n return `Scheduled ${row.direction} for ${row.integrationId} (${row.entityType})`\n }\n\n async function getById(id: string, scope: SyncScope): Promise<SyncSchedule | null> {\n return findOneWithDecryption(\n em,\n SyncSchedule,\n {\n id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n }\n\n async function getByKey(\n integrationId: string,\n entityType: string,\n direction: 'import' | 'export',\n scope: SyncScope,\n ): Promise<SyncSchedule | null> {\n return findOneWithDecryption(\n em,\n SyncSchedule,\n {\n integrationId,\n entityType,\n direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n }\n\n return {\n getById,\n getByKey,\n\n async listSchedules(query: {\n integrationId?: string\n entityType?: string\n direction?: 'import' | 'export'\n page: number\n pageSize: number\n }, scope: SyncScope): Promise<{ items: SyncSchedule[]; total: number }> {\n const where: FilterQuery<SyncSchedule> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (query.integrationId) where.integrationId = query.integrationId\n if (query.entityType) where.entityType = query.entityType\n if (query.direction) where.direction = query.direction\n\n const [items, total] = await findAndCountWithDecryption(\n em,\n SyncSchedule,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n scope,\n )\n\n return { items, total }\n },\n\n async saveSchedule(input: {\n id?: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n fullSync: boolean\n isEnabled: boolean\n expectedUpdatedAt?: string | null\n }, scope: SyncScope, container?: AwilixContainer): Promise<SyncSchedule> {\n const existing = input.id\n ? await getById(input.id, scope)\n : await getByKey(input.integrationId, input.entityType, input.direction, scope)\n\n if (existing && container) {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'data_sync.schedule',\n resourceId: existing.id,\n current: existing.updatedAt ?? null,\n expected: input.expectedUpdatedAt ?? null,\n })\n } else if (!existing && input.expectedUpdatedAt) {\n // Concurrent delete-then-edit race: the client edited a schedule that was\n // removed before this keyed upsert ran. Surface the unified conflict\n // instead of silently re-creating it. No-op when no expected version was\n // sent (a genuine create) or when OM_OPTIMISTIC_LOCK is off.\n enforceRecordGoneIsConflict({\n resourceKind: 'data_sync.schedule',\n resourceId: input.id ?? `${input.integrationId}:${input.entityType}:${input.direction}`,\n expected: input.expectedUpdatedAt,\n })\n }\n\n const id = existing?.id ?? randomUUID()\n const scheduledJobId = existing?.scheduledJobId ?? id\n\n // Validate the schedule (and register it with the scheduler) before writing\n // the SyncSchedule row \u2014 an unparseable scheduleValue must not leave a\n // persisted row with no working schedule behind it.\n await requireScheduler().register({\n id: scheduledJobId,\n name: buildScheduleName(input),\n description: buildScheduleDescription(input),\n scopeType: 'organization',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n scheduleType: input.scheduleType,\n scheduleValue: input.scheduleValue,\n timezone: input.timezone,\n targetType: 'queue',\n targetQueue: 'data-sync-scheduled',\n targetPayload: {\n scheduleId: id,\n scope,\n },\n requireFeature: 'data_sync.run',\n sourceType: 'module',\n sourceModule: 'data_sync',\n isEnabled: input.isEnabled,\n })\n\n const row = existing ?? em.create(SyncSchedule, {\n id,\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n scheduleType: input.scheduleType,\n scheduleValue: input.scheduleValue,\n timezone: input.timezone,\n fullSync: input.fullSync,\n isEnabled: input.isEnabled,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n row.integrationId = input.integrationId\n row.entityType = input.entityType\n row.direction = input.direction\n row.scheduleType = input.scheduleType\n row.scheduleValue = input.scheduleValue\n row.timezone = input.timezone\n row.fullSync = input.fullSync\n row.isEnabled = input.isEnabled\n row.scheduledJobId = scheduledJobId\n\n if (!existing) {\n em.persist(row)\n }\n\n await em.flush()\n\n return row\n },\n\n async deleteSchedule(\n id: string,\n scope: SyncScope,\n container?: AwilixContainer,\n expectedUpdatedAt?: string | null,\n ): Promise<boolean> {\n const row = await getById(id, scope)\n if (!row) return false\n\n if (container) {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'data_sync.schedule',\n resourceId: row.id,\n current: row.updatedAt ?? null,\n expected: expectedUpdatedAt ?? null,\n })\n }\n\n const scheduledJobId = row.scheduledJobId ?? row.id\n const wasEnabled = row.isEnabled\n await requireScheduler().unregister(scheduledJobId)\n\n try {\n row.deletedAt = new Date()\n row.isEnabled = false\n await em.flush()\n } catch (error) {\n try {\n await requireScheduler().register({\n id: scheduledJobId,\n name: buildScheduleName(row),\n description: buildScheduleDescription(row),\n scopeType: 'organization',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n scheduleType: row.scheduleType,\n scheduleValue: row.scheduleValue,\n timezone: row.timezone,\n targetType: 'queue',\n targetQueue: 'data-sync-scheduled',\n targetPayload: {\n scheduleId: row.id,\n scope,\n },\n requireFeature: 'data_sync.run',\n sourceType: 'module',\n sourceModule: 'data_sync',\n isEnabled: wasEnabled,\n })\n } catch (compensationError) {\n logger.error('Failed to restore scheduled job after a failed schedule delete', {\n scheduledJobId,\n scheduleId: row.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n error: compensationError instanceof Error ? compensationError.message : String(compensationError),\n })\n getTelemetryRuntime()?.reportError(compensationError, {\n module: 'data_sync',\n code: 'data_sync.schedule_delete_compensation_failed',\n attributes: {\n scheduledJobId,\n scheduleId: row.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n })\n }\n throw error\n }\n\n return true\n },\n }\n}\n\nexport type SyncScheduleService = ReturnType<typeof createSyncScheduleService>\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { AwilixContainer } from 'awilix'\nimport type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { findAndCountWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { enforceCommandOptimisticLockWithGuards, enforceRecordGoneIsConflict } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { SyncSchedule } from '../data/entities'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-schedule-service' })\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n}\n\ntype SchedulerServiceLike = {\n register: (registration: {\n id: string\n name: string\n description?: string\n scopeType: 'organization'\n organizationId: string\n tenantId: string\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone?: string\n targetType: 'queue'\n targetQueue: string\n targetPayload: Record<string, unknown>\n requireFeature?: string\n sourceType: 'module'\n sourceModule: string\n isEnabled?: boolean\n }) => Promise<void>\n unregister: (scheduleId: string) => Promise<void>\n exists?: (scheduleId: string) => Promise<boolean>\n}\n\nasync function scheduledJobExists(scheduler: SchedulerServiceLike, scheduledJobId: string): Promise<boolean> {\n // A scheduler substitute that cannot answer keeps the pre-existing behaviour:\n // treat the registration as minted, which compensates rather than stranding an\n // orphan on the create path where this is by far the likelier outcome.\n if (!scheduler.exists) return false\n return scheduler.exists(scheduledJobId)\n}\n\nexport function createSyncScheduleService(em: EntityManager, schedulerService?: SchedulerServiceLike) {\n function requireScheduler(): SchedulerServiceLike {\n if (!schedulerService) {\n throw new Error('Scheduler module is not available')\n }\n return schedulerService\n }\n\n function buildScheduleName(row: { integrationId: string; entityType: string; direction: 'import' | 'export' }): string {\n return `Data sync: ${row.integrationId} ${row.entityType} ${row.direction}`\n }\n\n function buildScheduleDescription(row: { integrationId: string; entityType: string; direction: 'import' | 'export' }): string {\n return `Scheduled ${row.direction} for ${row.integrationId} (${row.entityType})`\n }\n\n async function getById(id: string, scope: SyncScope): Promise<SyncSchedule | null> {\n return findOneWithDecryption(\n em,\n SyncSchedule,\n {\n id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n }\n\n async function getByKey(\n integrationId: string,\n entityType: string,\n direction: 'import' | 'export',\n scope: SyncScope,\n ): Promise<SyncSchedule | null> {\n return findOneWithDecryption(\n em,\n SyncSchedule,\n {\n integrationId,\n entityType,\n direction,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n }\n\n return {\n getById,\n getByKey,\n\n async listSchedules(query: {\n integrationId?: string\n entityType?: string\n direction?: 'import' | 'export'\n page: number\n pageSize: number\n }, scope: SyncScope): Promise<{ items: SyncSchedule[]; total: number }> {\n const where: FilterQuery<SyncSchedule> = {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (query.integrationId) where.integrationId = query.integrationId\n if (query.entityType) where.entityType = query.entityType\n if (query.direction) where.direction = query.direction\n\n const [items, total] = await findAndCountWithDecryption(\n em,\n SyncSchedule,\n where,\n {\n orderBy: { createdAt: 'DESC' },\n limit: query.pageSize,\n offset: (query.page - 1) * query.pageSize,\n },\n scope,\n )\n\n return { items, total }\n },\n\n async saveSchedule(input: {\n id?: string\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n fullSync: boolean\n isEnabled: boolean\n expectedUpdatedAt?: string | null\n }, scope: SyncScope, container?: AwilixContainer): Promise<SyncSchedule> {\n const existing = input.id\n ? await getById(input.id, scope)\n : await getByKey(input.integrationId, input.entityType, input.direction, scope)\n\n if (existing && container) {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'data_sync.schedule',\n resourceId: existing.id,\n current: existing.updatedAt ?? null,\n expected: input.expectedUpdatedAt ?? null,\n })\n } else if (!existing && input.expectedUpdatedAt) {\n // Concurrent delete-then-edit race: the client edited a schedule that was\n // removed before this keyed upsert ran. Surface the unified conflict\n // instead of silently re-creating it. No-op when no expected version was\n // sent (a genuine create) or when OM_OPTIMISTIC_LOCK is off.\n enforceRecordGoneIsConflict({\n resourceKind: 'data_sync.schedule',\n resourceId: input.id ?? `${input.integrationId}:${input.entityType}:${input.direction}`,\n expected: input.expectedUpdatedAt,\n })\n }\n\n const id = existing?.id ?? randomUUID()\n const scheduledJobId = existing?.scheduledJobId ?? id\n const scheduler = requireScheduler()\n // Only a registration this call mints may be compensated below. A create\n // mints a fresh id, so no job can already live at it. An update of a row\n // whose scheduledJobId is null is the ambiguous case \u2014 deleteSchedule reads\n // that null as \"the job lives at row.id\", and register() upserts on id \u2014 so\n // ask the scheduler instead of assuming, otherwise compensation would\n // delete a job this call merely overwrote.\n const mintsRegistration = existing\n ? !existing.scheduledJobId && !(await scheduledJobExists(scheduler, scheduledJobId))\n : true\n\n // Validate the schedule (and register it with the scheduler) before writing\n // the SyncSchedule row \u2014 an unparseable scheduleValue must not leave a\n // persisted row with no working schedule behind it.\n await scheduler.register({\n id: scheduledJobId,\n name: buildScheduleName(input),\n description: buildScheduleDescription(input),\n scopeType: 'organization',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n scheduleType: input.scheduleType,\n scheduleValue: input.scheduleValue,\n timezone: input.timezone,\n targetType: 'queue',\n targetQueue: 'data-sync-scheduled',\n targetPayload: {\n scheduleId: id,\n scope,\n },\n requireFeature: 'data_sync.run',\n sourceType: 'module',\n sourceModule: 'data_sync',\n isEnabled: input.isEnabled,\n })\n\n try {\n const row = existing ?? em.create(SyncSchedule, {\n id,\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n scheduleType: input.scheduleType,\n scheduleValue: input.scheduleValue,\n timezone: input.timezone,\n fullSync: input.fullSync,\n isEnabled: input.isEnabled,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n\n row.integrationId = input.integrationId\n row.entityType = input.entityType\n row.direction = input.direction\n row.scheduleType = input.scheduleType\n row.scheduleValue = input.scheduleValue\n row.timezone = input.timezone\n row.fullSync = input.fullSync\n row.isEnabled = input.isEnabled\n row.scheduledJobId = scheduledJobId\n\n if (!existing) {\n em.persist(row)\n }\n\n await em.flush()\n\n return row\n } catch (error: unknown) {\n // The registration above is already durable, so a failed write would\n // otherwise strand a live ScheduledJob the data-sync page cannot explain\n // or delete. Only a registration this call minted is compensated: when a\n // job was already there, register() overwrote one we inherited, and\n // SchedulerServiceLike offers no way to restore the previous definition,\n // so unregistering would destroy that job rather than roll anything back.\n if (mintsRegistration) {\n try {\n await scheduler.unregister(scheduledJobId)\n } catch (compensationError: unknown) {\n logger.error('Failed to unregister the scheduled job after a schedule write failure', {\n scheduledJobId,\n scheduleId: id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n err: compensationError,\n })\n getTelemetryRuntime()?.reportError(compensationError, {\n module: 'data_sync',\n code: 'data_sync.schedule_save_compensation_failed',\n attributes: {\n scheduledJobId,\n scheduleId: id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n })\n }\n }\n throw error\n }\n },\n\n async deleteSchedule(\n id: string,\n scope: SyncScope,\n container?: AwilixContainer,\n expectedUpdatedAt?: string | null,\n ): Promise<boolean> {\n const row = await getById(id, scope)\n if (!row) return false\n\n if (container) {\n await enforceCommandOptimisticLockWithGuards(container, {\n resourceKind: 'data_sync.schedule',\n resourceId: row.id,\n current: row.updatedAt ?? null,\n expected: expectedUpdatedAt ?? null,\n })\n }\n\n const scheduledJobId = row.scheduledJobId ?? row.id\n const wasEnabled = row.isEnabled\n await requireScheduler().unregister(scheduledJobId)\n\n try {\n row.deletedAt = new Date()\n row.isEnabled = false\n await em.flush()\n } catch (error) {\n try {\n await requireScheduler().register({\n id: scheduledJobId,\n name: buildScheduleName(row),\n description: buildScheduleDescription(row),\n scopeType: 'organization',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n scheduleType: row.scheduleType,\n scheduleValue: row.scheduleValue,\n timezone: row.timezone,\n targetType: 'queue',\n targetQueue: 'data-sync-scheduled',\n targetPayload: {\n scheduleId: row.id,\n scope,\n },\n requireFeature: 'data_sync.run',\n sourceType: 'module',\n sourceModule: 'data_sync',\n isEnabled: wasEnabled,\n })\n } catch (compensationError) {\n logger.error('Failed to restore scheduled job after a failed schedule delete', {\n scheduledJobId,\n scheduleId: row.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n error: compensationError instanceof Error ? compensationError.message : String(compensationError),\n })\n getTelemetryRuntime()?.reportError(compensationError, {\n module: 'data_sync',\n code: 'data_sync.schedule_delete_compensation_failed',\n attributes: {\n scheduledJobId,\n scheduleId: row.id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n })\n }\n throw error\n }\n\n return true\n },\n }\n}\n\nexport type SyncScheduleService = ReturnType<typeof createSyncScheduleService>\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -17,7 +17,7 @@ import {
17
17
  resolveActorFeatures,
18
18
  resolveMessageChannelThreadAccess
19
19
  } from "../lib/channelThreadAccess.js";
20
- import { MESSAGE_ATTACHMENT_ENTITY_ID, MESSAGE_ENTITY_ID } from "../lib/constants.js";
20
+ import { MESSAGE_ATTACHMENT_ENTITY_ID, MESSAGE_ENTITY_ID, SYSTEM_SENDER_USER_ID } from "../lib/constants.js";
21
21
  import { canUseChannelThreadFallback } from "../lib/routeHelpers.js";
22
22
  import { getMessageTypeOrDefault } from "../lib/message-types-registry.js";
23
23
  import { validateMessageObjectsForType } from "../lib/object-validation.js";
@@ -583,21 +583,22 @@ const replyMessageCommand = {
583
583
  }
584
584
  const messageType = getMessageTypeOrDefault(original.type);
585
585
  if (messageType.allowReply === false) throw new Error("Reply is not allowed for this message type");
586
+ const isReplyRecipient = (userId) => userId !== input.userId && userId !== SYSTEM_SENDER_USER_ID;
586
587
  const recipientIds = new Set(
587
- (input.recipients ?? []).map((recipient) => recipient.userId).filter((recipientUserId) => recipientUserId !== input.userId)
588
+ (input.recipients ?? []).map((recipient) => recipient.userId).filter(isReplyRecipient)
588
589
  );
589
590
  if (recipientIds.size === 0) {
590
591
  const originalRecipients = await em.find(MessageRecipient, { messageId: original.id, deletedAt: null });
591
592
  if (input.replyAll) {
592
- if (original.senderUserId !== input.userId) recipientIds.add(original.senderUserId);
593
+ if (isReplyRecipient(original.senderUserId)) recipientIds.add(original.senderUserId);
593
594
  for (const recipient of originalRecipients) {
594
- if (recipient.recipientUserId !== input.userId) recipientIds.add(recipient.recipientUserId);
595
+ if (isReplyRecipient(recipient.recipientUserId)) recipientIds.add(recipient.recipientUserId);
595
596
  }
596
597
  } else if (original.senderUserId !== input.userId) {
597
- recipientIds.add(original.senderUserId);
598
+ if (isReplyRecipient(original.senderUserId)) recipientIds.add(original.senderUserId);
598
599
  } else {
599
600
  for (const recipient of originalRecipients) {
600
- if (recipient.recipientUserId !== input.userId) {
601
+ if (isReplyRecipient(recipient.recipientUserId)) {
601
602
  recipientIds.add(recipient.recipientUserId);
602
603
  break;
603
604
  }
@@ -607,7 +608,9 @@ const replyMessageCommand = {
607
608
  recipientIds.add(input.userId);
608
609
  }
609
610
  }
610
- if (recipientIds.size === 0) throw new Error("No recipients available for reply");
611
+ if (recipientIds.size === 0 && original.senderUserId !== SYSTEM_SENDER_USER_ID) {
612
+ throw new Error("No recipients available for reply");
613
+ }
611
614
  let messageId = "";
612
615
  let responseExternalEmail = null;
613
616
  await em.transactional(async (trx) => {