@open-mercato/core 0.6.4-develop.4178.1.aad9ddaa95 → 0.6.4-develop.4210.1.d412061cfe

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 (99) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_docs/frontend/docs/api/Explorer.js +14 -14
  3. package/dist/modules/api_docs/frontend/docs/api/Explorer.js.map +2 -2
  4. package/dist/modules/attachments/components/AttachmentContentPreview.js +2 -2
  5. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  6. package/dist/modules/audit_logs/backend/audit-logs/page.js +3 -3
  7. package/dist/modules/audit_logs/backend/audit-logs/page.js.map +2 -2
  8. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +3 -7
  9. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
  10. package/dist/modules/customers/components/detail/ActivityCard.js +2 -2
  11. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  12. package/dist/modules/customers/components/detail/AssignRoleDialog.js +34 -49
  13. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  14. package/dist/modules/customers/components/detail/ChangelogEntryRow.js +2 -2
  15. package/dist/modules/customers/components/detail/ChangelogEntryRow.js.map +2 -2
  16. package/dist/modules/customers/components/detail/CompanyDetailHeader.js +10 -1
  17. package/dist/modules/customers/components/detail/CompanyDetailHeader.js.map +2 -2
  18. package/dist/modules/customers/components/detail/DealLinkedEntitiesTab.js +7 -51
  19. package/dist/modules/customers/components/detail/DealLinkedEntitiesTab.js.map +2 -2
  20. package/dist/modules/customers/components/detail/DetailTabsLayout.js +1 -1
  21. package/dist/modules/customers/components/detail/DetailTabsLayout.js.map +2 -2
  22. package/dist/modules/customers/components/detail/ManageTagsDialog.js +25 -33
  23. package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
  24. package/dist/modules/customers/components/detail/PersonCard.js +3 -2
  25. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  26. package/dist/modules/customers/components/detail/PersonDetailHeader.js +3 -2
  27. package/dist/modules/customers/components/detail/PersonDetailHeader.js.map +2 -2
  28. package/dist/modules/customers/components/detail/RoleAssignmentRow.js +4 -5
  29. package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +2 -2
  30. package/dist/modules/customers/components/detail/utils.js +0 -7
  31. package/dist/modules/customers/components/detail/utils.js.map +2 -2
  32. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +3 -8
  33. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +2 -2
  34. package/dist/modules/dictionaries/components/AppearanceSelector.js +3 -4
  35. package/dist/modules/dictionaries/components/AppearanceSelector.js.map +2 -2
  36. package/dist/modules/integrations/backend/integrations/[id]/page.js +17 -17
  37. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  38. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js +1 -1
  39. package/dist/modules/planner/backend/planner/availability-rulesets/[id]/page.js.map +2 -2
  40. package/dist/modules/planner/components/AvailabilityRulesEditor.js +65 -1
  41. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  42. package/dist/modules/planner/lib/deleteAvailabilityRuleSet.js +20 -0
  43. package/dist/modules/planner/lib/deleteAvailabilityRuleSet.js.map +7 -0
  44. package/dist/modules/resources/backend/resources/resources/[id]/page.js +2 -2
  45. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  46. package/dist/modules/sales/api/quotes/accept/route.js +14 -37
  47. package/dist/modules/sales/api/quotes/accept/route.js.map +3 -3
  48. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +1 -1
  49. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  50. package/dist/modules/sales/backend/sales/documents/[id]/page.js +1 -1
  51. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  52. package/dist/modules/sales/commands/documents.js +6 -2
  53. package/dist/modules/sales/commands/documents.js.map +2 -2
  54. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +3 -2
  55. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  56. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +1 -1
  57. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  58. package/dist/modules/translations/components/TranslationDrawerAction.js +27 -65
  59. package/dist/modules/translations/components/TranslationDrawerAction.js.map +2 -2
  60. package/dist/modules/translations/components/TranslationManager.js +2 -2
  61. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  62. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +54 -92
  63. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  64. package/package.json +7 -7
  65. package/src/modules/api_docs/frontend/docs/api/Explorer.tsx +14 -14
  66. package/src/modules/attachments/components/AttachmentContentPreview.tsx +2 -2
  67. package/src/modules/audit_logs/backend/audit-logs/page.tsx +3 -3
  68. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +4 -8
  69. package/src/modules/customers/components/detail/ActivityCard.tsx +2 -4
  70. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +28 -55
  71. package/src/modules/customers/components/detail/ChangelogEntryRow.tsx +6 -4
  72. package/src/modules/customers/components/detail/CompanyDetailHeader.tsx +7 -3
  73. package/src/modules/customers/components/detail/DealLinkedEntitiesTab.tsx +11 -49
  74. package/src/modules/customers/components/detail/DetailTabsLayout.tsx +1 -1
  75. package/src/modules/customers/components/detail/ManageTagsDialog.tsx +27 -36
  76. package/src/modules/customers/components/detail/PersonCard.tsx +3 -4
  77. package/src/modules/customers/components/detail/PersonDetailHeader.tsx +3 -4
  78. package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +4 -7
  79. package/src/modules/customers/components/detail/utils.ts +0 -7
  80. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +4 -9
  81. package/src/modules/dictionaries/components/AppearanceSelector.tsx +3 -4
  82. package/src/modules/integrations/backend/integrations/[id]/page.tsx +21 -21
  83. package/src/modules/planner/backend/planner/availability-rulesets/[id]/page.tsx +1 -1
  84. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +62 -0
  85. package/src/modules/planner/lib/deleteAvailabilityRuleSet.ts +35 -0
  86. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +2 -2
  87. package/src/modules/sales/api/quotes/accept/route.ts +22 -38
  88. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +1 -1
  89. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +1 -1
  90. package/src/modules/sales/commands/documents.ts +16 -2
  91. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +3 -2
  92. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +1 -1
  93. package/src/modules/staff/i18n/de.json +5 -0
  94. package/src/modules/staff/i18n/en.json +5 -0
  95. package/src/modules/staff/i18n/es.json +5 -0
  96. package/src/modules/staff/i18n/pl.json +5 -0
  97. package/src/modules/translations/components/TranslationDrawerAction.tsx +31 -66
  98. package/src/modules/translations/components/TranslationManager.tsx +2 -2
  99. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +53 -84
@@ -3,6 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { Ellipsis } from 'lucide-react'
5
5
  import { Button } from '@open-mercato/ui/primitives/button'
6
+ import { ColorPicker } from '@open-mercato/ui/primitives/color-picker'
6
7
  import { Input } from '@open-mercato/ui/primitives/input'
7
8
  import { ICON_LIBRARY, ICON_SUGGESTIONS, type IconOption, renderDictionaryColor, renderDictionaryIcon } from './dictionaryAppearance'
8
9
 
@@ -119,12 +120,10 @@ export function AppearanceSelector({
119
120
  {labels.colorHelp ? <span className="text-xs font-normal text-muted-foreground">{labels.colorHelp}</span> : null}
120
121
  </label>
121
122
  <div className="flex flex-wrap items-center gap-2">
122
- <input
123
- type="color"
123
+ <ColorPicker
124
124
  value={normalizedColor}
125
- onChange={(event) => onColorChange(event.target.value)}
125
+ onChange={(next) => onColorChange(next)}
126
126
  disabled={disabled}
127
- className="h-10 w-12 cursor-pointer rounded border border-border bg-background"
128
127
  aria-label={labels.colorLabel}
129
128
  />
130
129
  <Button
@@ -148,16 +148,16 @@ type DataSyncRunDetail = {
148
148
  }
149
149
 
150
150
  const LOG_LEVEL_STYLES: Record<string, string> = {
151
- info: 'bg-blue-100 text-blue-800',
152
- warn: 'bg-yellow-100 text-yellow-800',
153
- error: 'bg-red-100 text-red-800',
151
+ info: 'bg-status-info-bg text-status-info-text',
152
+ warn: 'bg-status-warning-bg text-status-warning-text',
153
+ error: 'bg-status-error-bg text-status-error-text',
154
154
  }
155
155
 
156
156
  const HEALTH_STATUS_STYLES: Record<string, string> = {
157
- healthy: 'bg-green-100 text-green-800',
158
- degraded: 'bg-yellow-100 text-yellow-800',
159
- unhealthy: 'bg-red-100 text-red-800',
160
- unconfigured: 'bg-zinc-100 text-zinc-700',
157
+ healthy: 'bg-status-success-bg text-status-success-text',
158
+ degraded: 'bg-status-warning-bg text-status-warning-text',
159
+ unhealthy: 'bg-status-error-bg text-status-error-text',
160
+ unconfigured: 'bg-status-neutral-bg text-status-neutral-text',
161
161
  }
162
162
 
163
163
  const HEALTH_STATUS_ICONS: Record<string, React.ElementType> = {
@@ -203,12 +203,12 @@ function RunActivityStrip({
203
203
  const processed = typeof run.progressJob?.processedCount === 'number' ? run.progressJob.processedCount : 0
204
204
  const total = typeof run.progressJob?.totalCount === 'number' ? run.progressJob.totalCount : null
205
205
  const statusClass = run.status === 'completed'
206
- ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
206
+ ? 'border-status-success-border bg-status-success-bg text-status-success-text'
207
207
  : run.status === 'failed'
208
- ? 'border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300'
208
+ ? 'border-status-error-border bg-status-error-bg text-status-error-text'
209
209
  : run.status === 'cancelled'
210
- ? 'border-zinc-500/30 bg-zinc-500/10 text-zinc-700 dark:text-zinc-300'
211
- : 'border-blue-500/30 bg-blue-500/10 text-blue-700 dark:text-blue-300'
210
+ ? 'border-status-neutral-border bg-status-neutral-bg text-status-neutral-text'
211
+ : 'border-status-info-border bg-status-info-bg text-status-info-text'
212
212
 
213
213
  return (
214
214
  <div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/20 px-4 py-3 text-sm">
@@ -958,8 +958,8 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
958
958
  ] satisfies IntegrationDetailTab[]
959
959
  const StateIcon = resolvedState?.isEnabled ? CheckCircle2 : XCircle
960
960
  const stateBadgeClass = resolvedState?.isEnabled
961
- ? 'border-emerald-500/30 bg-emerald-500/10 text-emerald-300'
962
- : 'border-zinc-500/30 bg-zinc-500/10 text-zinc-300'
961
+ ? 'border-status-success-border bg-status-success-bg text-status-success-text'
962
+ : 'border-status-neutral-border bg-status-neutral-bg text-status-neutral-text'
963
963
 
964
964
  const showCredentialActions = showCredentialsTab && activeTab === 'credentials' && credentialFormFields.length > 0
965
965
 
@@ -1150,7 +1150,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1150
1150
  {showCredentialsTab ? (
1151
1151
  <TabsTrigger
1152
1152
  value="credentials"
1153
- className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1153
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-accent-indigo aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1154
1154
  >
1155
1155
  <span className="inline-flex items-center gap-2">
1156
1156
  <Key className="h-4 w-4" />
@@ -1161,7 +1161,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1161
1161
  {leadingInjectedTab ? (
1162
1162
  <TabsTrigger
1163
1163
  value={leadingInjectedTab.id}
1164
- className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1164
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-accent-indigo aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1165
1165
  >
1166
1166
  <span className="inline-flex items-center gap-2">
1167
1167
  <Settings className="h-4 w-4" />
@@ -1172,7 +1172,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1172
1172
  {showVersionTab ? (
1173
1173
  <TabsTrigger
1174
1174
  value="version"
1175
- className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1175
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-accent-indigo aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1176
1176
  >
1177
1177
  <span className="inline-flex items-center gap-2">
1178
1178
  <RefreshCw className="h-4 w-4" />
@@ -1183,7 +1183,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1183
1183
  {showDataSyncScheduleTab ? (
1184
1184
  <TabsTrigger
1185
1185
  value="data-sync-schedule"
1186
- className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1186
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-accent-indigo aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1187
1187
  >
1188
1188
  <span className="inline-flex items-center gap-2">
1189
1189
  <Calendar className="h-4 w-4" />
@@ -1194,7 +1194,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1194
1194
  {showHealthTab ? (
1195
1195
  <TabsTrigger
1196
1196
  value="health"
1197
- className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1197
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-accent-indigo aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1198
1198
  >
1199
1199
  <span className="inline-flex items-center gap-2">
1200
1200
  <Activity className="h-4 w-4" />
@@ -1205,7 +1205,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1205
1205
  {showLogsTab ? (
1206
1206
  <TabsTrigger
1207
1207
  value="logs"
1208
- className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1208
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-accent-indigo aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1209
1209
  >
1210
1210
  <span className="inline-flex items-center gap-2">
1211
1211
  <FileText className="h-4 w-4" />
@@ -1217,7 +1217,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1217
1217
  <TabsTrigger
1218
1218
  key={tab.id}
1219
1219
  value={tab.id}
1220
- className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-foreground aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1220
+ className="mr-8 h-auto rounded-none border-b-2 border-transparent bg-transparent px-0 py-2.5 text-sm font-medium text-muted-foreground shadow-none transition-colors hover:bg-transparent hover:text-foreground aria-selected:border-accent-indigo aria-selected:bg-transparent aria-selected:text-foreground aria-selected:shadow-none last:mr-0"
1221
1221
  >
1222
1222
  <span className="inline-flex items-center gap-2">
1223
1223
  <Settings className="h-4 w-4" />
@@ -1231,7 +1231,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
1231
1231
  <TabsContent value="credentials" className="mt-0">
1232
1232
  <section className="space-y-4 rounded-lg border bg-card p-6">
1233
1233
  {detail.bundle ? (
1234
- <div className="rounded-lg border border-blue-200 bg-blue-50 p-3 text-sm text-blue-800">
1234
+ <div className="rounded-lg border border-status-info-border bg-status-info-bg p-3 text-sm text-status-info-text">
1235
1235
  {t('integrations.detail.credentials.bundleShared', { bundle: detail.bundle.title })}
1236
1236
  </div>
1237
1237
  ) : null}
@@ -166,7 +166,7 @@ export default function PlannerAvailabilityRuleSetDetailPage({ params }: { param
166
166
  size="sm"
167
167
  className={`relative -mb-px h-auto rounded-none border-b-2 px-0 py-2 font-medium ${
168
168
  activeTab === tab.id
169
- ? 'border-primary text-foreground'
169
+ ? 'border-accent-indigo text-foreground'
170
170
  : 'border-transparent text-muted-foreground hover:text-foreground'
171
171
  }`}
172
172
  onClick={() => setActiveTab(tab.id as 'details' | 'availability')}
@@ -27,7 +27,9 @@ import {
27
27
  } from '@open-mercato/core/modules/planner/components/unavailabilityReasons'
28
28
  import { useT } from '@open-mercato/shared/lib/i18n/context'
29
29
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
30
+ import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
30
31
  import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
32
+ import { deleteAvailabilityRuleSet } from '@open-mercato/core/modules/planner/lib/deleteAvailabilityRuleSet'
31
33
  import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
32
34
  import { Calendar, Clock, List, PencilLine, Plus, Trash2 } from 'lucide-react'
33
35
  import {
@@ -80,6 +82,7 @@ export type AvailabilityRulesEditorProps = {
80
82
  initialTimezone?: string
81
83
  rulesetId?: string | null
82
84
  onRulesetChange?: (rulesetId: string | null) => Promise<void>
85
+ allowRuleSetDelete?: boolean
83
86
  buildScheduleItems: AvailabilityScheduleItemBuilder
84
87
  loadBookedEvents?: (range: ScheduleRange) => Promise<AvailabilityBookedEvent[]>
85
88
  readOnly?: boolean
@@ -357,6 +360,7 @@ export function AvailabilityRulesEditor({
357
360
  initialTimezone,
358
361
  rulesetId,
359
362
  onRulesetChange,
363
+ allowRuleSetDelete,
360
364
  buildScheduleItems,
361
365
  loadBookedEvents,
362
366
  readOnly,
@@ -469,6 +473,11 @@ export function AvailabilityRulesEditor({
469
473
  ruleSetCreateTimezoneLabel: t(`${labelPrefix}.availability.ruleset.createTimezone`, 'Timezone'),
470
474
  ruleSetCreateSuccess: t(`${labelPrefix}.availability.ruleset.createSuccess`, 'Schedule saved.'),
471
475
  ruleSetCreateError: t(`${labelPrefix}.availability.ruleset.createError`, 'Failed to save schedule.'),
476
+ ruleSetDelete: t(`${labelPrefix}.availability.ruleset.delete`, 'Delete schedule'),
477
+ ruleSetDeleteConfirm: t(`${labelPrefix}.availability.ruleset.deleteConfirm`, 'Delete schedule "{{name}}"?'),
478
+ ruleSetDeleteSubmit: t(`${labelPrefix}.availability.ruleset.deleteSubmit`, 'Delete'),
479
+ ruleSetDeleteSuccess: t(`${labelPrefix}.availability.ruleset.deleteSuccess`, 'Schedule deleted.'),
480
+ ruleSetDeleteError: t(`${labelPrefix}.availability.ruleset.deleteError`, 'Failed to delete schedule.'),
472
481
  editTitle: t(`${modeBase}.form.title.edit`, 'Edit availability'),
473
482
  addTitle: t(`${modeBase}.form.title.create`, 'Add availability'),
474
483
  applyLabel: t(`${labelPrefix}.availability.actions.apply`, 'Apply'),
@@ -1009,6 +1018,47 @@ export function AvailabilityRulesEditor({
1009
1018
  isReadOnly,
1010
1019
  ])
1011
1020
 
1021
+ const handleDeleteRuleSet = React.useCallback(async () => {
1022
+ if (isReadOnly || !onRulesetChange || !rulesetId) return
1023
+ const selected = ruleSets.find((entry) => entry.id === rulesetId)
1024
+ const name = selected?.name ?? rulesetId
1025
+ await deleteAvailabilityRuleSet({
1026
+ ruleSetId: rulesetId,
1027
+ confirmDelete: () => confirm({
1028
+ title: listLabels.ruleSetDeleteConfirm.replace('{{name}}', name),
1029
+ confirmText: listLabels.ruleSetDeleteSubmit,
1030
+ variant: 'destructive',
1031
+ }),
1032
+ deleteRuleSet: async (id) => {
1033
+ await deleteCrud('planner/availability-rule-sets', id, { errorMessage: listLabels.ruleSetDeleteError })
1034
+ },
1035
+ clearAssignment: async () => {
1036
+ setCustomOverridesEnabled(false)
1037
+ await onRulesetChange(null)
1038
+ await refreshAvailability()
1039
+ },
1040
+ refreshRuleSets,
1041
+ onSuccess: () => flash(listLabels.ruleSetDeleteSuccess, 'success'),
1042
+ onError: (error) => {
1043
+ console.error('planner.availability-rule-sets.delete', error)
1044
+ const normalized = normalizeCrudServerError(error)
1045
+ flash(normalized.message ?? listLabels.ruleSetDeleteError, 'error')
1046
+ },
1047
+ })
1048
+ }, [
1049
+ confirm,
1050
+ isReadOnly,
1051
+ listLabels.ruleSetDeleteConfirm,
1052
+ listLabels.ruleSetDeleteError,
1053
+ listLabels.ruleSetDeleteSubmit,
1054
+ listLabels.ruleSetDeleteSuccess,
1055
+ onRulesetChange,
1056
+ refreshAvailability,
1057
+ refreshRuleSets,
1058
+ ruleSets,
1059
+ rulesetId,
1060
+ ])
1061
+
1012
1062
  const ruleSetFormSchema = React.useMemo(
1013
1063
  () => z.object({
1014
1064
  name: z.string().min(1, t('ui.forms.errors.required', 'Required')),
@@ -1369,6 +1419,18 @@ export function AvailabilityRulesEditor({
1369
1419
  {listLabels.ruleSetReset}
1370
1420
  </Button>
1371
1421
  ) : null}
1422
+ {allowRuleSetDelete && rulesetId ? (
1423
+ <Button
1424
+ type="button"
1425
+ variant="destructive-outline"
1426
+ size="sm"
1427
+ onClick={() => { void handleDeleteRuleSet() }}
1428
+ disabled={isReadOnly}
1429
+ >
1430
+ <Trash2 className="size-4 mr-2" aria-hidden />
1431
+ {listLabels.ruleSetDelete}
1432
+ </Button>
1433
+ ) : null}
1372
1434
  </div>
1373
1435
  ) : null}
1374
1436
 
@@ -0,0 +1,35 @@
1
+ export type DeleteAvailabilityRuleSetOutcome = 'deleted' | 'cancelled' | 'noop' | 'failed'
2
+
3
+ export type DeleteAvailabilityRuleSetActions = {
4
+ ruleSetId: string | null | undefined
5
+ confirmDelete: () => Promise<boolean>
6
+ deleteRuleSet: (ruleSetId: string) => Promise<void>
7
+ clearAssignment: () => Promise<void>
8
+ refreshRuleSets: () => Promise<void>
9
+ onSuccess: () => void
10
+ onError: (error: unknown) => void
11
+ }
12
+
13
+ /**
14
+ * Orchestrates deleting an availability schedule (rule set) from the availability
15
+ * editor: confirm, delete the schedule, clear it from the current subject so no
16
+ * dangling assignment remains, refresh the selector, and report the outcome.
17
+ */
18
+ export async function deleteAvailabilityRuleSet(
19
+ actions: DeleteAvailabilityRuleSetActions,
20
+ ): Promise<DeleteAvailabilityRuleSetOutcome> {
21
+ const ruleSetId = actions.ruleSetId
22
+ if (!ruleSetId) return 'noop'
23
+ const confirmed = await actions.confirmDelete()
24
+ if (!confirmed) return 'cancelled'
25
+ try {
26
+ await actions.deleteRuleSet(ruleSetId)
27
+ await actions.clearAssignment()
28
+ await actions.refreshRuleSets()
29
+ actions.onSuccess()
30
+ return 'deleted'
31
+ } catch (error) {
32
+ actions.onError(error)
33
+ return 'failed'
34
+ }
35
+ }
@@ -543,7 +543,7 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
543
543
  size="sm"
544
544
  className={`relative -mb-px h-auto rounded-none border-b-2 px-0 py-2 font-medium ${
545
545
  activeTab === tab.id
546
- ? 'border-primary text-foreground'
546
+ ? 'border-accent-indigo text-foreground'
547
547
  : 'border-transparent text-muted-foreground hover:text-foreground'
548
548
  }`}
549
549
  onClick={() => setActiveTab(tab.id as 'details' | 'availability')}
@@ -567,7 +567,7 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
567
567
  size="sm"
568
568
  className={`relative -mb-px h-auto rounded-none border-b-2 px-0 py-1 font-medium ${
569
569
  activeDetailTab === tab.id
570
- ? 'border-primary text-foreground'
570
+ ? 'border-accent-indigo text-foreground'
571
571
  : 'border-transparent text-muted-foreground hover:text-foreground'
572
572
  }`}
573
573
  onClick={() => setActiveDetailTab(tab.id)}
@@ -67,7 +67,15 @@ export async function POST(req: Request) {
67
67
  const hashedToken = hashAuthToken(token)
68
68
  const tenantScope = auth?.tenantId ? { tenantId: auth.tenantId } : undefined
69
69
 
70
- const quote = await em.transactional(async (trx) => {
70
+ const commandBus = container.resolve('commandBus') as CommandBus
71
+
72
+ // Lock the quote, flip it to confirmed, and convert it to an order inside a
73
+ // single transaction. The conversion command reuses this transaction (and its
74
+ // PESSIMISTIC_WRITE lock) via ctx.transactionalEm, so the status flip and the
75
+ // order creation are atomic: if conversion fails the whole transaction rolls
76
+ // back, leaving the quote in its prior 'sent' state with no partial order and
77
+ // no need for an out-of-band compensating write.
78
+ const { quote, orderId } = await em.transactional(async (trx) => {
71
79
  const findQuoteByToken = (acceptanceToken: string) =>
72
80
  findOneWithDecryption(
73
81
  trx,
@@ -106,45 +114,21 @@ export async function POST(req: Request) {
106
114
  trx.persist(quote)
107
115
  await trx.flush()
108
116
 
109
- return quote
110
- })
117
+ const ctx: CommandRuntimeContext = {
118
+ container,
119
+ auth: null,
120
+ organizationScope: null,
121
+ selectedOrganizationId: quote.organizationId,
122
+ organizationIds: [quote.organizationId],
123
+ request: req,
124
+ transactionalEm: trx,
125
+ }
111
126
 
112
- const commandBus = container.resolve('commandBus') as CommandBus
113
- const ctx: CommandRuntimeContext = {
114
- container,
115
- auth: null,
116
- organizationScope: null,
117
- selectedOrganizationId: quote.organizationId,
118
- organizationIds: [quote.organizationId],
119
- request: req,
120
- }
127
+ const result = (await commandBus.execute('sales.quotes.convert_to_order', { input: { quoteId: quote.id }, ctx })) as ConvertToOrderResult | null
128
+ const orderId = result?.result?.orderId ?? result?.orderId ?? quote.id
121
129
 
122
- let result: ConvertToOrderResult | null
123
- try {
124
- result = (await commandBus.execute('sales.quotes.convert_to_order', { input: { quoteId: quote.id }, ctx })) as ConvertToOrderResult | null
125
- } catch (conversionError) {
126
- const freshEm = (container.resolve('em') as EntityManager).fork()
127
- const staleQuote = await findOneWithDecryption(
128
- freshEm,
129
- SalesQuote,
130
- { id: quote.id, deletedAt: null },
131
- {},
132
- tenantScope,
133
- )
134
- if (staleQuote) {
135
- staleQuote.status = 'sent'
136
- staleQuote.statusEntryId = await resolveStatusEntryIdByValue(freshEm, {
137
- tenantId: staleQuote.tenantId,
138
- organizationId: staleQuote.organizationId,
139
- value: 'sent',
140
- })
141
- staleQuote.updatedAt = new Date()
142
- freshEm.persist(staleQuote)
143
- await freshEm.flush()
144
- }
145
- throw conversionError
146
- }
147
- const orderId = result?.result?.orderId ?? result?.orderId ?? quote.id
130
+ return { quote, orderId }
131
+ })
148
132
 
149
133
  const order = await findOneWithDecryption(em, SalesOrder, { id: orderId, deletedAt: null }, {}, tenantScope)
150
134
  const orderNumber = order?.orderNumber ?? orderId
@@ -102,7 +102,7 @@ export default function EditChannelPage({ params }: { params?: { channelId?: str
102
102
  <button
103
103
  key={value}
104
104
  type="button"
105
- className={`px-4 py-2 text-sm font-medium border-b-2 ${activeTab === value ? 'border-primary text-primary' : 'border-transparent text-muted-foreground'}`}
105
+ className={`px-4 py-2 text-sm font-medium border-b-2 ${activeTab === value ? 'border-accent-indigo text-foreground' : 'border-transparent text-muted-foreground'}`}
106
106
  onClick={() => handleTabSelect(value)}
107
107
  >
108
108
  {label}
@@ -4783,7 +4783,7 @@ export default function SalesDocumentDetailPage({
4783
4783
  className={cn(
4784
4784
  'h-auto rounded-none border-b-2 px-3 py-2 text-sm font-medium transition-colors hover:bg-transparent',
4785
4785
  activeTab === tab.id
4786
- ? 'border-b-2 border-primary text-primary'
4786
+ ? 'border-b-2 border-accent-indigo text-foreground'
4787
4787
  : 'border-transparent text-muted-foreground hover:text-foreground'
4788
4788
  )}
4789
4789
  onClick={() => setActiveTab(tab.id)}
@@ -5870,7 +5870,12 @@ const convertQuoteToOrderCommand: CommandHandler<
5870
5870
  },
5871
5871
  async execute(rawInput, ctx) {
5872
5872
  const payload = quoteConvertToOrderSchema.parse(rawInput ?? {});
5873
- const rootEm = (ctx.container.resolve("em") as EntityManager).fork();
5873
+ // When the caller supplies an existing transactional EntityManager, reuse it
5874
+ // (and its row locks) so the quote status flip and the order materialization
5875
+ // commit or roll back as one atomic unit — no out-of-band compensating write.
5876
+ const callerEm = ctx.transactionalEm;
5877
+ const rootEm =
5878
+ callerEm ?? (ctx.container.resolve("em") as EntityManager).fork();
5874
5879
  const transactionalEm = rootEm as EntityManager & {
5875
5880
  transactional?: <TResult>(
5876
5881
  callback: (trx: EntityManager) => Promise<TResult>,
@@ -6195,12 +6200,21 @@ const convertQuoteToOrderCommand: CommandHandler<
6195
6200
 
6196
6201
  return { orderId: order.id };
6197
6202
  };
6203
+ if (callerEm) {
6204
+ // Already inside the caller's transaction — run directly so the whole flow
6205
+ // stays a single transaction (no nested savepoint).
6206
+ return runConversion(callerEm);
6207
+ }
6198
6208
  return typeof transactionalEm.transactional === "function"
6199
6209
  ? transactionalEm.transactional((trx) => runConversion(trx))
6200
6210
  : runConversion(rootEm);
6201
6211
  },
6202
6212
  captureAfter: async (_input, result, ctx) => {
6203
- const em = (ctx.container.resolve("em") as EntityManager).fork();
6213
+ // Prefer the caller's transactional EM so the just-created (still
6214
+ // uncommitted) order is visible when building the audit "after" snapshot.
6215
+ const em =
6216
+ ctx.transactionalEm ??
6217
+ (ctx.container.resolve("em") as EntityManager).fork();
6204
6218
  return loadOrderSnapshot(em, result.orderId);
6205
6219
  },
6206
6220
  buildLog: async ({ snapshots, result }) => {
@@ -369,7 +369,7 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
369
369
  onClick={() => setActivePanel(tab.id)}
370
370
  className={`relative -mb-px border-b-2 px-0 py-2 text-sm font-medium transition-colors ${
371
371
  activePanel === tab.id
372
- ? 'border-primary text-foreground'
372
+ ? 'border-accent-indigo text-foreground'
373
373
  : 'border-transparent text-muted-foreground hover:text-foreground'
374
374
  }`}
375
375
  >
@@ -431,7 +431,7 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
431
431
  onClick={() => setActiveTab(tab.id)}
432
432
  className={`relative -mb-px border-b-2 px-0 py-1 text-sm font-medium transition-colors ${
433
433
  activeTab === tab.id
434
- ? 'border-primary text-foreground'
434
+ ? 'border-accent-indigo text-foreground'
435
435
  : 'border-transparent text-muted-foreground hover:text-foreground'
436
436
  }`}
437
437
  >
@@ -556,6 +556,7 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
556
556
  mode="availability"
557
557
  rulesetId={availabilityRuleSetId}
558
558
  onRulesetChange={handleRulesetChange}
559
+ allowRuleSetDelete
559
560
  buildScheduleItems={({ availabilityRules, translate: translateLabel }) => (
560
561
  buildMemberScheduleItems({ availabilityRules, translate: translateLabel })
561
562
  )}
@@ -306,7 +306,7 @@ export default function StaffTeamEditPage({ params }: { params?: { id?: string }
306
306
  onClick={() => setActiveTab(tab.id as 'details' | 'members')}
307
307
  className={`relative -mb-px border-b-2 px-0 py-2 text-sm font-medium transition-colors ${
308
308
  activeTab === tab.id
309
- ? 'border-primary text-foreground'
309
+ ? 'border-accent-indigo text-foreground'
310
310
  : 'border-transparent text-muted-foreground hover:text-foreground'
311
311
  }`}
312
312
  >
@@ -481,6 +481,11 @@
481
481
  "staff.teamMembers.availability.ruleset.createTitle": "Als Zeitplan speichern",
482
482
  "staff.teamMembers.availability.ruleset.customize": "Zeitplan anpassen",
483
483
  "staff.teamMembers.availability.ruleset.customizePrompt": "Dieser Zeitplan basiert auf einem gemeinsamen Regelwerk. Passe ihn an, um Änderungen vorzunehmen.",
484
+ "staff.teamMembers.availability.ruleset.delete": "Zeitplan löschen",
485
+ "staff.teamMembers.availability.ruleset.deleteConfirm": "Zeitplan „{{name}}“ löschen?",
486
+ "staff.teamMembers.availability.ruleset.deleteError": "Zeitplan konnte nicht gelöscht werden.",
487
+ "staff.teamMembers.availability.ruleset.deleteSubmit": "Löschen",
488
+ "staff.teamMembers.availability.ruleset.deleteSuccess": "Zeitplan gelöscht.",
484
489
  "staff.teamMembers.availability.ruleset.error": "Zeitpläne konnten nicht geladen werden.",
485
490
  "staff.teamMembers.availability.ruleset.label": "Zeitplan",
486
491
  "staff.teamMembers.availability.ruleset.loading": "Zeitpläne werden geladen...",
@@ -481,6 +481,11 @@
481
481
  "staff.teamMembers.availability.ruleset.createTitle": "Save as schedule",
482
482
  "staff.teamMembers.availability.ruleset.customize": "Customize schedule",
483
483
  "staff.teamMembers.availability.ruleset.customizePrompt": "This schedule is based on a shared ruleset. Customize it to make changes.",
484
+ "staff.teamMembers.availability.ruleset.delete": "Delete schedule",
485
+ "staff.teamMembers.availability.ruleset.deleteConfirm": "Delete schedule \"{{name}}\"?",
486
+ "staff.teamMembers.availability.ruleset.deleteError": "Failed to delete schedule.",
487
+ "staff.teamMembers.availability.ruleset.deleteSubmit": "Delete",
488
+ "staff.teamMembers.availability.ruleset.deleteSuccess": "Schedule deleted.",
484
489
  "staff.teamMembers.availability.ruleset.error": "Failed to load schedules.",
485
490
  "staff.teamMembers.availability.ruleset.label": "Schedule",
486
491
  "staff.teamMembers.availability.ruleset.loading": "Loading schedules...",
@@ -481,6 +481,11 @@
481
481
  "staff.teamMembers.availability.ruleset.createTitle": "Guardar como horario",
482
482
  "staff.teamMembers.availability.ruleset.customize": "Personalizar horario",
483
483
  "staff.teamMembers.availability.ruleset.customizePrompt": "Este horario se basa en un conjunto de reglas compartido. Personalízalo para hacer cambios.",
484
+ "staff.teamMembers.availability.ruleset.delete": "Eliminar horario",
485
+ "staff.teamMembers.availability.ruleset.deleteConfirm": "¿Eliminar el horario \"{{name}}\"?",
486
+ "staff.teamMembers.availability.ruleset.deleteError": "No se pudo eliminar el horario.",
487
+ "staff.teamMembers.availability.ruleset.deleteSubmit": "Eliminar",
488
+ "staff.teamMembers.availability.ruleset.deleteSuccess": "Horario eliminado.",
484
489
  "staff.teamMembers.availability.ruleset.error": "No se pudieron cargar los horarios.",
485
490
  "staff.teamMembers.availability.ruleset.label": "Horario",
486
491
  "staff.teamMembers.availability.ruleset.loading": "Cargando horarios...",
@@ -481,6 +481,11 @@
481
481
  "staff.teamMembers.availability.ruleset.createTitle": "Zapisz jako harmonogram",
482
482
  "staff.teamMembers.availability.ruleset.customize": "Dostosuj harmonogram",
483
483
  "staff.teamMembers.availability.ruleset.customizePrompt": "Ten harmonogram jest oparty na wspólnym zestawie reguł. Dostosuj go, aby wprowadzić zmiany.",
484
+ "staff.teamMembers.availability.ruleset.delete": "Usuń harmonogram",
485
+ "staff.teamMembers.availability.ruleset.deleteConfirm": "Usunąć harmonogram „{{name}}”?",
486
+ "staff.teamMembers.availability.ruleset.deleteError": "Nie udało się usunąć harmonogramu.",
487
+ "staff.teamMembers.availability.ruleset.deleteSubmit": "Usuń",
488
+ "staff.teamMembers.availability.ruleset.deleteSuccess": "Harmonogram usunięty.",
484
489
  "staff.teamMembers.availability.ruleset.error": "Nie udało się wczytać harmonogramów.",
485
490
  "staff.teamMembers.availability.ruleset.label": "Harmonogram",
486
491
  "staff.teamMembers.availability.ruleset.loading": "Wczytywanie harmonogramów...",