@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/business_rules/data/validators.js +8 -3
- package/dist/modules/business_rules/data/validators.js.map +2 -2
- package/dist/modules/catalog/api/dictionaries/[key]/route.js +35 -7
- package/dist/modules/catalog/api/dictionaries/[key]/route.js.map +2 -2
- package/dist/modules/communication_channels/lib/email-mime.js +1 -2
- package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
- package/dist/modules/configs/cli.js +8 -2
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/people/route.js +3 -0
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +3 -3
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/companies/route.js +3 -0
- package/dist/modules/customers/api/deals/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/people/route.js +3 -0
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/kind-settings/route.js +6 -0
- package/dist/modules/customers/api/dictionaries/kind-settings/route.js.map +2 -2
- package/dist/modules/customers/api/labels/route.js +10 -1
- package/dist/modules/customers/api/labels/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js +6 -0
- package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -0
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/route.js +6 -0
- package/dist/modules/customers/api/people/[id]/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +3 -3
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +2 -0
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/components/ActivityTypeEditor.js +11 -5
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +2 -2
- package/dist/modules/customers/components/detail/InlineEditors.js +7 -3
- package/dist/modules/customers/components/detail/InlineEditors.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonHighlights.js +1 -1
- package/dist/modules/customers/components/detail/PersonHighlights.js.map +2 -2
- package/dist/modules/customers/components/detail/PlannedActivitiesSection.js +4 -5
- package/dist/modules/customers/components/detail/PlannedActivitiesSection.js.map +2 -2
- package/dist/modules/data_sync/di.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +60 -27
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +10 -7
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/lib/constants.js +3 -1
- package/dist/modules/messages/lib/constants.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +2 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +3 -4
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/commands/payments.js +9 -1
- package/dist/modules/sales/commands/payments.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/business_rules/data/validators.ts +12 -3
- package/src/modules/catalog/api/dictionaries/[key]/route.ts +49 -7
- package/src/modules/communication_channels/lib/email-mime.ts +1 -2
- package/src/modules/configs/cli.ts +10 -1
- package/src/modules/customers/api/companies/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/companies/route.ts +6 -6
- package/src/modules/customers/api/deals/[id]/companies/route.ts +3 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +3 -0
- package/src/modules/customers/api/dictionaries/kind-settings/route.ts +6 -0
- package/src/modules/customers/api/labels/route.ts +10 -1
- package/src/modules/customers/api/people/[id]/companies/[linkId]/route.ts +6 -0
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +3 -0
- package/src/modules/customers/api/people/[id]/companies/route.ts +6 -0
- package/src/modules/customers/api/people/route.ts +6 -6
- package/src/modules/customers/commands/deals.ts +2 -0
- package/src/modules/customers/components/ActivityTypeEditor.tsx +11 -5
- package/src/modules/customers/components/detail/InlineEditors.tsx +8 -4
- package/src/modules/customers/components/detail/PersonHighlights.tsx +4 -1
- package/src/modules/customers/components/detail/PlannedActivitiesSection.tsx +4 -5
- package/src/modules/customers/i18n/de.json +2 -0
- package/src/modules/customers/i18n/en.json +2 -0
- package/src/modules/customers/i18n/es.json +2 -0
- package/src/modules/customers/i18n/ko.json +2 -0
- package/src/modules/customers/i18n/pl.json +2 -0
- package/src/modules/data_sync/di.ts +1 -0
- package/src/modules/data_sync/lib/sync-schedule-service.ts +80 -28
- package/src/modules/messages/commands/messages.ts +10 -7
- package/src/modules/messages/lib/constants.ts +6 -0
- package/src/modules/messages/subscribers/message-notification.ts +2 -0
- package/src/modules/sales/api/documents/factory.ts +6 -7
- 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": ";AAsOU,
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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": "AAAA,SAAS,kBAAkB;AAG3B,SAAS,4BAA4B,6BAA6B;AAClE,SAAS,wCAAwC,mCAAmC;AACpF,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,wBAAwB,CAAC;
|
|
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(
|
|
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
|
|
593
|
+
if (isReplyRecipient(original.senderUserId)) recipientIds.add(original.senderUserId);
|
|
593
594
|
for (const recipient of originalRecipients) {
|
|
594
|
-
if (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
|
|
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
|
|
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) => {
|