@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30

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 (243) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/commands/categories.js +1 -1
  9. package/dist/modules/catalog/commands/categories.js.map +2 -2
  10. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  11. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  12. package/dist/modules/catalog/commands/products.js +52 -17
  13. package/dist/modules/catalog/commands/products.js.map +2 -2
  14. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  15. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  16. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  17. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  18. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  19. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  20. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  21. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  23. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  24. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  25. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  27. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  28. package/dist/modules/customers/api/activity-types/route.js +151 -0
  29. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  30. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  31. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  32. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  33. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  35. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  36. package/dist/modules/customers/api/interactions/route.js +3 -0
  37. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  38. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  39. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  40. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  41. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  43. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  45. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/calendar-event-types.js +548 -0
  47. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  48. package/dist/modules/customers/commands/dictionaries.js +70 -10
  49. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +187 -22
  51. package/dist/modules/customers/commands/interactions.js.map +3 -3
  52. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  53. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  54. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  55. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  56. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  57. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  58. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  59. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  60. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  61. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  63. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  65. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  66. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  67. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  68. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  69. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  70. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  71. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  72. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  73. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  74. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  75. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  76. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  77. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  79. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  81. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  83. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  85. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/types.js +1 -0
  87. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  88. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  89. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  90. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  91. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  92. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  93. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  94. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  95. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  96. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  97. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  98. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  99. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  100. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  101. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  102. package/dist/modules/customers/data/entities.js +6 -0
  103. package/dist/modules/customers/data/entities.js.map +2 -2
  104. package/dist/modules/customers/data/validators.js +39 -10
  105. package/dist/modules/customers/data/validators.js.map +2 -2
  106. package/dist/modules/customers/di.js +20 -1
  107. package/dist/modules/customers/di.js.map +2 -2
  108. package/dist/modules/customers/extension-points.js +13 -0
  109. package/dist/modules/customers/extension-points.js.map +2 -2
  110. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  111. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  112. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  113. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  114. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  115. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  117. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  119. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  120. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  121. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  122. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  123. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  124. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  125. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  126. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  127. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  128. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  129. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  130. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  131. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  132. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  133. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  134. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  135. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  136. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  137. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  138. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  139. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  140. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  141. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  142. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  143. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  144. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  145. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  146. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  148. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  149. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  150. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  151. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  152. package/generated/entities/customer_interaction/index.ts +1 -0
  153. package/generated/entity-fields-registry.ts +2 -0
  154. package/package.json +9 -8
  155. package/src/modules/auth/i18n/de.json +3 -0
  156. package/src/modules/auth/i18n/en.json +3 -0
  157. package/src/modules/auth/i18n/es.json +3 -0
  158. package/src/modules/auth/i18n/ko.json +3 -0
  159. package/src/modules/auth/i18n/pl.json +3 -0
  160. package/src/modules/catalog/commands/categories.ts +1 -1
  161. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  162. package/src/modules/catalog/commands/products.ts +58 -17
  163. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  164. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  165. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  166. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  167. package/src/modules/communication_channels/i18n/de.json +4 -0
  168. package/src/modules/communication_channels/i18n/en.json +4 -0
  169. package/src/modules/communication_channels/i18n/es.json +4 -0
  170. package/src/modules/communication_channels/i18n/ko.json +4 -0
  171. package/src/modules/communication_channels/i18n/pl.json +4 -0
  172. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  173. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  174. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  175. package/src/modules/customers/api/activity-types/route.ts +154 -0
  176. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  177. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  178. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  179. package/src/modules/customers/api/interactions/route.ts +4 -0
  180. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  181. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  182. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  183. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  184. package/src/modules/customers/calendar-event-types.ts +674 -0
  185. package/src/modules/customers/commands/dictionaries.ts +84 -8
  186. package/src/modules/customers/commands/interactions.ts +244 -21
  187. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  188. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  189. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  190. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  191. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  192. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  193. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  194. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  195. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  196. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  197. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  198. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  199. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  200. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  201. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  202. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  203. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  204. package/src/modules/customers/components/calendar/types.ts +1 -0
  205. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  206. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  207. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  208. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  209. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  210. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  211. package/src/modules/customers/components/detail/types.ts +1 -0
  212. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  213. package/src/modules/customers/data/entities.ts +8 -1
  214. package/src/modules/customers/data/validators.ts +42 -7
  215. package/src/modules/customers/di.ts +21 -0
  216. package/src/modules/customers/extension-points.ts +13 -0
  217. package/src/modules/customers/i18n/de.json +104 -8
  218. package/src/modules/customers/i18n/en.json +104 -8
  219. package/src/modules/customers/i18n/es.json +104 -8
  220. package/src/modules/customers/i18n/ko.json +104 -8
  221. package/src/modules/customers/i18n/pl.json +104 -8
  222. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  223. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  224. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  225. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  226. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  227. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  228. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  229. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  230. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  231. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  232. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  233. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  234. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  235. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  236. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  237. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  238. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  239. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  240. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  241. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  242. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  243. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -9,6 +9,24 @@ export function normalizeNumber(value: unknown, fallback = 0): number {
9
9
  return fallback
10
10
  }
11
11
 
12
+ /**
13
+ * A catalog price is only usable on a line when it carries a positive amount, because line
14
+ * submission rejects a unit price of zero. Returns the amount for the requested mode, falling
15
+ * back to the other mode only when the requested amount is missing, or `null` when the price
16
+ * has no positive amount to use.
17
+ */
18
+ export function resolveCatalogPriceAmount(
19
+ amountNet: number | null | undefined,
20
+ amountGross: number | null | undefined,
21
+ mode: 'net' | 'gross',
22
+ ): number | null {
23
+ const isPresent = (value: number | null | undefined): value is number =>
24
+ typeof value === 'number' && Number.isFinite(value)
25
+ const [preferred, fallback] = mode === 'net' ? [amountNet, amountGross] : [amountGross, amountNet]
26
+ const amount = isPresent(preferred) ? preferred : isPresent(fallback) ? fallback : null
27
+ return amount !== null && amount > 0 ? amount : null
28
+ }
29
+
12
30
  /**
13
31
  * Pass `locale` to format in the application locale (`useLocale()` in client components).
14
32
  * Omitting it keeps the runtime default, which varies per machine and is therefore not
@@ -177,7 +177,13 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
177
177
  const [loading, setLoading] = React.useState(true)
178
178
  const [error, setError] = React.useState<string | null>(null)
179
179
  const [isNotFound, setIsNotFound] = React.useState(false)
180
- const [accessDenied, setAccessDenied] = React.useState(false)
180
+ // Keyed by project id so a route change to another project never shows the
181
+ // previous project's guard (with a live request button) while the new one loads.
182
+ const [accessDeniedProjectId, setAccessDeniedProjectId] = React.useState<string | null>(null)
183
+ const accessDenied = accessDeniedProjectId !== null && accessDeniedProjectId === projectId
184
+ // The id the last finished load was for. Until it matches the route, the page
185
+ // is still loading — never the previous project's error or empty state.
186
+ const [settledProjectId, setSettledProjectId] = React.useState<string | null>(null)
181
187
 
182
188
  const [employees, setEmployees] = React.useState<EmployeeAssignment[]>([])
183
189
  const [employeesLoading, setEmployeesLoading] = React.useState(false)
@@ -387,7 +393,6 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
387
393
  setLoading(true)
388
394
  setError(null)
389
395
  setIsNotFound(false)
390
- setAccessDenied(false)
391
396
  try {
392
397
  const queryParams = new URLSearchParams({ page: '1', pageSize: '1', ids: projectId! })
393
398
  const call = await apiCall<ProjectResponse>(
@@ -396,9 +401,10 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
396
401
  // Screen 17: the route answers 404 with this discriminator for a project
397
402
  // the caller is not a member of, without naming it.
398
403
  if (call.status === 404 && readDenialReason(call.result) === NO_PROJECT_ACCESS_REASON) {
399
- if (!cancelled) setAccessDenied(true)
404
+ if (!cancelled) setAccessDeniedProjectId(projectId ?? null)
400
405
  return
401
406
  }
407
+ if (!cancelled) setAccessDeniedProjectId(null)
402
408
  if (!call.ok) {
403
409
  throw new Error(t('staff.timesheets.projects.errors.load', 'Failed to load project.'))
404
410
  }
@@ -414,7 +420,10 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
414
420
  setError(loadError instanceof Error ? loadError.message : t('staff.timesheets.projects.errors.load', 'Failed to load project.'))
415
421
  }
416
422
  } finally {
417
- if (!cancelled) setLoading(false)
423
+ if (!cancelled) {
424
+ setLoading(false)
425
+ setSettledProjectId(projectId ?? null)
426
+ }
418
427
  }
419
428
  }
420
429
  loadProject()
@@ -649,7 +658,18 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
649
658
  }, [handleAddEmployee])
650
659
 
651
660
  // --- Render ---
652
- if (loading) {
661
+ // Screen 17 — the caller may not open this project. The guard state names
662
+ // neither the customer nor the project (note 1), and the surrounding shell
663
+ // (sidebar, topbar) is untouched because the caller still has other projects
664
+ // (note 3). It is checked before `loading` and only cleared by a response that
665
+ // grants access, so a background reload (e.g. the organization scope
666
+ // resolving late) keeps the guard mounted instead of resetting a request the
667
+ // user has just sent.
668
+ if (accessDenied) {
669
+ return <NoProjectAccess timeProjectId={projectId} />
670
+ }
671
+
672
+ if (loading || settledProjectId !== (projectId ?? null)) {
653
673
  return (
654
674
  <Page>
655
675
  <PageBody>
@@ -659,14 +679,6 @@ export default function TimesheetProjectDetailPage({ params }: { params?: { id?:
659
679
  )
660
680
  }
661
681
 
662
- // Screen 17 — the caller may not open this project. The guard state names
663
- // neither the customer nor the project (note 1), and the surrounding shell
664
- // (sidebar, topbar) is untouched because the caller still has other projects
665
- // (note 3).
666
- if (accessDenied) {
667
- return <NoProjectAccess timeProjectId={projectId} />
668
- }
669
-
670
682
  if (isNotFound) {
671
683
  return (
672
684
  <Page>
@@ -764,7 +764,19 @@ async function resolveUpload(em: EntityManager, runId: string | undefined, curso
764
764
  return null
765
765
  }
766
766
 
767
+ async function personExists(em: EntityManager, personId: string, scope: TenantScope): Promise<boolean> {
768
+ const matches = await em.count(CustomerEntity, {
769
+ id: personId,
770
+ kind: 'person',
771
+ organizationId: scope.organizationId,
772
+ tenantId: scope.tenantId,
773
+ deletedAt: null,
774
+ })
775
+ return matches > 0
776
+ }
777
+
767
778
  async function resolveExistingPersonId(params: {
779
+ em: EntityManager
768
780
  externalIdMappingService: ExternalIdMappingService
769
781
  externalId: string | null | undefined
770
782
  email: string | null | undefined
@@ -778,7 +790,7 @@ async function resolveExistingPersonId(params: {
778
790
  params.externalId,
779
791
  params.scope,
780
792
  )
781
- if (mappedLocalId) return mappedLocalId
793
+ if (mappedLocalId && await personExists(params.em, mappedLocalId, params.scope)) return mappedLocalId
782
794
  }
783
795
 
784
796
  if (!params.email) return null
@@ -866,6 +878,7 @@ async function processRow(params: {
866
878
  }
867
879
  }
868
880
  const existingId = await resolveExistingPersonId({
881
+ em: params.em,
869
882
  externalIdMappingService: params.externalIdMappingService,
870
883
  externalId,
871
884
  email: payload.values.primaryEmail,
@@ -885,6 +898,21 @@ async function processRow(params: {
885
898
  }
886
899
  }
887
900
 
901
+ const rememberPersonIdentity = async (personId: string): Promise<void> => {
902
+ if (externalId) {
903
+ await params.externalIdMappingService.storeExternalIdMapping(
904
+ 'sync_excel',
905
+ 'customers.person',
906
+ personId,
907
+ externalId,
908
+ params.scope,
909
+ )
910
+ }
911
+ if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {
912
+ params.emailDedupeIndex.set(payload.values.primaryEmail, personId)
913
+ }
914
+ }
915
+
888
916
  try {
889
917
  if (existingId) {
890
918
  const updateInput = {
@@ -897,6 +925,8 @@ async function processRow(params: {
897
925
  ctx: params.commandContext,
898
926
  })
899
927
 
928
+ await rememberPersonIdentity(existingId)
929
+
900
930
  await upsertPrimaryAddress({
901
931
  entityId: existingId,
902
932
  addressValues: payload.addressValues,
@@ -906,19 +936,6 @@ async function processRow(params: {
906
936
  em: params.em,
907
937
  })
908
938
 
909
- if (externalId) {
910
- await params.externalIdMappingService.storeExternalIdMapping(
911
- 'sync_excel',
912
- 'customers.person',
913
- existingId,
914
- externalId,
915
- params.scope,
916
- )
917
- }
918
- if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {
919
- params.emailDedupeIndex.set(payload.values.primaryEmail, existingId)
920
- }
921
-
922
939
  return {
923
940
  externalId: externalId ?? sourceIdentifier,
924
941
  action: 'update',
@@ -941,6 +958,8 @@ async function processRow(params: {
941
958
  ctx: params.commandContext,
942
959
  })
943
960
 
961
+ await rememberPersonIdentity(commandResult.result.entityId)
962
+
944
963
  await upsertPrimaryAddress({
945
964
  entityId: commandResult.result.entityId,
946
965
  addressValues: payload.addressValues,
@@ -950,19 +969,6 @@ async function processRow(params: {
950
969
  em: params.em,
951
970
  })
952
971
 
953
- if (externalId) {
954
- await params.externalIdMappingService.storeExternalIdMapping(
955
- 'sync_excel',
956
- 'customers.person',
957
- commandResult.result.entityId,
958
- externalId,
959
- params.scope,
960
- )
961
- }
962
- if (payload.values.primaryEmail && !params.emailDedupeIndex.has(payload.values.primaryEmail)) {
963
- params.emailDedupeIndex.set(payload.values.primaryEmail, commandResult.result.entityId)
964
- }
965
-
966
972
  return {
967
973
  externalId: externalId ?? sourceIdentifier,
968
974
  action: 'create',