@open-mercato/core 0.6.5-develop.5382.1.f542de69af → 0.6.6-develop.5412.1.e2a52b14f0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/crmFixtures.js +4 -0
- package/dist/helpers/integration/crmFixtures.js.map +2 -2
- package/dist/modules/attachments/api/route.js +2 -0
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/access.js +18 -0
- package/dist/modules/attachments/lib/access.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +3 -2
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +43 -2
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +402 -0
- package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +221 -56
- package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/cli.js +15 -9
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
- package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +100 -17
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
- package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
- package/dist/modules/customers/lib/dealsMetrics.js +82 -0
- package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +59 -27
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/entities.js +7 -0
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +26 -15
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
- package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
- package/dist/modules/query_index/lib/engine.js +4 -2
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/staff/api/team-members.js +9 -2
- package/dist/modules/staff/api/team-members.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/commands/team-members.js +1 -1
- package/dist/modules/staff/commands/team-members.js.map +2 -2
- package/dist/modules/staff/components/TeamMemberForm.js +1 -1
- package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
- package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
- package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
- package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
- package/package.json +8 -8
- package/src/helpers/integration/crmFixtures.ts +21 -1
- package/src/modules/attachments/AGENTS.md +79 -0
- package/src/modules/attachments/api/route.ts +2 -0
- package/src/modules/attachments/lib/access.ts +36 -0
- package/src/modules/auth/services/rbacService.ts +11 -2
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
- package/src/modules/customers/api/deals/route.ts +51 -2
- package/src/modules/customers/api/deals/summary/route.ts +496 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
- package/src/modules/customers/cli.ts +15 -15
- package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
- package/src/modules/customers/components/detail/DealForm.tsx +121 -19
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
- package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
- package/src/modules/customers/i18n/de.json +43 -0
- package/src/modules/customers/i18n/en.json +43 -0
- package/src/modules/customers/i18n/es.json +43 -0
- package/src/modules/customers/i18n/pl.json +43 -0
- package/src/modules/customers/lib/dealsMetrics.ts +159 -0
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
- package/src/modules/directory/utils/organizationScope.ts +85 -30
- package/src/modules/entities/api/entities.ts +11 -0
- package/src/modules/entities/api/records.ts +46 -25
- package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
- package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
- package/src/modules/entities/i18n/de.json +1 -0
- package/src/modules/entities/i18n/en.json +1 -0
- package/src/modules/entities/i18n/es.json +1 -0
- package/src/modules/entities/i18n/pl.json +1 -0
- package/src/modules/query_index/lib/engine.ts +11 -5
- package/src/modules/staff/api/team-members.ts +9 -2
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
- package/src/modules/staff/commands/team-members.ts +5 -2
- package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
- package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
- package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
- package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
- package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"entities.userEntities.records.errors.deleteFailed": "Nie udało się usunąć rekordu",
|
|
83
83
|
"entities.userEntities.records.errors.entityIdRequired": "Wymagany jest identyfikator encji",
|
|
84
84
|
"entities.userEntities.records.errors.recordIdRequired": "Wymagany jest identyfikator rekordu",
|
|
85
|
+
"entities.userEntities.records.errors.systemEntity": "Ta encja jest zarządzana systemowo. Rekordy są dostępne wyłącznie dla encji niestandardowych.",
|
|
85
86
|
"entities.userEntities.records.form.createTitle": "Utwórz rekord",
|
|
86
87
|
"entities.userEntities.records.form.editTitle": "Edytuj rekord",
|
|
87
88
|
"entities.userEntities.records.form.submitCreate": "Utwórz",
|
|
@@ -2,7 +2,7 @@ import type { QueryEngine, QueryOptions, QueryResult, FilterOp, Filter, QueryCus
|
|
|
2
2
|
import { SortDir } from '@open-mercato/shared/lib/query/types'
|
|
3
3
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
4
4
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
5
|
-
import { BasicQueryEngine, resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
5
|
+
import { BasicQueryEngine, resolveEntityTableName, resolveRegisteredEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
6
6
|
import { type Kysely, sql, type RawBuilder } from 'kysely'
|
|
7
7
|
import type { EventBus } from '@open-mercato/events'
|
|
8
8
|
import { readCoverageSnapshot, refreshCoverageSnapshot } from './coverage'
|
|
@@ -219,7 +219,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
219
219
|
const debugEnabled = this.isDebugVerbosity()
|
|
220
220
|
if (debugEnabled) this.debug('query:start', { entity })
|
|
221
221
|
|
|
222
|
-
const isCustom = await this.isCustomEntity(entity)
|
|
222
|
+
const isCustom = opts.forceCustomEntityStorage === true || await this.isCustomEntity(entity)
|
|
223
223
|
if (isCustom) {
|
|
224
224
|
if (debugEnabled) this.debug('query:custom-entity', { entity })
|
|
225
225
|
const section = profiler.section('custom_entity')
|
|
@@ -1005,6 +1005,14 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1005
1005
|
.executeTakeFirst()
|
|
1006
1006
|
if (row) {
|
|
1007
1007
|
result = true
|
|
1008
|
+
} else if (resolveRegisteredEntityTableName(this.em, entity) !== null) {
|
|
1009
|
+
// An id backed by a registered ORM table is never doc-storage-backed by
|
|
1010
|
+
// inference: stray `custom_entities_storage` rows for such an id (e.g. written
|
|
1011
|
+
// through the generic entities data engine) must not hijack every list/detail
|
|
1012
|
+
// read for the whole entity type away from its base table (#2939). Surfaces
|
|
1013
|
+
// that intentionally read doc records for a dual-declared id pass
|
|
1014
|
+
// `forceCustomEntityStorage` in QueryOptions instead.
|
|
1015
|
+
result = false
|
|
1008
1016
|
} else {
|
|
1009
1017
|
// Read/write symmetry. Records written through the entities data engine
|
|
1010
1018
|
// (`de.createCustomEntityRecord`) always land in `custom_entities_storage`,
|
|
@@ -1012,9 +1020,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1012
1020
|
// id — those are NEVER registered in `custom_entities` (install treats a
|
|
1013
1021
|
// system id as non-registrable). Without this fallback the query routes to
|
|
1014
1022
|
// the empty ORM/index path and those records are write-only (created with
|
|
1015
|
-
// 200 but unreadable on the edit form).
|
|
1016
|
-
// to `custom_entities_storage`, so this can only ever re-classify genuine
|
|
1017
|
-
// doc-storage entities — it cannot misroute table-backed entities.
|
|
1023
|
+
// 200 but unreadable on the edit form).
|
|
1018
1024
|
result = await this.hasCustomEntityStorageRows(entity)
|
|
1019
1025
|
}
|
|
1020
1026
|
} catch {
|
|
@@ -225,7 +225,12 @@ const crud = makeCrudRoute({
|
|
|
225
225
|
const { translate } = await resolveTranslations()
|
|
226
226
|
return parseScopedCommandInput(staffTeamMemberUpdateSchema, raw ?? {}, ctx, translate)
|
|
227
227
|
},
|
|
228
|
-
|
|
228
|
+
// Surface the freshly-bumped updatedAt so inline (non-CrudForm) callers can
|
|
229
|
+
// refresh their optimistic-lock token between sequential edits (#2848).
|
|
230
|
+
response: (arg: { result?: { updatedAt?: string | null } | null }) => ({
|
|
231
|
+
ok: true,
|
|
232
|
+
updatedAt: arg?.result?.updatedAt ?? null,
|
|
233
|
+
}),
|
|
229
234
|
},
|
|
230
235
|
delete: {
|
|
231
236
|
commandId: 'staff.team-members.delete',
|
|
@@ -287,7 +292,9 @@ export const openApi = createStaffCrudOpenApi({
|
|
|
287
292
|
},
|
|
288
293
|
update: {
|
|
289
294
|
schema: staffTeamMemberUpdateSchema,
|
|
290
|
-
responseSchema: defaultOkResponseSchema
|
|
295
|
+
responseSchema: defaultOkResponseSchema.extend({
|
|
296
|
+
updatedAt: z.string().nullable().optional(),
|
|
297
|
+
}),
|
|
291
298
|
description: 'Updates a team member by id.',
|
|
292
299
|
},
|
|
293
300
|
del: {
|
|
@@ -120,6 +120,36 @@ export async function POST(req: Request) {
|
|
|
120
120
|
})
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Single-active-timer invariant (issue #2855): reject the start when the
|
|
124
|
+
// staff member already has another running entry (started_at set,
|
|
125
|
+
// ended_at null). Without this guard a second surface (dashboard widget,
|
|
126
|
+
// another tab) could create and start a parallel timer, leaving two
|
|
127
|
+
// concurrent running entries and the "stopped timer comes back running"
|
|
128
|
+
// symptom reported in #2456.
|
|
129
|
+
const otherRunningEntry = await findOneWithDecryption(
|
|
130
|
+
trx,
|
|
131
|
+
StaffTimeEntry,
|
|
132
|
+
{
|
|
133
|
+
tenantId,
|
|
134
|
+
organizationId,
|
|
135
|
+
staffMemberId: lockedEntry.staffMemberId,
|
|
136
|
+
id: { $ne: lockedEntry.id },
|
|
137
|
+
startedAt: { $ne: null },
|
|
138
|
+
endedAt: null,
|
|
139
|
+
deletedAt: null,
|
|
140
|
+
},
|
|
141
|
+
{},
|
|
142
|
+
scopeCtx,
|
|
143
|
+
)
|
|
144
|
+
if (otherRunningEntry) {
|
|
145
|
+
throw new CrudHttpError(409, {
|
|
146
|
+
error: translate(
|
|
147
|
+
'staff.timesheets.errors.timerAlreadyRunning',
|
|
148
|
+
'Another timer is already running. Stop it before starting a new one.',
|
|
149
|
+
),
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
123
153
|
const startedAt = new Date()
|
|
124
154
|
lockedEntry.startedAt = startedAt
|
|
125
155
|
lockedEntry.source = 'timer'
|
|
@@ -184,7 +214,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
184
214
|
responses: [
|
|
185
215
|
{ status: 200, description: 'Timer started', schema: z.object({ ok: z.literal(true) }) },
|
|
186
216
|
{ status: 404, description: 'Time entry not found', schema: z.object({ error: z.string() }) },
|
|
187
|
-
{ status: 409, description: 'Timer already started', schema: z.object({ error: z.string() }) },
|
|
217
|
+
{ status: 409, description: 'Timer already started, or another timer is already running for this staff member', schema: z.object({ error: z.string() }) },
|
|
188
218
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
189
219
|
],
|
|
190
220
|
},
|
|
@@ -5,10 +5,10 @@ import Link from 'next/link'
|
|
|
5
5
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
6
6
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
|
-
import { readApiResultOrThrow
|
|
8
|
+
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
9
|
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
10
10
|
import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
11
|
-
import {
|
|
11
|
+
import { switchTeamMemberSchedule } from '@open-mercato/core/modules/staff/lib/scheduleSwitch'
|
|
12
12
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
13
13
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
14
14
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
@@ -276,6 +276,11 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
|
|
|
276
276
|
}
|
|
277
277
|
}, [memberId, router, searchParams, t])
|
|
278
278
|
|
|
279
|
+
// optimistic-lock-exempt: the inline updateCrud/deleteCrud below run inside the
|
|
280
|
+
// TeamMemberForm <CrudForm> host, which auto-derives the version header from
|
|
281
|
+
// initialValues.updatedAt and surfaces 409 conflicts for both submit and delete.
|
|
282
|
+
// The availability-schedule switch is version-locked separately via
|
|
283
|
+
// switchTeamMemberSchedule (see ../../../../lib/scheduleSwitch).
|
|
279
284
|
const handleSubmit = React.useCallback(async (values: TeamMemberFormValues) => {
|
|
280
285
|
if (!memberId) return
|
|
281
286
|
const payload = buildTeamMemberPayload(values, { id: memberId })
|
|
@@ -297,12 +302,17 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
|
|
|
297
302
|
|
|
298
303
|
const handleRulesetChange = React.useCallback(async (nextId: string | null) => {
|
|
299
304
|
if (!memberId) return
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
)
|
|
305
|
+
const { updatedAt: nextUpdatedAt } = await switchTeamMemberSchedule({
|
|
306
|
+
memberId,
|
|
307
|
+
nextRuleSetId: nextId,
|
|
308
|
+
expectedUpdatedAt: initialValues?.updatedAt,
|
|
309
|
+
t,
|
|
310
|
+
})
|
|
311
|
+
// Advance the optimistic-lock token so the next sequential switch sends the
|
|
312
|
+
// fresh updatedAt instead of the stale one and does not falsely 409 (#2848).
|
|
313
|
+
if (nextUpdatedAt) {
|
|
314
|
+
setInitialValues((prev) => (prev ? { ...prev, updatedAt: nextUpdatedAt } : prev))
|
|
315
|
+
}
|
|
306
316
|
setAvailabilityRuleSetId(nextId)
|
|
307
317
|
flash(t('staff.teamMembers.availability.ruleset.updateSuccess', 'Schedule updated.'), 'success')
|
|
308
318
|
}, [initialValues?.updatedAt, memberId, t])
|
|
@@ -292,7 +292,7 @@ const createTeamMemberCommand: CommandHandler<StaffTeamMemberCreateInput, { memb
|
|
|
292
292
|
},
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
const updateTeamMemberCommand: CommandHandler<StaffTeamMemberUpdateInput, { memberId: string }> = {
|
|
295
|
+
const updateTeamMemberCommand: CommandHandler<StaffTeamMemberUpdateInput, { memberId: string; updatedAt: string | null }> = {
|
|
296
296
|
id: 'staff.team-members.update',
|
|
297
297
|
async prepare(rawInput, ctx) {
|
|
298
298
|
const { parsed } = parseWithCustomFields(staffTeamMemberUpdateSchema, rawInput)
|
|
@@ -368,7 +368,10 @@ const updateTeamMemberCommand: CommandHandler<StaffTeamMemberUpdateInput, { memb
|
|
|
368
368
|
indexer: teamMemberCrudIndexer,
|
|
369
369
|
})
|
|
370
370
|
|
|
371
|
-
|
|
371
|
+
// Return the freshly-bumped updatedAt so non-CrudForm callers (e.g. the
|
|
372
|
+
// availability schedule switcher) can refresh their optimistic-lock token
|
|
373
|
+
// and not falsely 409 on the next sequential edit (#2848).
|
|
374
|
+
return { memberId: member.id, updatedAt: member.updatedAt ? member.updatedAt.toISOString() : null }
|
|
372
375
|
},
|
|
373
376
|
buildLog: async ({ snapshots, ctx }) => {
|
|
374
377
|
const before = snapshots.before as TeamMemberSnapshot | undefined
|
|
@@ -454,9 +454,12 @@ export function TeamMemberForm(props: TeamMemberFormProps) {
|
|
|
454
454
|
]
|
|
455
455
|
|
|
456
456
|
if (!tagsSection) {
|
|
457
|
+
// The tags field lives in its own card whose group title already reads
|
|
458
|
+
// "Tags" (see groups below). Leave the field label empty so the heading
|
|
459
|
+
// is not rendered twice in the team member edit view.
|
|
457
460
|
baseFields.splice(5, 0, {
|
|
458
461
|
id: 'tags',
|
|
459
|
-
label:
|
|
462
|
+
label: '',
|
|
460
463
|
type: 'tags',
|
|
461
464
|
placeholder: translate('staff.teamMembers.form.fields.tags.placeholder', 'Add tags'),
|
|
462
465
|
})
|
|
@@ -950,6 +950,7 @@
|
|
|
950
950
|
"staff.timesheets.errors.projectsKpis": "Failed to load project KPIs.",
|
|
951
951
|
"staff.timesheets.errors.segmentCreate": "Zeiteintragssegment konnte nicht erstellt werden.",
|
|
952
952
|
"staff.timesheets.errors.staffMemberNotFound": "Mitarbeiter nicht gefunden oder nicht zugänglich.",
|
|
953
|
+
"staff.timesheets.errors.timerAlreadyRunning": "Es läuft bereits ein anderer Timer. Stoppen Sie ihn, bevor Sie einen neuen starten.",
|
|
953
954
|
"staff.timesheets.errors.timerAlreadyStarted": "Der Timer ist für diesen Eintrag bereits gestartet.",
|
|
954
955
|
"staff.timesheets.errors.timerStart": "Timer konnte nicht gestartet werden.",
|
|
955
956
|
"staff.timesheets.errors.timerStop": "Timer konnte nicht gestoppt werden.",
|
|
@@ -950,6 +950,7 @@
|
|
|
950
950
|
"staff.timesheets.errors.projectsKpis": "Failed to load project KPIs.",
|
|
951
951
|
"staff.timesheets.errors.segmentCreate": "Failed to create time entry segment.",
|
|
952
952
|
"staff.timesheets.errors.staffMemberNotFound": "Staff member not found or not accessible.",
|
|
953
|
+
"staff.timesheets.errors.timerAlreadyRunning": "Another timer is already running. Stop it before starting a new one.",
|
|
953
954
|
"staff.timesheets.errors.timerAlreadyStarted": "Timer is already started for this entry.",
|
|
954
955
|
"staff.timesheets.errors.timerStart": "Failed to start timer.",
|
|
955
956
|
"staff.timesheets.errors.timerStop": "Failed to stop timer.",
|
|
@@ -950,6 +950,7 @@
|
|
|
950
950
|
"staff.timesheets.errors.projectsKpis": "Failed to load project KPIs.",
|
|
951
951
|
"staff.timesheets.errors.segmentCreate": "No se pudo crear el segmento del registro de tiempo.",
|
|
952
952
|
"staff.timesheets.errors.staffMemberNotFound": "Miembro del personal no encontrado o no accesible.",
|
|
953
|
+
"staff.timesheets.errors.timerAlreadyRunning": "Ya hay otro temporizador en marcha. Detenlo antes de iniciar uno nuevo.",
|
|
953
954
|
"staff.timesheets.errors.timerAlreadyStarted": "El temporizador ya está iniciado para este registro.",
|
|
954
955
|
"staff.timesheets.errors.timerStart": "No se pudo iniciar el temporizador.",
|
|
955
956
|
"staff.timesheets.errors.timerStop": "No se pudo detener el temporizador.",
|
|
@@ -950,6 +950,7 @@
|
|
|
950
950
|
"staff.timesheets.errors.projectsKpis": "Failed to load project KPIs.",
|
|
951
951
|
"staff.timesheets.errors.segmentCreate": "Nie udało się utworzyć segmentu wpisu czasu.",
|
|
952
952
|
"staff.timesheets.errors.staffMemberNotFound": "Pracownik nie został znaleziony lub jest niedostępny.",
|
|
953
|
+
"staff.timesheets.errors.timerAlreadyRunning": "Inny licznik czasu jest już uruchomiony. Zatrzymaj go przed rozpoczęciem nowego.",
|
|
953
954
|
"staff.timesheets.errors.timerAlreadyStarted": "Czasomierz jest już uruchomiony dla tego wpisu.",
|
|
954
955
|
"staff.timesheets.errors.timerStart": "Nie udało się uruchomić czasomierza.",
|
|
955
956
|
"staff.timesheets.errors.timerStop": "Nie udało się zatrzymać czasomierza.",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
2
|
+
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
3
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
4
|
+
import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
5
|
+
|
|
6
|
+
type Translate = (key: string, fallback?: string) => string
|
|
7
|
+
|
|
8
|
+
export type TeamMemberScheduleSwitchResult = {
|
|
9
|
+
/** The team member's freshly-bumped updatedAt, used to refresh the optimistic-lock token. */
|
|
10
|
+
updatedAt: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Persist a team member's selected availability schedule.
|
|
15
|
+
*
|
|
16
|
+
* Sends the optimistic-lock header derived from the caller's current
|
|
17
|
+
* `expectedUpdatedAt` and returns the server's freshly-bumped `updatedAt` so the
|
|
18
|
+
* caller can advance its token before the next sequential switch — otherwise the
|
|
19
|
+
* second switch reuses a stale version and falsely 409s (#2848).
|
|
20
|
+
*
|
|
21
|
+
* On an optimistic-lock conflict the shared conflict bar is surfaced before the
|
|
22
|
+
* error is re-thrown, so the selection reverts AND the user sees visible feedback
|
|
23
|
+
* instead of a silent revert.
|
|
24
|
+
*/
|
|
25
|
+
export async function switchTeamMemberSchedule(args: {
|
|
26
|
+
memberId: string
|
|
27
|
+
nextRuleSetId: string | null
|
|
28
|
+
expectedUpdatedAt: string | null | undefined
|
|
29
|
+
t: Translate
|
|
30
|
+
}): Promise<TeamMemberScheduleSwitchResult> {
|
|
31
|
+
const { memberId, nextRuleSetId, expectedUpdatedAt, t } = args
|
|
32
|
+
const headers = buildOptimisticLockHeader(expectedUpdatedAt)
|
|
33
|
+
try {
|
|
34
|
+
const call = await withScopedApiRequestHeaders(headers, () => (
|
|
35
|
+
updateCrud<{ ok?: boolean; updatedAt?: string | null }>(
|
|
36
|
+
'staff/team-members',
|
|
37
|
+
{ id: memberId, availabilityRuleSetId: nextRuleSetId },
|
|
38
|
+
{ errorMessage: t('staff.teamMembers.availability.ruleset.updateError', 'Failed to update schedule.') },
|
|
39
|
+
)
|
|
40
|
+
))
|
|
41
|
+
return { updatedAt: call?.result?.updatedAt ?? null }
|
|
42
|
+
} catch (error) {
|
|
43
|
+
surfaceRecordConflict(error, t)
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -53,8 +53,7 @@ export default function CreateWorkflowDefinitionPage() {
|
|
|
53
53
|
return (
|
|
54
54
|
<Page>
|
|
55
55
|
<PageBody>
|
|
56
|
-
<Alert variant="info" className="mb-6">
|
|
57
|
-
<Zap className="w-4 h-4" />
|
|
56
|
+
<Alert variant="info" icon={<Zap aria-hidden="true" />} className="mb-6">
|
|
58
57
|
<AlertTitle>{t('workflows.create.eventTriggersTitle')}</AlertTitle>
|
|
59
58
|
<AlertDescription>
|
|
60
59
|
{t('workflows.create.eventTriggersDescription')}
|
|
@@ -36,7 +36,7 @@ import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/u
|
|
|
36
36
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
37
37
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
38
38
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
39
|
-
import { CircleQuestionMark,
|
|
39
|
+
import { CircleQuestionMark, PanelTopClose, PanelTopOpen, Play, Save, Trash2 } from 'lucide-react'
|
|
40
40
|
import { NODE_TYPE_ICONS, NODE_TYPE_COLORS, NODE_TYPE_LABELS } from '../../../lib/node-type-icons'
|
|
41
41
|
import { DefinitionTriggersEditor } from '../../../components/DefinitionTriggersEditor'
|
|
42
42
|
import { MobileVisualEditor } from '../../../components/mobile/MobileVisualEditor'
|
|
@@ -1139,7 +1139,6 @@ export default function VisualEditorPage() {
|
|
|
1139
1139
|
|
|
1140
1140
|
{/* Instructions */}
|
|
1141
1141
|
<Alert variant="info" className="mt-6">
|
|
1142
|
-
<Info className="size-4" />
|
|
1143
1142
|
<AlertTitle className="text-xs">{t('workflows.visualEditor.howToUse', 'How to use:')}</AlertTitle>
|
|
1144
1143
|
<div className="mt-2">
|
|
1145
1144
|
<ul className="list-inside list-disc space-y-1 text-xs">
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
27
27
|
import { EventPatternInput } from '@open-mercato/ui/backend/inputs/EventPatternInput'
|
|
28
28
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
29
|
-
import { Plus, Trash2, Edit2, Zap,
|
|
29
|
+
import { Plus, Trash2, Edit2, Zap, X } from 'lucide-react'
|
|
30
30
|
import type { WorkflowDefinitionTrigger } from '../data/entities'
|
|
31
31
|
|
|
32
32
|
interface DefinitionTriggersEditorProps {
|
|
@@ -302,7 +302,6 @@ export function DefinitionTriggersEditor({
|
|
|
302
302
|
|
|
303
303
|
{value.length === 0 ? (
|
|
304
304
|
<Alert variant="info">
|
|
305
|
-
<Info className="w-4 h-4" />
|
|
306
305
|
<AlertTitle>{t('workflows.triggers.empty.title', 'No triggers configured')}</AlertTitle>
|
|
307
306
|
<AlertDescription>
|
|
308
307
|
{t('workflows.triggers.empty.description', 'Click "Add Trigger" to create an event trigger that automatically starts this workflow.')}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
SelectValue,
|
|
15
15
|
} from '@open-mercato/ui/primitives/select'
|
|
16
16
|
import {Alert, AlertDescription} from '@open-mercato/ui/primitives/alert'
|
|
17
|
-
import {ChevronDown,
|
|
17
|
+
import {ChevronDown, Plus, Trash2} from 'lucide-react'
|
|
18
18
|
import {sanitizeId} from '../lib/graph-utils'
|
|
19
19
|
import {WorkflowDefinition, WorkflowSelector} from './WorkflowSelector'
|
|
20
20
|
import {JsonBuilder} from '@open-mercato/ui/backend/JsonBuilder'
|
|
@@ -539,7 +539,6 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
|
|
|
539
539
|
<div className="space-y-4">
|
|
540
540
|
{!isEditable ? (
|
|
541
541
|
<Alert variant="info">
|
|
542
|
-
<Info className="size-4" />
|
|
543
542
|
<AlertDescription>
|
|
544
543
|
{t('workflows.nodeEditor.endStepsNotEditable')}
|
|
545
544
|
</AlertDescription>
|
|
@@ -548,7 +547,6 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
|
|
|
548
547
|
<div className="space-y-4">
|
|
549
548
|
{/* Info Alert for START nodes */}
|
|
550
549
|
<Alert variant="info">
|
|
551
|
-
<Info className="size-4" />
|
|
552
550
|
<AlertDescription>
|
|
553
551
|
{t('workflows.nodeEditor.startStepsInfo')}
|
|
554
552
|
</AlertDescription>
|
|
@@ -693,7 +691,6 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
|
|
|
693
691
|
{/* JSON Schema Format Notice */}
|
|
694
692
|
{isJsonSchemaFormat && (
|
|
695
693
|
<Alert variant="info" className="mb-3">
|
|
696
|
-
<Info className="size-4" />
|
|
697
694
|
<AlertDescription>
|
|
698
695
|
{t('workflows.nodeEditor.jsonSchemaFormat')}
|
|
699
696
|
</AlertDescription>
|
|
@@ -6,7 +6,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } f
|
|
|
6
6
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
7
7
|
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
8
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
|
-
import {
|
|
9
|
+
import { Trash2 } from 'lucide-react'
|
|
10
10
|
import { CrudForm, type CrudFormGroup, type CrudField, type CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'
|
|
11
11
|
import { JsonBuilder } from '@open-mercato/ui/backend/JsonBuilder'
|
|
12
12
|
import { FormFieldArrayEditor } from './fields/FormFieldArrayEditor'
|
|
@@ -115,8 +115,7 @@ export function NodeEditDialogCrudForm({ node, isOpen, onClose, onSave, onDelete
|
|
|
115
115
|
column: 1,
|
|
116
116
|
bare: true,
|
|
117
117
|
component: () => (
|
|
118
|
-
<Alert variant="
|
|
119
|
-
<Info className="size-4" />
|
|
118
|
+
<Alert variant="info">
|
|
120
119
|
<AlertDescription>
|
|
121
120
|
End nodes cannot be edited. They mark the completion of the workflow.
|
|
122
121
|
</AlertDescription>
|
|
@@ -134,8 +133,7 @@ export function NodeEditDialogCrudForm({ node, isOpen, onClose, onSave, onDelete
|
|
|
134
133
|
column: 1,
|
|
135
134
|
bare: true,
|
|
136
135
|
component: () => (
|
|
137
|
-
<Alert variant="
|
|
138
|
-
<Info className="size-4" />
|
|
136
|
+
<Alert variant="info" className="mb-4">
|
|
139
137
|
<AlertDescription>
|
|
140
138
|
Start nodes mark the beginning of the workflow. You can define pre-conditions that must pass before the workflow can be started.
|
|
141
139
|
</AlertDescription>
|
|
@@ -476,8 +474,7 @@ export function NodeEditDialogCrudForm({ node, isOpen, onClose, onSave, onDelete
|
|
|
476
474
|
<div className="flex-1 overflow-y-auto min-h-0 px-6">
|
|
477
475
|
{/* JSON Schema Conversion Warning */}
|
|
478
476
|
{showJsonSchemaWarning && (
|
|
479
|
-
<Alert variant="
|
|
480
|
-
<Info className="size-4" />
|
|
477
|
+
<Alert variant="info" className="mb-4">
|
|
481
478
|
<AlertDescription className="text-xs">
|
|
482
479
|
This form uses JSON Schema format. Fields have been converted for visual editing.
|
|
483
480
|
When you save, it will be converted to the simplified format. To preserve the original JSON Schema,
|
|
@@ -221,8 +221,7 @@ export default function WorkflowGraphImpl({
|
|
|
221
221
|
|
|
222
222
|
{editable && !isCompactViewport && (
|
|
223
223
|
<Panel position="top-left" style={{ margin: 10 }}>
|
|
224
|
-
<Alert variant="info" className="max-w-sm">
|
|
225
|
-
<Edit3 className="size-4" />
|
|
224
|
+
<Alert variant="info" icon={<Edit3 aria-hidden="true" />} className="max-w-sm">
|
|
226
225
|
<AlertDescription className="font-medium">
|
|
227
226
|
{t('workflows.graph.editModeInfo')}
|
|
228
227
|
</AlertDescription>
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
SelectValue,
|
|
16
16
|
} from '@open-mercato/ui/primitives/select'
|
|
17
17
|
import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
|
|
18
|
-
import { ChevronDown, Plus, Trash2
|
|
18
|
+
import { ChevronDown, Plus, Trash2 } from 'lucide-react'
|
|
19
19
|
import type { CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -134,8 +134,7 @@ export function FormFieldArrayEditor({
|
|
|
134
134
|
|
|
135
135
|
{/* JSON Schema Format Notice */}
|
|
136
136
|
{isJsonSchemaFormat && (
|
|
137
|
-
<Alert variant="
|
|
138
|
-
<Info className="size-4" />
|
|
137
|
+
<Alert variant="info">
|
|
139
138
|
<AlertDescription className="text-xs">
|
|
140
139
|
{t('workflows.fieldEditors.formFields.jsonSchemaNotice')}
|
|
141
140
|
</AlertDescription>
|