@open-mercato/core 0.6.6-develop.6367.1.2321ccacd4 → 0.6.6-develop.6373.1.5bece5409d

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.
@@ -103,88 +103,88 @@
103
103
  "customer_accounts.admin.flash.deactivated": "User deactivated",
104
104
  "customer_accounts.admin.flash.deleted": "User deleted",
105
105
  "customer_accounts.admin.inactive": "Inactive",
106
- "customer_accounts.admin.portalFeatures.addresses.manage": "Manage addresses",
107
- "customer_accounts.admin.portalFeatures.addresses.view": "View addresses",
108
- "customer_accounts.admin.portalFeatures.groups.addresses": "Addresses",
109
- "customer_accounts.admin.portalFeatures.groups.invoices": "Invoices",
110
- "customer_accounts.admin.portalFeatures.groups.orders": "Orders",
111
- "customer_accounts.admin.portalFeatures.groups.profile": "Profile",
112
- "customer_accounts.admin.portalFeatures.groups.quotes": "Quotes",
113
- "customer_accounts.admin.portalFeatures.groups.users": "Users",
114
- "customer_accounts.admin.portalFeatures.invoices.view": "View invoices",
115
- "customer_accounts.admin.portalFeatures.orders.create": "Create orders",
116
- "customer_accounts.admin.portalFeatures.orders.view": "View orders",
117
- "customer_accounts.admin.portalFeatures.profile.edit": "Edit profile",
118
- "customer_accounts.admin.portalFeatures.profile.view": "View profile",
119
- "customer_accounts.admin.portalFeatures.quotes.request": "Request quotes",
120
- "customer_accounts.admin.portalFeatures.quotes.view": "View quotes",
121
- "customer_accounts.admin.portalFeatures.users.invite": "Invite team members",
122
- "customer_accounts.admin.portalFeatures.users.manage": "Manage team members",
123
- "customer_accounts.admin.portalFeatures.users.view": "View team members",
106
+ "customer_accounts.admin.portalFeatures.addresses.manage": "Adressen verwalten",
107
+ "customer_accounts.admin.portalFeatures.addresses.view": "Adressen anzeigen",
108
+ "customer_accounts.admin.portalFeatures.groups.addresses": "Adressen",
109
+ "customer_accounts.admin.portalFeatures.groups.invoices": "Rechnungen",
110
+ "customer_accounts.admin.portalFeatures.groups.orders": "Bestellungen",
111
+ "customer_accounts.admin.portalFeatures.groups.profile": "Profil",
112
+ "customer_accounts.admin.portalFeatures.groups.quotes": "Angebote",
113
+ "customer_accounts.admin.portalFeatures.groups.users": "Benutzer",
114
+ "customer_accounts.admin.portalFeatures.invoices.view": "Rechnungen anzeigen",
115
+ "customer_accounts.admin.portalFeatures.orders.create": "Bestellungen erstellen",
116
+ "customer_accounts.admin.portalFeatures.orders.view": "Bestellungen anzeigen",
117
+ "customer_accounts.admin.portalFeatures.profile.edit": "Profil bearbeiten",
118
+ "customer_accounts.admin.portalFeatures.profile.view": "Profil anzeigen",
119
+ "customer_accounts.admin.portalFeatures.quotes.request": "Angebote anfordern",
120
+ "customer_accounts.admin.portalFeatures.quotes.view": "Angebote anzeigen",
121
+ "customer_accounts.admin.portalFeatures.users.invite": "Teammitglieder einladen",
122
+ "customer_accounts.admin.portalFeatures.users.manage": "Teammitglieder verwalten",
123
+ "customer_accounts.admin.portalFeatures.users.view": "Teammitglieder anzeigen",
124
124
  "customer_accounts.admin.portalInfo.credentials": "Demo credentials: alice.johnson@example.com / Password123!",
125
125
  "customer_accounts.admin.portalInfo.description": "Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.",
126
126
  "customer_accounts.admin.portalInfo.open": "Open Portal",
127
127
  "customer_accounts.admin.portalInfo.openConfiguration": "Open Configuration",
128
128
  "customer_accounts.admin.portalInfo.title": "Customer Portal",
129
129
  "customer_accounts.admin.portalInfo.url": "Portal URL: {url}",
130
- "customer_accounts.admin.roleCreate.actions.cancel": "Cancel",
131
- "customer_accounts.admin.roleCreate.actions.create": "Create Role",
132
- "customer_accounts.admin.roleCreate.actions.saving": "Creating...",
133
- "customer_accounts.admin.roleCreate.error.nameRequired": "Name is required",
134
- "customer_accounts.admin.roleCreate.error.required": "Name and slug are required",
135
- "customer_accounts.admin.roleCreate.error.save": "Failed to create role",
136
- "customer_accounts.admin.roleCreate.error.slugFormat": "Slug must contain only lowercase letters, numbers, hyphens, and underscores",
137
- "customer_accounts.admin.roleCreate.error.slugRequired": "Slug is required",
138
- "customer_accounts.admin.roleCreate.fields.customerAssignable": "Customers can self-assign",
139
- "customer_accounts.admin.roleCreate.fields.description": "Description",
140
- "customer_accounts.admin.roleCreate.fields.isDefault": "Default role (auto-assigned to new users)",
130
+ "customer_accounts.admin.roleCreate.actions.cancel": "Abbrechen",
131
+ "customer_accounts.admin.roleCreate.actions.create": "Rolle erstellen",
132
+ "customer_accounts.admin.roleCreate.actions.saving": "Erstellen...",
133
+ "customer_accounts.admin.roleCreate.error.nameRequired": "Name ist erforderlich",
134
+ "customer_accounts.admin.roleCreate.error.required": "Name und Slug sind erforderlich",
135
+ "customer_accounts.admin.roleCreate.error.save": "Rolle konnte nicht erstellt werden",
136
+ "customer_accounts.admin.roleCreate.error.slugFormat": "Slug darf nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche enthalten",
137
+ "customer_accounts.admin.roleCreate.error.slugRequired": "Slug ist erforderlich",
138
+ "customer_accounts.admin.roleCreate.fields.customerAssignable": "Kunden können sich selbst zuweisen",
139
+ "customer_accounts.admin.roleCreate.fields.description": "Beschreibung",
140
+ "customer_accounts.admin.roleCreate.fields.isDefault": "Standardrolle (wird neuen Benutzern automatisch zugewiesen)",
141
141
  "customer_accounts.admin.roleCreate.fields.name": "Name",
142
- "customer_accounts.admin.roleCreate.fields.namePlaceholder": "e.g. Buyer",
142
+ "customer_accounts.admin.roleCreate.fields.namePlaceholder": "z. B. Käufer",
143
143
  "customer_accounts.admin.roleCreate.fields.slug": "Slug",
144
- "customer_accounts.admin.roleCreate.fields.slugHint": "Lowercase letters, numbers, hyphens, and underscores only.",
145
- "customer_accounts.admin.roleCreate.fields.slugPlaceholder": "e.g. buyer",
146
- "customer_accounts.admin.roleCreate.flash.created": "Role created",
147
- "customer_accounts.admin.roleCreate.sections.details": "Role Details",
148
- "customer_accounts.admin.roleCreate.sections.options": "Options",
149
- "customer_accounts.admin.roleCreate.title": "Create Customer Role",
150
- "customer_accounts.admin.roleDetail.actions.backToList": "Back to roles",
151
- "customer_accounts.admin.roleDetail.actions.cancel": "Cancel",
152
- "customer_accounts.admin.roleDetail.actions.delete": "Delete",
153
- "customer_accounts.admin.roleDetail.actions.save": "Save Changes",
154
- "customer_accounts.admin.roleDetail.actions.saving": "Saving...",
155
- "customer_accounts.admin.roleDetail.error.load": "Failed to load role",
156
- "customer_accounts.admin.roleDetail.error.notFound": "Role not found",
157
- "customer_accounts.admin.roleDetail.error.save": "Failed to save role",
158
- "customer_accounts.admin.roleDetail.error.saveAcl": "Failed to save permissions",
159
- "customer_accounts.admin.roleDetail.fields.customerAssignable": "Customers can self-assign",
160
- "customer_accounts.admin.roleDetail.fields.description": "Description",
161
- "customer_accounts.admin.roleDetail.fields.isDefault": "Default role (auto-assigned to new users)",
144
+ "customer_accounts.admin.roleCreate.fields.slugHint": "Nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche.",
145
+ "customer_accounts.admin.roleCreate.fields.slugPlaceholder": "z. B. kaeufer",
146
+ "customer_accounts.admin.roleCreate.flash.created": "Rolle erstellt",
147
+ "customer_accounts.admin.roleCreate.sections.details": "Rollendetails",
148
+ "customer_accounts.admin.roleCreate.sections.options": "Optionen",
149
+ "customer_accounts.admin.roleCreate.title": "Kundenrolle erstellen",
150
+ "customer_accounts.admin.roleDetail.actions.backToList": "Zurück zu Rollen",
151
+ "customer_accounts.admin.roleDetail.actions.cancel": "Abbrechen",
152
+ "customer_accounts.admin.roleDetail.actions.delete": "Löschen",
153
+ "customer_accounts.admin.roleDetail.actions.save": "Änderungen speichern",
154
+ "customer_accounts.admin.roleDetail.actions.saving": "Speichern...",
155
+ "customer_accounts.admin.roleDetail.error.load": "Rolle konnte nicht geladen werden",
156
+ "customer_accounts.admin.roleDetail.error.notFound": "Rolle nicht gefunden",
157
+ "customer_accounts.admin.roleDetail.error.save": "Rolle konnte nicht gespeichert werden",
158
+ "customer_accounts.admin.roleDetail.error.saveAcl": "Berechtigungen konnten nicht gespeichert werden",
159
+ "customer_accounts.admin.roleDetail.fields.customerAssignable": "Kunden können sich selbst zuweisen",
160
+ "customer_accounts.admin.roleDetail.fields.description": "Beschreibung",
161
+ "customer_accounts.admin.roleDetail.fields.isDefault": "Standardrolle (wird neuen Benutzern automatisch zugewiesen)",
162
162
  "customer_accounts.admin.roleDetail.fields.name": "Name",
163
- "customer_accounts.admin.roleDetail.flash.saved": "Role updated",
164
- "customer_accounts.admin.roleDetail.loading": "Loading role...",
165
- "customer_accounts.admin.roleDetail.sections.details": "Role Details",
166
- "customer_accounts.admin.roleDetail.sections.options": "Options",
167
- "customer_accounts.admin.roleDetail.sections.permissions": "Portal Permissions",
168
- "customer_accounts.admin.roleDetail.selectAll": "Select all",
169
- "customer_accounts.admin.roles": "Roles",
170
- "customer_accounts.admin.roles.actions.create": "Create Role",
171
- "customer_accounts.admin.roles.actions.delete": "Delete",
172
- "customer_accounts.admin.roles.actions.edit": "Edit",
173
- "customer_accounts.admin.roles.assignable": "Yes",
174
- "customer_accounts.admin.roles.columns.customerAssignable": "Self-assignable",
175
- "customer_accounts.admin.roles.columns.description": "Description",
176
- "customer_accounts.admin.roles.columns.isDefault": "Default",
163
+ "customer_accounts.admin.roleDetail.flash.saved": "Rolle aktualisiert",
164
+ "customer_accounts.admin.roleDetail.loading": "Rolle wird geladen...",
165
+ "customer_accounts.admin.roleDetail.sections.details": "Rollendetails",
166
+ "customer_accounts.admin.roleDetail.sections.options": "Optionen",
167
+ "customer_accounts.admin.roleDetail.sections.permissions": "Portal-Berechtigungen",
168
+ "customer_accounts.admin.roleDetail.selectAll": "Alle auswählen",
169
+ "customer_accounts.admin.roles": "Rollen",
170
+ "customer_accounts.admin.roles.actions.create": "Rolle erstellen",
171
+ "customer_accounts.admin.roles.actions.delete": "Löschen",
172
+ "customer_accounts.admin.roles.actions.edit": "Bearbeiten",
173
+ "customer_accounts.admin.roles.assignable": "Ja",
174
+ "customer_accounts.admin.roles.columns.customerAssignable": "Selbst zuweisbar",
175
+ "customer_accounts.admin.roles.columns.description": "Beschreibung",
176
+ "customer_accounts.admin.roles.columns.isDefault": "Standard",
177
177
  "customer_accounts.admin.roles.columns.isSystem": "System",
178
178
  "customer_accounts.admin.roles.columns.name": "Name",
179
179
  "customer_accounts.admin.roles.columns.slug": "Slug",
180
- "customer_accounts.admin.roles.confirm.delete": "Delete role \"{{name}}\"?",
181
- "customer_accounts.admin.roles.default": "Default",
182
- "customer_accounts.admin.roles.error.delete": "Failed to delete role",
183
- "customer_accounts.admin.roles.error.deleteSystem": "System roles cannot be deleted",
184
- "customer_accounts.admin.roles.error.load": "Failed to load roles",
185
- "customer_accounts.admin.roles.flash.deleted": "Role deleted",
186
- "customer_accounts.admin.roles.notAssignable": "No",
187
- "customer_accounts.admin.roles.searchPlaceholder": "Search roles...",
180
+ "customer_accounts.admin.roles.confirm.delete": "Rolle \"{{name}}\" löschen?",
181
+ "customer_accounts.admin.roles.default": "Standard",
182
+ "customer_accounts.admin.roles.error.delete": "Rolle konnte nicht gelöscht werden",
183
+ "customer_accounts.admin.roles.error.deleteSystem": "Systemrollen können nicht gelöscht werden",
184
+ "customer_accounts.admin.roles.error.load": "Rollen konnten nicht geladen werden",
185
+ "customer_accounts.admin.roles.flash.deleted": "Rolle gelöscht",
186
+ "customer_accounts.admin.roles.notAssignable": "Nein",
187
+ "customer_accounts.admin.roles.searchPlaceholder": "Rollen suchen...",
188
188
  "customer_accounts.admin.roles.system": "System",
189
189
  "customer_accounts.admin.roles.title": "Rollen",
190
190
  "customer_accounts.admin.searchPlaceholder": "Search by name or email...",
@@ -263,8 +263,8 @@
263
263
  "customer_accounts.domainMapping.tls.retryButton": "Retry SSL",
264
264
  "customer_accounts.domainMapping.verifyNow": "Check Now",
265
265
  "customer_accounts.nav.group": "Customers",
266
- "customer_accounts.nav.role_create": "Create Role",
267
- "customer_accounts.nav.role_detail": "Role Detail",
266
+ "customer_accounts.nav.role_create": "Rolle erstellen",
267
+ "customer_accounts.nav.role_detail": "Rollendetails",
268
268
  "customer_accounts.nav.roles": "Kundenrollen",
269
269
  "customer_accounts.nav.user_detail": "User Detail",
270
270
  "customer_accounts.nav.users": "Kundenbenutzer",
@@ -50,6 +50,13 @@ import {
50
50
  } from '../../../lib/personCompanyLinkTable'
51
51
  import { normalizeCustomerDetailCustomFields } from '../../detailCustomFields'
52
52
  import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
53
+ import { runWithCacheTenant } from '@open-mercato/cache'
54
+ import {
55
+ buildCollectionTags,
56
+ canonicalizeResourceTag,
57
+ isCrudCacheEnabled,
58
+ resolveCrudCache,
59
+ } from '@open-mercato/shared/lib/crud/cache'
53
60
 
54
61
  export const metadata = {
55
62
  GET: { requireAuth: true, requireFeatures: ['customers.companies.view'] },
@@ -59,6 +66,66 @@ const paramsSchema = z.object({
59
66
  id: z.string().uuid(),
60
67
  })
61
68
 
69
+ const COMPANY_DETAIL_CACHE_TTL_MS = 60_000
70
+
71
+ // The customers write commands already invalidate these resource collections via
72
+ // invalidateCrudCache using these exact resourceKind strings. Reusing them as
73
+ // cache tags means the cached detail is flushed by those existing commands with
74
+ // no new wiring (#3664, mirroring #3143). Canonicalize each kind the same way
75
+ // invalidateCrudCache does (camelCase split + lowercase) so the tag shapes match.
76
+ const COMPANY_DETAIL_CACHE_RESOURCE_KINDS = [
77
+ 'customers.company',
78
+ 'customers.address',
79
+ 'customers.tagAssignment',
80
+ 'customers.labelAssignment',
81
+ 'customers.personCompanyLink',
82
+ 'customers.interaction',
83
+ 'customers.activity',
84
+ ] as const
85
+
86
+ function buildCompanyDetailCacheTags(tenantId: string | null, organizationId: string | null): string[] {
87
+ const tags = new Set<string>()
88
+ for (const resourceKind of COMPANY_DETAIL_CACHE_RESOURCE_KINDS) {
89
+ const resource = canonicalizeResourceTag(resourceKind) ?? resourceKind
90
+ for (const tag of buildCollectionTags(resource, tenantId, [organizationId])) {
91
+ tags.add(tag)
92
+ }
93
+ }
94
+ return Array.from(tags)
95
+ }
96
+
97
+ function buildCompanyDetailCacheKey(parts: {
98
+ tenantId: string | null
99
+ organizationId: string | null
100
+ companyId: string
101
+ selectedOrganizationId: string | null
102
+ filterIds: string[] | null
103
+ allowedIds: string[] | null
104
+ isSuperAdmin: boolean
105
+ viewerUserId: string | null
106
+ interactionMode: string
107
+ includeTokens: string[]
108
+ }): string {
109
+ const sortKeys = (values: string[]): string[] =>
110
+ [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
111
+ const filterIds = parts.filterIds ? sortKeys(parts.filterIds).join(',') : 'all'
112
+ const allowedIds = parts.allowedIds ? sortKeys(parts.allowedIds).join(',') : 'all'
113
+ const includeTokens = sortKeys(parts.includeTokens).join(',')
114
+ return [
115
+ 'customers:companies:detail',
116
+ `tenant:${parts.tenantId ?? 'null'}`,
117
+ `org:${parts.organizationId ?? 'null'}`,
118
+ `company:${parts.companyId}`,
119
+ `selected:${parts.selectedOrganizationId ?? 'null'}`,
120
+ `filter:${filterIds}`,
121
+ `allowed:${allowedIds}`,
122
+ `super:${parts.isSuperAdmin ? '1' : '0'}`,
123
+ `viewer:${parts.viewerUserId ?? 'null'}`,
124
+ `mode:${parts.interactionMode}`,
125
+ `include:${includeTokens}`,
126
+ ].join('|')
127
+ }
128
+
62
129
  function parseIncludeParams(request: Request): Set<string> {
63
130
  const url = new URL(request.url)
64
131
  const raw = url.searchParams.getAll('include')
@@ -414,6 +481,31 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
414
481
  userFeatures: undefined,
415
482
  })
416
483
 
484
+ // Opt-in cache (#3664): the company existence + org read-access checks above
485
+ // always run, so authorization is never served from cache. Only the expensive
486
+ // detail sweeps below are cached. Key on tenant/org/company + the RBAC scope,
487
+ // viewer (email privacy), interaction mode, and the requested include set.
488
+ const cacheTenantId = companyScope.tenantId
489
+ const cache = isCrudCacheEnabled() ? resolveCrudCache(container) : null
490
+ const cacheKey = cache
491
+ ? buildCompanyDetailCacheKey({
492
+ tenantId: cacheTenantId,
493
+ organizationId: companyScope.organizationId,
494
+ companyId: company.id,
495
+ selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,
496
+ filterIds: scope?.filterIds ?? null,
497
+ allowedIds: scope?.allowedIds ?? null,
498
+ isSuperAdmin: auth.isSuperAdmin === true,
499
+ viewerUserId,
500
+ interactionMode,
501
+ includeTokens: Array.from(includeTokens),
502
+ })
503
+ : null
504
+ if (cache && cacheKey) {
505
+ const cached = await runWithCacheTenant(cacheTenantId, () => cache.get(cacheKey))
506
+ if (cached) return NextResponse.json(cached)
507
+ }
508
+
417
509
  const shouldLoadCanonicalInteractions = includeInteractions || includeActivities || includeTodos
418
510
 
419
511
  // These reads only depend on the resolved company + scope + email visibility
@@ -961,7 +1053,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
961
1053
  addresses: addressesCount,
962
1054
  }
963
1055
 
964
- return NextResponse.json({
1056
+ const payload = {
965
1057
  interactionMode,
966
1058
  company: {
967
1059
  id: company.id,
@@ -1170,7 +1262,22 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
1170
1262
  name: viewerUserId ? userMap.get(viewerUserId)?.name ?? null : null,
1171
1263
  email: viewerUserId ? userMap.get(viewerUserId)?.email ?? auth.email ?? null : auth.email ?? null,
1172
1264
  },
1173
- })
1265
+ }
1266
+
1267
+ if (cache && cacheKey) {
1268
+ try {
1269
+ await runWithCacheTenant(cacheTenantId, () =>
1270
+ cache.set(cacheKey, payload, {
1271
+ ttl: COMPANY_DETAIL_CACHE_TTL_MS,
1272
+ tags: buildCompanyDetailCacheTags(cacheTenantId, companyScope.organizationId),
1273
+ }),
1274
+ )
1275
+ } catch (err) {
1276
+ console.warn('[customers:companies] Failed to set company detail cache', err)
1277
+ }
1278
+ }
1279
+
1280
+ return NextResponse.json(payload)
1174
1281
  }
1175
1282
 
1176
1283
  const companyDetailQuerySchema = z.object({
@@ -13,7 +13,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
13
13
  import { booleanOverrideSelectValue } from "./overrideFormConfig";
14
14
 
15
15
 
16
- export function renderDefaultValueCreateComponent(props: CrudCustomFieldRenderProps) {
16
+ export function DefaultValueField(props: CrudCustomFieldRenderProps) {
17
17
  const t = useT()
18
18
  const selectedType = props.values?.type as string;
19
19
 
@@ -132,7 +132,7 @@ export function createFieldDefinitions(
132
132
  id: 'defaultValue',
133
133
  label: '',
134
134
  type: 'custom',
135
- component: renderDefaultValueCreateComponent,
135
+ component: (props) => <DefaultValueField {...props} />,
136
136
  description: t('feature_toggles.form.fields.defaultValue.description'),
137
137
  },
138
138
  ]
@@ -20,7 +20,7 @@ export function booleanOverrideSelectValue(value: unknown): 'true' | 'false' {
20
20
  return value === true || value === 'true' ? 'true' : 'false'
21
21
  }
22
22
 
23
- export function renderOverrideValueComponent(props: CrudCustomFieldRenderProps) {
23
+ export function OverrideValueField(props: CrudCustomFieldRenderProps) {
24
24
  const t = useT()
25
25
  const toggleType = props.values?.toggleType as string;
26
26
  const isOverride = props.values?.isOverride as boolean;
@@ -119,7 +119,7 @@ export function createOverrideFieldDefinitions(
119
119
  id: 'overrideValue',
120
120
  label: '',
121
121
  type: 'custom',
122
- component: renderOverrideValueComponent,
122
+ component: (props) => <OverrideValueField {...props} />,
123
123
  description: t('feature_toggles.override.fields.overrideValue.description'),
124
124
  },
125
125
  ]
@@ -324,6 +324,29 @@ export function ActionCard({
324
324
  )
325
325
  }
326
326
 
327
+ if (action.status !== 'pending') {
328
+ const isProcessing = action.status === 'processing'
329
+ const statusLabel = action.status === 'accepted'
330
+ ? t('inbox_ops.status.accepted', 'Accepted')
331
+ : isProcessing
332
+ ? t('inbox_ops.status.processing', 'Processing')
333
+ : action.status
334
+ return (
335
+ <div className="border rounded-lg p-3 md:p-4 bg-muted/50">
336
+ <div className="flex items-center gap-2 mb-2">
337
+ {isProcessing ? (
338
+ <RefreshCw className="h-5 w-5 text-muted-foreground flex-shrink-0 animate-spin" />
339
+ ) : (
340
+ <CheckCircle className="h-5 w-5 text-muted-foreground flex-shrink-0" />
341
+ )}
342
+ <span className="text-sm font-medium">{label}</span>
343
+ <span className="text-xs text-muted-foreground">{statusLabel}</span>
344
+ </div>
345
+ <p className="text-sm text-muted-foreground">{displayDescription}</p>
346
+ </div>
347
+ )
348
+ }
349
+
327
350
  const hasNameIssue = hasContactNameIssue(action)
328
351
 
329
352
  return (
@@ -13,7 +13,7 @@ import { toErrorMessage } from './message-detail/utils'
13
13
 
14
14
  export type MessageFolder = 'inbox' | 'sent' | 'drafts' | 'archived' | 'all'
15
15
 
16
- export type MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'delete'
16
+ export type MessageBulkActionId = 'markRead' | 'markUnread' | 'archive' | 'unarchive' | 'delete'
17
17
 
18
18
  type BulkExecutionSummary = {
19
19
  action: MessageBulkActionId
@@ -84,6 +84,14 @@ const MESSAGE_BULK_REQUESTS: Record<MessageBulkActionId, MessageBulkRequestConfi
84
84
  errorKey: 'messages.errors.stateChangeFailed',
85
85
  errorFallback: 'Failed to update message state.',
86
86
  },
87
+ unarchive: {
88
+ method: 'DELETE',
89
+ buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}/archive`,
90
+ successKey: 'messages.bulk.flash.unarchiveSuccess',
91
+ successFallback: '{count} messages moved to inbox.',
92
+ errorKey: 'messages.errors.stateChangeFailed',
93
+ errorFallback: 'Failed to update message state.',
94
+ },
87
95
  delete: {
88
96
  method: 'DELETE',
89
97
  buildUrl: (messageId) => `/api/messages/${encodeURIComponent(messageId)}`,
@@ -110,6 +118,11 @@ const MESSAGE_BULK_ACTIONS: Record<MessageBulkActionId, MessageBulkActionDefinit
110
118
  labelKey: 'messages.actions.archive',
111
119
  labelFallback: 'Archive',
112
120
  },
121
+ unarchive: {
122
+ id: 'messages-unarchive',
123
+ labelKey: 'messages.actions.unarchive',
124
+ labelFallback: 'Unarchive',
125
+ },
113
126
  delete: {
114
127
  id: 'messages-delete',
115
128
  labelKey: 'messages.actions.delete',
@@ -120,7 +133,7 @@ const MESSAGE_BULK_ACTIONS: Record<MessageBulkActionId, MessageBulkActionDefinit
120
133
 
121
134
  const MESSAGE_BULK_ACTION_IDS_BY_FOLDER = {
122
135
  inbox: ['markRead', 'markUnread', 'archive', 'delete'],
123
- archived: ['markRead', 'markUnread', 'delete'],
136
+ archived: ['markRead', 'markUnread', 'unarchive', 'delete'],
124
137
  sent: ['delete'],
125
138
  drafts: ['delete'],
126
139
  all: [],
@@ -34,6 +34,7 @@
34
34
  "messages.bulk.flash.markReadSuccess": "{count} Nachrichten als gelesen markiert.",
35
35
  "messages.bulk.flash.markUnreadSuccess": "{count} Nachrichten als ungelesen markiert.",
36
36
  "messages.bulk.flash.partial": "{succeeded} von {total} Nachrichten verarbeitet; {failed} fehlgeschlagen.",
37
+ "messages.bulk.flash.unarchiveSuccess": "{count} Nachrichten in den Posteingang verschoben.",
37
38
  "messages.compose": "Nachricht verfassen",
38
39
  "messages.composeHint": "Erstelle eine Nachricht, hänge Objekte an und sende sie an ausgewählte Empfänger.",
39
40
  "messages.composer.attachmentPicker.confirm": "Auswahl anhängen",
@@ -34,6 +34,7 @@
34
34
  "messages.bulk.flash.markReadSuccess": "{count} messages marked as read.",
35
35
  "messages.bulk.flash.markUnreadSuccess": "{count} messages marked as unread.",
36
36
  "messages.bulk.flash.partial": "{succeeded} of {total} messages processed; {failed} failed.",
37
+ "messages.bulk.flash.unarchiveSuccess": "{count} messages moved to inbox.",
37
38
  "messages.compose": "Compose message",
38
39
  "messages.composeHint": "Create a message, attach objects, and send it to selected recipients.",
39
40
  "messages.composer.attachmentPicker.confirm": "Attach selected",
@@ -34,6 +34,7 @@
34
34
  "messages.bulk.flash.markReadSuccess": "{count} mensajes marcados como leídos.",
35
35
  "messages.bulk.flash.markUnreadSuccess": "{count} mensajes marcados como no leídos.",
36
36
  "messages.bulk.flash.partial": "Se procesaron {succeeded} de {total} mensajes; {failed} fallaron.",
37
+ "messages.bulk.flash.unarchiveSuccess": "{count} mensajes movidos a la bandeja de entrada.",
37
38
  "messages.compose": "Redactar mensaje",
38
39
  "messages.composeHint": "Crea un mensaje, adjunta objetos y envíalo a los destinatarios seleccionados.",
39
40
  "messages.composer.attachmentPicker.confirm": "Adjuntar seleccionados",
@@ -34,6 +34,7 @@
34
34
  "messages.bulk.flash.markReadSuccess": "Oznaczono {count} wiadomości jako przeczytane.",
35
35
  "messages.bulk.flash.markUnreadSuccess": "Oznaczono {count} wiadomości jako nieprzeczytane.",
36
36
  "messages.bulk.flash.partial": "Przetworzono {succeeded} z {total} wiadomości; niepowodzeń: {failed}.",
37
+ "messages.bulk.flash.unarchiveSuccess": "Przeniesiono {count} wiadomości do skrzynki odbiorczej.",
37
38
  "messages.compose": "Napisz wiadomość",
38
39
  "messages.composeHint": "Utwórz wiadomość, dołącz obiekty i wyślij do wybranych odbiorców.",
39
40
  "messages.composer.attachmentPicker.confirm": "Dołącz wybrane",