@open-mercato/core 0.6.4-develop.4331.1.64a8535120 → 0.6.4-develop.4358.1.233d5675c7
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/modules/customers/components/detail/ConfirmDealLostDialog.js +15 -10
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +2 -6
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/entities/api/records.js +2 -2
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/lib/helpers.js +25 -1
- package/dist/modules/entities/lib/helpers.js.map +2 -2
- package/dist/modules/entities/lib/validation.js +3 -1
- package/dist/modules/entities/lib/validation.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentDialog.js +10 -12
- package/dist/modules/sales/components/documents/AdjustmentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +10 -10
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/PaymentDialog.js +8 -15
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/ShipmentDialog.js +10 -14
- package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js +50 -17
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js +24 -10
- package/dist/modules/staff/api/timesheets/time-entries/[id]/segments/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +31 -12
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js +42 -29
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.js.map +2 -2
- package/dist/modules/workflows/components/EdgeEditDialog.js +3 -9
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +3 -9
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +15 -10
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +2 -6
- package/src/modules/entities/api/records.ts +2 -2
- package/src/modules/entities/lib/helpers.ts +29 -4
- package/src/modules/entities/lib/validation.ts +10 -2
- package/src/modules/sales/components/documents/AdjustmentDialog.tsx +11 -12
- package/src/modules/sales/components/documents/LineItemDialog.tsx +11 -10
- package/src/modules/sales/components/documents/PaymentDialog.tsx +9 -16
- package/src/modules/sales/components/documents/ShipmentDialog.tsx +10 -14
- package/src/modules/staff/api/timesheets/time-entries/[id]/segments/[segmentId]/route.ts +57 -18
- package/src/modules/staff/api/timesheets/time-entries/[id]/segments/route.ts +29 -10
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +38 -14
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-stop/route.ts +52 -34
- package/src/modules/workflows/components/EdgeEditDialog.tsx +3 -9
- package/src/modules/workflows/components/NodeEditDialog.tsx +3 -9
|
@@ -7,6 +7,7 @@ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
|
7
7
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
8
8
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
9
9
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
10
|
+
import { LockMode } from '@mikro-orm/core'
|
|
10
11
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
11
12
|
import { StaffTimeEntry, StaffTimeEntrySegment } from '../../../../../data/entities'
|
|
12
13
|
import { getStaffMemberByUserId } from '../../../../../lib/staffMemberResolver'
|
|
@@ -70,22 +71,6 @@ export async function POST(req: Request) {
|
|
|
70
71
|
throw new CrudHttpError(403, { error: translate('staff.timesheets.errors.notOwner', 'You can only manage your own time entries.') })
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
const segments = await findWithDecryption(
|
|
74
|
-
em,
|
|
75
|
-
StaffTimeEntrySegment,
|
|
76
|
-
{ timeEntryId: entry.id, tenantId, organizationId, deletedAt: null },
|
|
77
|
-
{},
|
|
78
|
-
scopeCtx,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
const activeSegment = segments.find((segment) => !segment.endedAt)
|
|
82
|
-
if (!activeSegment) {
|
|
83
|
-
return NextResponse.json(
|
|
84
|
-
{ error: translate('staff.timesheets.errors.noActiveSegment', 'No active timer segment found for this entry.') },
|
|
85
|
-
{ status: 409 },
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
74
|
const guardResult = await runStaffMutationGuards(
|
|
90
75
|
container,
|
|
91
76
|
{
|
|
@@ -107,29 +92,62 @@ export async function POST(req: Request) {
|
|
|
107
92
|
)
|
|
108
93
|
}
|
|
109
94
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
entry
|
|
95
|
+
// Recompute and persist the timer state inside a single transaction with a
|
|
96
|
+
// PESSIMISTIC_WRITE lock on the time entry row, so concurrent timer-stop /
|
|
97
|
+
// segment writes on the same entry serialize instead of racing on a shared
|
|
98
|
+
// in-memory snapshot (issue #2416).
|
|
99
|
+
const { now, durationMinutes } = await em.transactional(async (trx) => {
|
|
100
|
+
const lockedEntry = await findOneWithDecryption(
|
|
101
|
+
trx,
|
|
102
|
+
StaffTimeEntry,
|
|
103
|
+
{ id: entryId, tenantId, organizationId, deletedAt: null },
|
|
104
|
+
{ lockMode: LockMode.PESSIMISTIC_WRITE },
|
|
105
|
+
scopeCtx,
|
|
106
|
+
)
|
|
107
|
+
if (!lockedEntry) {
|
|
108
|
+
throw new CrudHttpError(404, { error: translate('staff.timesheets.errors.entryNotFound', 'Time entry not found.') })
|
|
109
|
+
}
|
|
113
110
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
const segments = await findWithDecryption(
|
|
112
|
+
trx,
|
|
113
|
+
StaffTimeEntrySegment,
|
|
114
|
+
{ timeEntryId: lockedEntry.id, tenantId, organizationId, deletedAt: null },
|
|
115
|
+
{},
|
|
116
|
+
scopeCtx,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const activeSegment = segments.find((segment) => !segment.endedAt)
|
|
120
|
+
if (!activeSegment) {
|
|
121
|
+
throw new CrudHttpError(409, {
|
|
122
|
+
error: translate('staff.timesheets.errors.noActiveSegment', 'No active timer segment found for this entry.'),
|
|
123
|
+
})
|
|
117
124
|
}
|
|
118
|
-
return segment
|
|
119
|
-
})
|
|
120
125
|
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
.
|
|
124
|
-
const startMs = new Date(segment.startedAt).getTime()
|
|
125
|
-
const endMs = new Date(segment.endedAt!).getTime()
|
|
126
|
-
return sum + (endMs - startMs)
|
|
127
|
-
}, 0)
|
|
126
|
+
const stoppedAt = new Date()
|
|
127
|
+
activeSegment.endedAt = stoppedAt
|
|
128
|
+
lockedEntry.endedAt = stoppedAt
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
const allSegments = segments.map((segment) => {
|
|
131
|
+
if (segment.id === activeSegment.id) {
|
|
132
|
+
return { ...segment, endedAt: stoppedAt }
|
|
133
|
+
}
|
|
134
|
+
return segment
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const totalWorkMinutes = allSegments
|
|
138
|
+
.filter((segment) => segment.segmentType === 'work' && segment.startedAt && segment.endedAt)
|
|
139
|
+
.reduce((sum, segment) => {
|
|
140
|
+
const startMs = new Date(segment.startedAt).getTime()
|
|
141
|
+
const endMs = new Date(segment.endedAt!).getTime()
|
|
142
|
+
return sum + (endMs - startMs)
|
|
143
|
+
}, 0)
|
|
131
144
|
|
|
132
|
-
|
|
145
|
+
const computedMinutes = Math.round(totalWorkMinutes / 60000)
|
|
146
|
+
lockedEntry.durationMinutes = computedMinutes
|
|
147
|
+
|
|
148
|
+
await trx.flush()
|
|
149
|
+
return { now: stoppedAt, durationMinutes: computedMinutes }
|
|
150
|
+
})
|
|
133
151
|
|
|
134
152
|
void emitStaffEvent('staff.timesheets.time_entry.timer_stopped', {
|
|
135
153
|
id: entry.id,
|
|
@@ -26,6 +26,7 @@ import {Plus, Trash2} from 'lucide-react'
|
|
|
26
26
|
import {type BusinessRule, BusinessRulesSelector} from './BusinessRulesSelector'
|
|
27
27
|
import {JsonBuilder} from '@open-mercato/ui/backend/JsonBuilder'
|
|
28
28
|
import {useT} from '@open-mercato/shared/lib/i18n/context'
|
|
29
|
+
import {useDialogKeyHandler} from '@open-mercato/ui/hooks/useDialogKeyHandler'
|
|
29
30
|
import {useConfirmDialog} from '@open-mercato/ui/backend/confirm-dialog'
|
|
30
31
|
|
|
31
32
|
export interface EdgeEditDialogProps {
|
|
@@ -302,14 +303,7 @@ export function EdgeEditDialog({ edge, isOpen, onClose, onSave, onDelete }: Edge
|
|
|
302
303
|
onDelete(edge.id)
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
const handleKeyDown = (
|
|
306
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
307
|
-
handleSave()
|
|
308
|
-
}
|
|
309
|
-
if (e.key === 'Escape') {
|
|
310
|
-
onClose()
|
|
311
|
-
}
|
|
312
|
-
}
|
|
306
|
+
const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave, onCancel: onClose })
|
|
313
307
|
|
|
314
308
|
if (!isOpen || !edge) return null
|
|
315
309
|
|
|
@@ -317,7 +311,7 @@ export function EdgeEditDialog({ edge, isOpen, onClose, onSave, onDelete }: Edge
|
|
|
317
311
|
|
|
318
312
|
return (
|
|
319
313
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
320
|
-
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
314
|
+
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto" onKeyDown={handleKeyDown}>
|
|
321
315
|
<DialogHeader>
|
|
322
316
|
<div className="flex items-center gap-2 mb-2">
|
|
323
317
|
<DialogTitle>{t('workflows.edgeEditor.title')}</DialogTitle>
|
|
@@ -20,6 +20,7 @@ import {WorkflowDefinition, WorkflowSelector} from './WorkflowSelector'
|
|
|
20
20
|
import {JsonBuilder} from '@open-mercato/ui/backend/JsonBuilder'
|
|
21
21
|
import {StartPreConditionsEditor, type StartPreCondition} from './fields/StartPreConditionsEditor'
|
|
22
22
|
import {useT} from '@open-mercato/shared/lib/i18n/context'
|
|
23
|
+
import {useDialogKeyHandler} from '@open-mercato/ui/hooks/useDialogKeyHandler'
|
|
23
24
|
import {useConfirmDialog} from '@open-mercato/ui/backend/confirm-dialog'
|
|
24
25
|
import {isFutureIsoDateString, isValidDurationString} from '../data/validators'
|
|
25
26
|
|
|
@@ -485,14 +486,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
|
|
|
485
486
|
onDelete(node.id)
|
|
486
487
|
}
|
|
487
488
|
|
|
488
|
-
const handleKeyDown = (
|
|
489
|
-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
490
|
-
handleSave()
|
|
491
|
-
}
|
|
492
|
-
if (e.key === 'Escape') {
|
|
493
|
-
onClose()
|
|
494
|
-
}
|
|
495
|
-
}
|
|
489
|
+
const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave, onCancel: onClose })
|
|
496
490
|
|
|
497
491
|
if (!isOpen || !node) return null
|
|
498
492
|
|
|
@@ -518,7 +512,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
|
|
|
518
512
|
|
|
519
513
|
return (
|
|
520
514
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
521
|
-
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
515
|
+
<DialogContent className="sm:max-w-2xl max-h-[90vh] overflow-y-auto" onKeyDown={handleKeyDown}>
|
|
522
516
|
<DialogHeader>
|
|
523
517
|
<div className="flex items-center gap-2 mb-2">
|
|
524
518
|
<DialogTitle>{t('workflows.nodeEditor.title')}</DialogTitle>
|