@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6227.1.2695efff30

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 (198) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/currencies/backend/currencies/[id]/page.js +33 -8
  3. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  4. package/dist/modules/currencies/backend/currencies/page.js +63 -27
  5. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  6. package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
  7. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  8. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
  9. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  10. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
  11. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
  13. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  14. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
  15. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
  16. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
  17. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  18. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
  19. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  20. package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
  21. package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
  22. package/dist/modules/customers/components/DictionarySettings.js +57 -31
  23. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  24. package/dist/modules/customers/components/PipelineSettings.js +156 -76
  25. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  26. package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
  27. package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
  28. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
  29. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  30. package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
  31. package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
  33. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  34. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
  35. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
  36. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
  37. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  38. package/dist/modules/customers/lib/calendar/labels.js +33 -0
  39. package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
  40. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
  41. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  42. package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
  43. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  44. package/dist/modules/data_sync/api/mappings/route.js +44 -0
  45. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  46. package/dist/modules/data_sync/api/run.js +31 -0
  47. package/dist/modules/data_sync/api/run.js.map +2 -2
  48. package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
  49. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  50. package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
  51. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  52. package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
  53. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  54. package/dist/modules/data_sync/api/schedules/route.js +33 -4
  55. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  56. package/dist/modules/directory/api/organization-branding/route.js +54 -2
  57. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  58. package/dist/modules/directory/backend/directory/branding/page.js +15 -9
  59. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  60. package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
  61. package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
  62. package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
  63. package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
  64. package/dist/modules/entities/api/definitions.batch.js +15 -0
  65. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  66. package/dist/modules/entities/api/definitions.js +26 -0
  67. package/dist/modules/entities/api/definitions.js.map +2 -2
  68. package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
  69. package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
  70. package/dist/modules/entities/api/definitions.restore.js +15 -0
  71. package/dist/modules/entities/api/definitions.restore.js.map +2 -2
  72. package/dist/modules/entities/api/entities.js +31 -3
  73. package/dist/modules/entities/api/entities.js.map +2 -2
  74. package/dist/modules/entities/api/records.js +18 -0
  75. package/dist/modules/entities/api/records.js.map +2 -2
  76. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
  77. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  78. package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
  79. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  80. package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
  81. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  82. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
  83. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  84. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
  85. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  86. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
  87. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
  88. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
  89. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  90. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
  91. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  92. package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
  93. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  94. package/dist/modules/payment_gateways/api/capture/route.js +37 -0
  95. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  96. package/dist/modules/payment_gateways/api/guards.js +31 -0
  97. package/dist/modules/payment_gateways/api/guards.js.map +7 -0
  98. package/dist/modules/payment_gateways/api/refund/route.js +37 -0
  99. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  100. package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
  101. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  102. package/dist/modules/planner/api/availability-date-specific.js +11 -1
  103. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  104. package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
  105. package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
  106. package/dist/modules/planner/commands/availability-date-specific.js +16 -0
  107. package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
  108. package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
  109. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  110. package/dist/modules/query_index/api/purge.js +35 -3
  111. package/dist/modules/query_index/api/purge.js.map +2 -2
  112. package/dist/modules/query_index/api/reindex.js +41 -3
  113. package/dist/modules/query_index/api/reindex.js.map +2 -2
  114. package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
  115. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  116. package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
  117. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  118. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
  119. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
  120. package/dist/modules/shipping_carriers/data/validators.js +6 -1
  121. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  122. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
  123. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  124. package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
  125. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  126. package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
  127. package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
  128. package/dist/modules/translations/components/TranslationManager.js +49 -20
  129. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  130. package/package.json +7 -7
  131. package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
  132. package/src/modules/currencies/backend/currencies/page.tsx +68 -29
  133. package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
  134. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
  135. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
  136. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
  137. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
  138. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
  139. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
  140. package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
  141. package/src/modules/customers/components/DictionarySettings.tsx +63 -29
  142. package/src/modules/customers/components/PipelineSettings.tsx +165 -80
  143. package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
  144. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
  145. package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
  146. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
  147. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
  148. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
  149. package/src/modules/customers/i18n/de.json +11 -7
  150. package/src/modules/customers/i18n/en.json +6 -2
  151. package/src/modules/customers/i18n/es.json +11 -7
  152. package/src/modules/customers/i18n/pl.json +11 -7
  153. package/src/modules/customers/lib/calendar/labels.ts +42 -0
  154. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
  155. package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
  156. package/src/modules/data_sync/api/mappings/route.ts +48 -0
  157. package/src/modules/data_sync/api/run.ts +33 -0
  158. package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
  159. package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
  160. package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
  161. package/src/modules/data_sync/api/schedules/route.ts +36 -4
  162. package/src/modules/directory/api/organization-branding/route.ts +61 -0
  163. package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
  164. package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
  165. package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
  166. package/src/modules/entities/api/definitions.batch.ts +17 -0
  167. package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
  168. package/src/modules/entities/api/definitions.restore.ts +17 -0
  169. package/src/modules/entities/api/definitions.ts +30 -0
  170. package/src/modules/entities/api/entities.ts +35 -3
  171. package/src/modules/entities/api/records.ts +20 -0
  172. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
  173. package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
  174. package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
  175. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
  176. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
  177. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
  178. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
  179. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
  180. package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
  181. package/src/modules/payment_gateways/api/capture/route.ts +39 -0
  182. package/src/modules/payment_gateways/api/guards.ts +59 -0
  183. package/src/modules/payment_gateways/api/refund/route.ts +39 -0
  184. package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
  185. package/src/modules/planner/api/availability-date-specific.ts +10 -0
  186. package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
  187. package/src/modules/planner/commands/availability-date-specific.ts +24 -0
  188. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
  189. package/src/modules/query_index/api/purge.ts +37 -3
  190. package/src/modules/query_index/api/reindex.ts +43 -3
  191. package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
  192. package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
  193. package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
  194. package/src/modules/shipping_carriers/data/validators.ts +5 -0
  195. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
  196. package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
  197. package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
  198. package/src/modules/translations/components/TranslationManager.tsx +65 -21
@@ -16,6 +16,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
16
16
  import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
17
17
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
18
18
  import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
19
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
19
20
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
20
21
  import { useT } from '@open-mercato/shared/lib/i18n/context'
21
22
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
@@ -103,6 +104,31 @@ export default function PipelineSettings(): React.ReactElement {
103
104
  const [stageForm, setStageForm] = React.useState({ label: '', color: null as string | null, icon: null as string | null })
104
105
  const [submittingStage, setSubmittingStage] = React.useState(false)
105
106
 
107
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
108
+ formId: string
109
+ resourceKind: string
110
+ retryLastMutation: () => Promise<boolean>
111
+ }>({
112
+ contextId: 'customers-pipeline-settings',
113
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
114
+ })
115
+ const pipelineMutationContext = React.useMemo(
116
+ () => ({
117
+ formId: 'customers-pipeline-settings',
118
+ resourceKind: 'customers.pipeline',
119
+ retryLastMutation,
120
+ }),
121
+ [retryLastMutation],
122
+ )
123
+ const stageMutationContext = React.useMemo(
124
+ () => ({
125
+ formId: 'customers-pipeline-settings',
126
+ resourceKind: 'customers.pipeline_stage',
127
+ retryLastMutation,
128
+ }),
129
+ [retryLastMutation],
130
+ )
131
+
106
132
  const loadPipelines = React.useCallback(async () => {
107
133
  setLoadingPipelines(true)
108
134
  try {
@@ -165,38 +191,50 @@ export default function PipelineSettings(): React.ReactElement {
165
191
  setSubmittingPipeline(true)
166
192
  try {
167
193
  if (pipelineDialog?.mode === 'create') {
168
- const res = await apiCall('/api/customers/pipelines', {
169
- method: 'POST',
170
- headers: { 'content-type': 'application/json' },
171
- body: JSON.stringify({ name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault }),
194
+ await runMutation({
195
+ operation: async () => {
196
+ const res = await apiCall('/api/customers/pipelines', {
197
+ method: 'POST',
198
+ headers: { 'content-type': 'application/json' },
199
+ body: JSON.stringify({ name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault }),
200
+ })
201
+ if (!res.ok) {
202
+ await raiseCrudError(res.response, t('customers.pipelines.errors.createFailed', 'Failed to create pipeline'))
203
+ }
204
+ },
205
+ context: pipelineMutationContext,
206
+ mutationPayload: { action: 'create', name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault },
172
207
  })
173
- if (!res.ok) {
174
- await raiseCrudError(res.response, t('customers.pipelines.errors.createFailed', 'Failed to create pipeline'))
175
- return
176
- }
177
208
  flash(t('customers.pipelines.flash.created', 'Pipeline created'), 'success')
178
209
  } else if (pipelineDialog?.mode === 'edit') {
179
- const res = await withScopedApiRequestHeaders(
180
- buildOptimisticLockHeader(pipelineDialog.entry.updatedAt),
181
- () =>
182
- apiCall('/api/customers/pipelines', {
183
- method: 'PUT',
184
- headers: { 'content-type': 'application/json' },
185
- body: JSON.stringify({ id: pipelineDialog.entry.id, name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault }),
186
- }),
187
- )
188
- if (!res.ok) {
189
- await raiseCrudError(res.response, t('customers.pipelines.errors.updateFailed', 'Failed to update pipeline'))
190
- return
191
- }
210
+ await runMutation({
211
+ operation: async () => {
212
+ const res = await withScopedApiRequestHeaders(
213
+ buildOptimisticLockHeader(pipelineDialog.entry.updatedAt),
214
+ () =>
215
+ apiCall('/api/customers/pipelines', {
216
+ method: 'PUT',
217
+ headers: { 'content-type': 'application/json' },
218
+ body: JSON.stringify({ id: pipelineDialog.entry.id, name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault }),
219
+ }),
220
+ )
221
+ if (!res.ok) {
222
+ await raiseCrudError(res.response, t('customers.pipelines.errors.updateFailed', 'Failed to update pipeline'))
223
+ }
224
+ },
225
+ context: pipelineMutationContext,
226
+ mutationPayload: { action: 'update', id: pipelineDialog.entry.id, name: pipelineForm.name.trim(), isDefault: pipelineForm.isDefault },
227
+ })
192
228
  flash(t('customers.pipelines.flash.updated', 'Pipeline updated'), 'success')
193
229
  }
194
230
  setPipelineDialog(null)
195
231
  await loadPipelines()
232
+ } catch {
233
+ return
196
234
  } finally {
197
235
  setSubmittingPipeline(false)
198
236
  }
199
- }, [pipelineDialog, pipelineForm, loadPipelines, t])
237
+ }, [pipelineDialog, pipelineForm, loadPipelines, pipelineMutationContext, runMutation, t])
200
238
 
201
239
  const handleDeletePipeline = React.useCallback(async (pipeline: Pipeline) => {
202
240
  const confirmed = await confirm({
@@ -206,25 +244,36 @@ export default function PipelineSettings(): React.ReactElement {
206
244
  variant: 'destructive',
207
245
  })
208
246
  if (!confirmed) return
209
- const res = await withScopedApiRequestHeaders(
210
- buildOptimisticLockHeader(pipeline.updatedAt),
211
- () =>
212
- apiCall('/api/customers/pipelines', {
213
- method: 'DELETE',
214
- headers: { 'content-type': 'application/json' },
215
- body: JSON.stringify({ id: pipeline.id }),
216
- }),
217
- )
218
- if (!res.ok) {
219
- const body = (res.result ?? {}) as Record<string, unknown>
220
- const msg = typeof body.error === 'string' ? body.error : t('customers.pipelines.errors.deleteFailed', 'Failed to delete pipeline')
247
+ try {
248
+ await runMutation({
249
+ operation: async () => {
250
+ const res = await withScopedApiRequestHeaders(
251
+ buildOptimisticLockHeader(pipeline.updatedAt),
252
+ () =>
253
+ apiCall('/api/customers/pipelines', {
254
+ method: 'DELETE',
255
+ headers: { 'content-type': 'application/json' },
256
+ body: JSON.stringify({ id: pipeline.id }),
257
+ }),
258
+ )
259
+ if (!res.ok) {
260
+ const body = (res.result ?? {}) as Record<string, unknown>
261
+ const msg = typeof body.error === 'string' ? body.error : t('customers.pipelines.errors.deleteFailed', 'Failed to delete pipeline')
262
+ throw new Error(msg)
263
+ }
264
+ },
265
+ context: pipelineMutationContext,
266
+ mutationPayload: { action: 'delete', id: pipeline.id },
267
+ })
268
+ } catch (err) {
269
+ const msg = err instanceof Error && err.message ? err.message : t('customers.pipelines.errors.deleteFailed', 'Failed to delete pipeline')
221
270
  flash(msg, 'error')
222
271
  return
223
272
  }
224
273
  flash(t('customers.pipelines.flash.deleted', 'Pipeline deleted'), 'success')
225
274
  if (expandedPipelineId === pipeline.id) setExpandedPipelineId(null)
226
275
  await loadPipelines()
227
- }, [confirm, expandedPipelineId, loadPipelines, t])
276
+ }, [confirm, expandedPipelineId, loadPipelines, pipelineMutationContext, runMutation, t])
228
277
 
229
278
  const openCreateStage = React.useCallback((pipelineId: string) => {
230
279
  setStageForm({ label: '', color: null, icon: null })
@@ -249,39 +298,53 @@ export default function PipelineSettings(): React.ReactElement {
249
298
  if (stageForm.icon) appearance.icon = stageForm.icon
250
299
 
251
300
  if (stageDialog?.mode === 'create') {
252
- const res = await apiCall('/api/customers/pipeline-stages', {
253
- method: 'POST',
254
- headers: { 'content-type': 'application/json' },
255
- body: JSON.stringify({ pipelineId: stageDialog.pipelineId, label: stageForm.label.trim(), ...appearance }),
301
+ const targetPipelineId = stageDialog.pipelineId
302
+ await runMutation({
303
+ operation: async () => {
304
+ const res = await apiCall('/api/customers/pipeline-stages', {
305
+ method: 'POST',
306
+ headers: { 'content-type': 'application/json' },
307
+ body: JSON.stringify({ pipelineId: targetPipelineId, label: stageForm.label.trim(), ...appearance }),
308
+ })
309
+ if (!res.ok) {
310
+ await raiseCrudError(res.response, t('customers.pipelines.errors.stageCreateFailed', 'Failed to create stage'))
311
+ }
312
+ },
313
+ context: stageMutationContext,
314
+ mutationPayload: { action: 'create', pipelineId: targetPipelineId, label: stageForm.label.trim(), ...appearance },
256
315
  })
257
- if (!res.ok) {
258
- await raiseCrudError(res.response, t('customers.pipelines.errors.stageCreateFailed', 'Failed to create stage'))
259
- return
260
- }
261
316
  flash(t('customers.pipelines.flash.stageCreated', 'Stage created'), 'success')
262
- await loadStages(stageDialog.pipelineId)
317
+ await loadStages(targetPipelineId)
263
318
  } else if (stageDialog?.mode === 'edit') {
264
- const res = await withScopedApiRequestHeaders(
265
- buildOptimisticLockHeader(stageDialog.entry.updatedAt),
266
- () =>
267
- apiCall('/api/customers/pipeline-stages', {
268
- method: 'PUT',
269
- headers: { 'content-type': 'application/json' },
270
- body: JSON.stringify({ id: stageDialog.entry.id, label: stageForm.label.trim(), ...appearance }),
271
- }),
272
- )
273
- if (!res.ok) {
274
- await raiseCrudError(res.response, t('customers.pipelines.errors.stageUpdateFailed', 'Failed to update stage'))
275
- return
276
- }
319
+ const targetStage = stageDialog.entry
320
+ await runMutation({
321
+ operation: async () => {
322
+ const res = await withScopedApiRequestHeaders(
323
+ buildOptimisticLockHeader(targetStage.updatedAt),
324
+ () =>
325
+ apiCall('/api/customers/pipeline-stages', {
326
+ method: 'PUT',
327
+ headers: { 'content-type': 'application/json' },
328
+ body: JSON.stringify({ id: targetStage.id, label: stageForm.label.trim(), ...appearance }),
329
+ }),
330
+ )
331
+ if (!res.ok) {
332
+ await raiseCrudError(res.response, t('customers.pipelines.errors.stageUpdateFailed', 'Failed to update stage'))
333
+ }
334
+ },
335
+ context: stageMutationContext,
336
+ mutationPayload: { action: 'update', id: targetStage.id, label: stageForm.label.trim(), ...appearance },
337
+ })
277
338
  flash(t('customers.pipelines.flash.stageUpdated', 'Stage updated'), 'success')
278
- await loadStages(stageDialog.entry.pipelineId)
339
+ await loadStages(targetStage.pipelineId)
279
340
  }
280
341
  setStageDialog(null)
342
+ } catch {
343
+ return
281
344
  } finally {
282
345
  setSubmittingStage(false)
283
346
  }
284
- }, [stageDialog, stageForm, loadStages, t])
347
+ }, [stageDialog, stageForm, loadStages, runMutation, stageMutationContext, t])
285
348
 
286
349
  const handleDeleteStage = React.useCallback(async (stage: PipelineStage) => {
287
350
  const confirmed = await confirm({
@@ -291,24 +354,35 @@ export default function PipelineSettings(): React.ReactElement {
291
354
  variant: 'destructive',
292
355
  })
293
356
  if (!confirmed) return
294
- const res = await withScopedApiRequestHeaders(
295
- buildOptimisticLockHeader(stage.updatedAt),
296
- () =>
297
- apiCall('/api/customers/pipeline-stages', {
298
- method: 'DELETE',
299
- headers: { 'content-type': 'application/json' },
300
- body: JSON.stringify({ id: stage.id }),
301
- }),
302
- )
303
- if (!res.ok) {
304
- const body = (res.result ?? {}) as Record<string, unknown>
305
- const msg = typeof body.error === 'string' ? body.error : t('customers.pipelines.errors.stageDeleteFailed', 'Failed to delete stage')
357
+ try {
358
+ await runMutation({
359
+ operation: async () => {
360
+ const res = await withScopedApiRequestHeaders(
361
+ buildOptimisticLockHeader(stage.updatedAt),
362
+ () =>
363
+ apiCall('/api/customers/pipeline-stages', {
364
+ method: 'DELETE',
365
+ headers: { 'content-type': 'application/json' },
366
+ body: JSON.stringify({ id: stage.id }),
367
+ }),
368
+ )
369
+ if (!res.ok) {
370
+ const body = (res.result ?? {}) as Record<string, unknown>
371
+ const msg = typeof body.error === 'string' ? body.error : t('customers.pipelines.errors.stageDeleteFailed', 'Failed to delete stage')
372
+ throw new Error(msg)
373
+ }
374
+ },
375
+ context: stageMutationContext,
376
+ mutationPayload: { action: 'delete', id: stage.id },
377
+ })
378
+ } catch (err) {
379
+ const msg = err instanceof Error && err.message ? err.message : t('customers.pipelines.errors.stageDeleteFailed', 'Failed to delete stage')
306
380
  flash(msg, 'error')
307
381
  return
308
382
  }
309
383
  flash(t('customers.pipelines.flash.stageDeleted', 'Stage deleted'), 'success')
310
384
  await loadStages(stage.pipelineId)
311
- }, [confirm, loadStages, t])
385
+ }, [confirm, loadStages, runMutation, stageMutationContext, t])
312
386
 
313
387
  const handleMoveStage = React.useCallback(async (stage: PipelineStage, direction: 'up' | 'down') => {
314
388
  const pipelineStages = stages[stage.pipelineId] ?? []
@@ -323,17 +397,28 @@ export default function PipelineSettings(): React.ReactElement {
323
397
  reordered[swapIdx] = temp
324
398
 
325
399
  const orderedStages = reordered.map((s, i) => ({ id: s.id, order: i }))
326
- const res = await apiCall('/api/customers/pipeline-stages/reorder', {
327
- method: 'POST',
328
- headers: { 'content-type': 'application/json' },
329
- body: JSON.stringify({ stages: orderedStages }),
330
- })
331
- if (!res.ok) {
332
- flash(t('customers.pipelines.errors.reorderFailed', 'Failed to reorder stages'), 'error')
400
+ try {
401
+ await runMutation({
402
+ operation: async () => {
403
+ const res = await apiCall('/api/customers/pipeline-stages/reorder', {
404
+ method: 'POST',
405
+ headers: { 'content-type': 'application/json' },
406
+ body: JSON.stringify({ stages: orderedStages }),
407
+ })
408
+ if (!res.ok) {
409
+ throw new Error(t('customers.pipelines.errors.reorderFailed', 'Failed to reorder stages'))
410
+ }
411
+ },
412
+ context: stageMutationContext,
413
+ mutationPayload: { action: 'reorder', stages: orderedStages },
414
+ })
415
+ } catch (err) {
416
+ const msg = err instanceof Error && err.message ? err.message : t('customers.pipelines.errors.reorderFailed', 'Failed to reorder stages')
417
+ flash(msg, 'error')
333
418
  return
334
419
  }
335
420
  await loadStages(stage.pipelineId)
336
- }, [stages, loadStages, t])
421
+ }, [stages, loadStages, runMutation, stageMutationContext, t])
337
422
 
338
423
  const handleKeyDown = React.useCallback(
339
424
  (handler: () => void) => (e: React.KeyboardEvent) => {
@@ -7,11 +7,12 @@ import { isToday } from 'date-fns/isToday'
7
7
  import { isTomorrow } from 'date-fns/isTomorrow'
8
8
  import { startOfDay } from 'date-fns/startOfDay'
9
9
  import { CalendarClock } from 'lucide-react'
10
- import { useT } from '@open-mercato/shared/lib/i18n/context'
10
+ import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
11
11
  import { cn } from '@open-mercato/shared/lib/utils'
12
12
  import { Avatar } from '@open-mercato/ui/primitives/avatar'
13
13
  import { Button } from '@open-mercato/ui/primitives/button'
14
14
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
15
+ import { eventDisplayTitle, pluralCategory } from '../../lib/calendar/labels'
15
16
  import type { AgendaListProps, CalendarCategory, CalendarItem } from './types'
16
17
 
17
18
  const MAX_AVATARS_PER_ROW = 2
@@ -84,8 +85,15 @@ function buildDayGroups(anchor: Date, horizonDays: number, items: CalendarItem[]
84
85
 
85
86
  function AgendaDayHeader({ date, count }: { date: Date; count: number }) {
86
87
  const t = useT()
88
+ const locale = useLocale()
87
89
  const todayMarker = isToday(date)
88
90
  const tomorrowMarker = !todayMarker && isTomorrow(date)
91
+ const countKey = `customers.calendar.agenda.eventsCount.${pluralCategory(locale, count)}`
92
+ const resolvedCount = t(countKey, { count })
93
+ const countLabel =
94
+ resolvedCount === countKey
95
+ ? t('customers.calendar.agenda.eventsCount.other', '{count} events', { count })
96
+ : resolvedCount
89
97
  return (
90
98
  <div className="flex w-full items-center gap-2 bg-muted/50 px-3 py-2.5 sm:px-5">
91
99
  <span className="text-sm font-semibold text-foreground">{groupLabelOf(date)}</span>
@@ -95,11 +103,7 @@ function AgendaDayHeader({ date, count }: { date: Date; count: number }) {
95
103
  </span>
96
104
  ) : null}
97
105
  <span aria-hidden="true" className="w-2.5 shrink-0" />
98
- <span className="text-xs font-medium text-muted-foreground">
99
- {count === 1
100
- ? t('customers.calendar.agenda.eventCount', '1 event')
101
- : t('customers.calendar.agenda.eventsCount', '{count} events', { count })}
102
- </span>
106
+ <span className="text-xs font-medium text-muted-foreground">{countLabel}</span>
103
107
  </div>
104
108
  )
105
109
  }
@@ -116,6 +120,7 @@ function AgendaRow({
116
120
  const t = useT()
117
121
  const canceled = item.status === 'canceled'
118
122
  const done = item.status === 'done'
123
+ const title = eventDisplayTitle(item.title, t('customers.calendar.grid.untitled', 'Untitled'))
119
124
  const typeLabel = typeLabels?.[item.interactionType] ?? deriveTypeLabel(item.interactionType)
120
125
  const platformLabels: Record<string, string> = {
121
126
  zoom: t('customers.calendar.platformFull.zoom', 'Zoom'),
@@ -135,7 +140,7 @@ function AgendaRow({
135
140
  <Button
136
141
  type="button"
137
142
  variant="ghost"
138
- aria-label={`${item.title} · ${ariaTime}`}
143
+ aria-label={`${title} · ${ariaTime}`}
139
144
  onClick={() => onItemClick(item)}
140
145
  className={cn(
141
146
  'h-auto w-full justify-start whitespace-normal rounded-none bg-background px-3 py-3 text-left transition-colors hover:bg-muted/30 sm:gap-3.5 sm:px-5',
@@ -161,7 +166,7 @@ function AgendaRow({
161
166
  canceled && 'line-through',
162
167
  )}
163
168
  >
164
- {item.title}
169
+ {title}
165
170
  </span>
166
171
  <span className="truncate text-xs text-muted-foreground">
167
172
  {locationLabel ? `${typeLabel} · ${locationLabel}` : typeLabel}
@@ -78,6 +78,10 @@ export function CalendarSettingsModal({
78
78
  ]
79
79
 
80
80
  const title = t('customers.calendar.settings.title', 'Customization')
81
+ const removeTagLabel = React.useCallback(
82
+ (tag: string) => t('customers.calendar.settings.removeTag', 'Remove {tag}', { tag }),
83
+ [t],
84
+ )
81
85
 
82
86
  return (
83
87
  <Dialog open={open} onOpenChange={onOpenChange}>
@@ -119,6 +123,7 @@ export function CalendarSettingsModal({
119
123
  placeholder={t('customers.calendar.settings.addCategory', 'Add a category…')}
120
124
  value={draft.eventCategories}
121
125
  maxTags={MAX_EVENT_CATEGORIES}
126
+ removeTagLabel={removeTagLabel}
122
127
  onChange={(eventCategories) => setDraft((current) => ({ ...current, eventCategories }))}
123
128
  />
124
129
  <SettingsTagInput
@@ -131,6 +136,7 @@ export function CalendarSettingsModal({
131
136
  placeholder={t('customers.calendar.settings.addType', 'Add a type…')}
132
137
  value={draft.activityTypes}
133
138
  maxTags={MAX_ACTIVITY_TYPES}
139
+ removeTagLabel={removeTagLabel}
134
140
  onChange={(activityTypes) => setDraft((current) => ({ ...current, activityTypes }))}
135
141
  />
136
142
  {toggleRows.map((row) => (
@@ -186,10 +192,11 @@ type SettingsTagInputProps = {
186
192
  placeholder: string
187
193
  value: string[]
188
194
  maxTags: number
195
+ removeTagLabel: (tag: string) => string
189
196
  onChange(value: string[]): void
190
197
  }
191
198
 
192
- function SettingsTagInput({ label, maxLabel, hint, placeholder, value, maxTags, onChange }: SettingsTagInputProps) {
199
+ function SettingsTagInput({ label, maxLabel, hint, placeholder, value, maxTags, removeTagLabel, onChange }: SettingsTagInputProps) {
193
200
  return (
194
201
  <div className="flex flex-col gap-1">
195
202
  <div className="flex items-center gap-1">
@@ -201,7 +208,14 @@ function SettingsTagInput({ label, maxLabel, hint, placeholder, value, maxTags,
201
208
  </span>
202
209
  </SimpleTooltip>
203
210
  </div>
204
- <TagInput value={value} onChange={onChange} placeholder={placeholder} maxTags={maxTags} aria-label={label} />
211
+ <TagInput
212
+ value={value}
213
+ onChange={onChange}
214
+ placeholder={placeholder}
215
+ maxTags={maxTags}
216
+ aria-label={label}
217
+ removeTagLabel={removeTagLabel}
218
+ />
205
219
  </div>
206
220
  )
207
221
  }
@@ -11,6 +11,7 @@ import { cn } from '@open-mercato/shared/lib/utils'
11
11
  import { Button } from '@open-mercato/ui/primitives/button'
12
12
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
13
13
  import { getVisibleRange } from '../../lib/calendar/range'
14
+ import { eventDisplayTitle } from '../../lib/calendar/labels'
14
15
  import type { CalendarItem, MonthGridProps } from './types'
15
16
 
16
17
  const MAX_PILLS_PER_DAY = 2
@@ -61,7 +62,9 @@ function groupItemsByDay(items: CalendarItem[]): Map<string, CalendarItem[]> {
61
62
  }
62
63
 
63
64
  function MonthPill({ item, onItemClick }: { item: CalendarItem; onItemClick: (item: CalendarItem) => void }) {
65
+ const t = useT()
64
66
  const canceled = item.status === 'canceled'
67
+ const title = eventDisplayTitle(item.title, t('customers.calendar.grid.untitled', 'Untitled'))
65
68
  const timeLabel = item.allDay ? '' : ` · ${formatTime(item.start)} – ${formatTime(item.end)}`
66
69
  const tintStyle = item.color
67
70
  ? { backgroundColor: `${item.color}${SOFT_TINT_ALPHA}`, color: item.color }
@@ -70,7 +73,7 @@ function MonthPill({ item, onItemClick }: { item: CalendarItem; onItemClick: (it
70
73
  <Button
71
74
  type="button"
72
75
  variant="ghost"
73
- aria-label={`${item.title}${timeLabel}`}
76
+ aria-label={`${title}${timeLabel}`}
74
77
  onClick={(event) => {
75
78
  event.stopPropagation()
76
79
  onItemClick(item)
@@ -94,7 +97,7 @@ function MonthPill({ item, onItemClick }: { item: CalendarItem; onItemClick: (it
94
97
  canceled && 'line-through',
95
98
  )}
96
99
  >
97
- {item.title}
100
+ {title}
98
101
  </span>
99
102
  </Button>
100
103
  )
@@ -7,6 +7,7 @@ import { Avatar } from '@open-mercato/ui/primitives/avatar'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
8
8
  import { Input } from '@open-mercato/ui/primitives/input'
9
9
  import type { EditorParticipant } from '../../../lib/calendar/editorPayload'
10
+ import { composeAccessibleName } from '../../../lib/calendar/labels'
10
11
  import { searchPeopleOptions, type PersonOption } from './lookups'
11
12
  import { CONTROL_BORDER, DROPDOWN_PANEL_CLASS, PersonChip, UppercaseBadge } from './inputs'
12
13
 
@@ -119,6 +120,11 @@ export function PeopleField({
119
120
  variant="ghost"
120
121
  role="option"
121
122
  aria-selected={false}
123
+ aria-label={composeAccessibleName([
124
+ option.name,
125
+ option.email,
126
+ option.isCustomer ? t('customers.calendar.editor.customerBadge', 'Customer') : null,
127
+ ])}
122
128
  onClick={() => {
123
129
  const participant: EditorParticipant = {
124
130
  userId: option.userId,
@@ -8,6 +8,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
8
8
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
9
9
  import { Input } from '@open-mercato/ui/primitives/input'
10
10
  import type { EditorRelatedTo } from '../../../lib/calendar/editorPayload'
11
+ import { composeAccessibleName } from '../../../lib/calendar/labels'
11
12
  import {
12
13
  fetchDealsForEntity,
13
14
  searchRelatedEntities,
@@ -178,6 +179,7 @@ export function RelatedToField({
178
179
  variant="ghost"
179
180
  role="option"
180
181
  aria-selected={value?.id === option.id}
182
+ aria-label={composeAccessibleName([option.label, option.subtitle, kindLabel(option.kind)])}
181
183
  onClick={() => {
182
184
  onChange({ id: option.id, kind: option.kind, label: option.label })
183
185
  if (value?.id !== option.id) onDealChange(null)
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { useT } from '@open-mercato/shared/lib/i18n/context'
5
5
  import type { EditorDateLabel } from '../../../lib/calendar/editorPayload'
6
+ import { multiDayEventSpan } from '../../../lib/calendar/labels'
6
7
  import { AllDayToggle, DateControl, LABEL_CLASS, TimeControl } from './inputs'
7
8
 
8
9
  const DATE_LABEL_TEXT: Record<EditorDateLabel, { key: string; fallback: string }> = {
@@ -76,6 +77,7 @@ export function ScheduleSection({
76
77
  }) {
77
78
  const t = useT()
78
79
  const showTime = !(hasAllDay && allDay)
80
+ const multiDaySpan = hasEnd && !endsError ? multiDayEventSpan(date, endDate) : 0
79
81
  return (
80
82
  <div className="flex w-full flex-col gap-2.5">
81
83
  {hasAllDay ? (
@@ -105,6 +107,11 @@ export function ScheduleSection({
105
107
  />
106
108
  ) : null}
107
109
  {endsError ? <p className="text-xs text-status-error-text">{endsError}</p> : null}
110
+ {multiDaySpan > 0 ? (
111
+ <p className="text-xs text-muted-foreground">
112
+ {t('customers.calendar.editor.multiDayHint', 'Multi-day event · {count} days', { count: multiDaySpan })}
113
+ </p>
114
+ ) : null}
108
115
  </div>
109
116
  )
110
117
  }
@@ -151,11 +151,11 @@
151
151
  "customers.audit.entityRoles.create": "Create entity role",
152
152
  "customers.audit.entityRoles.delete": "Delete entity role",
153
153
  "customers.audit.entityRoles.update": "Update entity role",
154
- "customers.audit.interactions.cancel": "Interaction abbrechen",
155
- "customers.audit.interactions.complete": "Interaction abschliessen",
156
- "customers.audit.interactions.create": "Interaction erstellen",
157
- "customers.audit.interactions.delete": "Interaction loeschen",
158
- "customers.audit.interactions.update": "Interaction aktualisieren",
154
+ "customers.audit.interactions.cancel": "Interaktion abbrechen",
155
+ "customers.audit.interactions.complete": "Interaktion abschließen",
156
+ "customers.audit.interactions.create": "Interaktion erstellen",
157
+ "customers.audit.interactions.delete": "Interaktion löschen",
158
+ "customers.audit.interactions.update": "Interaktion aktualisieren",
159
159
  "customers.audit.labels.assign": "Label zuweisen",
160
160
  "customers.audit.labels.create": "Label erstellen",
161
161
  "customers.audit.labels.unassign": "Labelzuweisung entfernen",
@@ -174,8 +174,10 @@
174
174
  "customers.audit.todos.link": "To-do verknüpfen",
175
175
  "customers.audit.todos.unlink": "To-do-Verknüpfung lösen",
176
176
  "customers.calendar.actions.newEvent": "Neuer Termin",
177
- "customers.calendar.agenda.eventCount": "1 Termin",
178
- "customers.calendar.agenda.eventsCount": "{count} Termine",
177
+ "customers.calendar.agenda.eventsCount.few": "{count} Termine",
178
+ "customers.calendar.agenda.eventsCount.many": "{count} Termine",
179
+ "customers.calendar.agenda.eventsCount.one": "{count} Termin",
180
+ "customers.calendar.agenda.eventsCount.other": "{count} Termine",
179
181
  "customers.calendar.agenda.today": "Heute",
180
182
  "customers.calendar.agenda.tomorrow": "Morgen",
181
183
  "customers.calendar.cards.allDay": "Ganztägig",
@@ -230,6 +232,7 @@
230
232
  "customers.calendar.editor.error": "Termin konnte nicht gespeichert werden",
231
233
  "customers.calendar.editor.location": "Ort",
232
234
  "customers.calendar.editor.locationPlaceholder": "Ort oder Link hinzufügen…",
235
+ "customers.calendar.editor.multiDayHint": "Mehrtägiger Termin · {count} Tage",
233
236
  "customers.calendar.editor.noResults": "Keine Ergebnisse",
234
237
  "customers.calendar.editor.participants": "Teilnehmer",
235
238
  "customers.calendar.editor.phoneLink": "Telefon / Link",
@@ -334,6 +337,7 @@
334
337
  "customers.calendar.settings.eventCategories": "Ereigniskategorien",
335
338
  "customers.calendar.settings.eventCategoriesHint": "Eigene Gruppierungslabels (z. B. Teammeeting, Verkaufsanruf). Werden beim Erstellen eines Ereignisses angeboten.",
336
339
  "customers.calendar.settings.max": "(max. {count})",
340
+ "customers.calendar.settings.removeTag": "{tag} entfernen",
337
341
  "customers.calendar.settings.save": "Änderungen speichern",
338
342
  "customers.calendar.settings.saved": "Kalendereinstellungen gespeichert",
339
343
  "customers.calendar.settings.showCrmActivities": "CRM-Aktivitäten im Kalender anzeigen",
@@ -174,8 +174,10 @@
174
174
  "customers.audit.todos.link": "Link todo",
175
175
  "customers.audit.todos.unlink": "Unlink todo",
176
176
  "customers.calendar.actions.newEvent": "New event",
177
- "customers.calendar.agenda.eventCount": "1 event",
178
- "customers.calendar.agenda.eventsCount": "{count} events",
177
+ "customers.calendar.agenda.eventsCount.few": "{count} events",
178
+ "customers.calendar.agenda.eventsCount.many": "{count} events",
179
+ "customers.calendar.agenda.eventsCount.one": "{count} event",
180
+ "customers.calendar.agenda.eventsCount.other": "{count} events",
179
181
  "customers.calendar.agenda.today": "Today",
180
182
  "customers.calendar.agenda.tomorrow": "Tomorrow",
181
183
  "customers.calendar.cards.allDay": "All day",
@@ -230,6 +232,7 @@
230
232
  "customers.calendar.editor.error": "Failed to save event",
231
233
  "customers.calendar.editor.location": "Location",
232
234
  "customers.calendar.editor.locationPlaceholder": "Add a location or link…",
235
+ "customers.calendar.editor.multiDayHint": "Multi-day event · {count} days",
233
236
  "customers.calendar.editor.noResults": "No results",
234
237
  "customers.calendar.editor.participants": "Participants",
235
238
  "customers.calendar.editor.phoneLink": "Phone / link",
@@ -334,6 +337,7 @@
334
337
  "customers.calendar.settings.eventCategories": "Event Categories",
335
338
  "customers.calendar.settings.eventCategoriesHint": "Your own grouping labels (e.g. Team Meeting, Sales Call). Offered when creating an event.",
336
339
  "customers.calendar.settings.max": "(max. {count})",
340
+ "customers.calendar.settings.removeTag": "Remove {tag}",
337
341
  "customers.calendar.settings.save": "Save Changes",
338
342
  "customers.calendar.settings.saved": "Calendar settings saved",
339
343
  "customers.calendar.settings.showCrmActivities": "Show CRM activities on calendar",