@open-mercato/core 0.6.6-develop.6416.1.888fffc007 → 0.6.6-develop.6422.1.b3d024f05c
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/messages/api/[id]/actions/[actionId]/route.js +5 -1
- package/dist/modules/messages/api/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/messages/api/[id]/route.js +25 -2
- package/dist/modules/messages/api/[id]/route.js.map +2 -2
- package/dist/modules/messages/api/openapi.js +1 -0
- package/dist/modules/messages/api/openapi.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +8 -0
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +50 -24
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js +1 -0
- package/dist/modules/messages/components/message-detail/panels/composer-dialogs.js.map +2 -2
- package/dist/modules/messages/lib/constants.js +3 -1
- package/dist/modules/messages/lib/constants.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +56 -24
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/route.js +3 -28
- package/dist/modules/staff/api/timesheets/time-entries/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets/timeEntryListFilters.js +44 -0
- package/dist/modules/staff/lib/timesheets/timeEntryListFilters.js.map +7 -0
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +1 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +1 -2
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js +1 -6
- package/dist/modules/staff/widgets/injection/timer-sidebar-indicator/widget.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +6 -6
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/messages/api/[id]/actions/[actionId]/route.ts +5 -1
- package/src/modules/messages/api/[id]/route.ts +30 -1
- package/src/modules/messages/api/openapi.ts +1 -0
- package/src/modules/messages/commands/actions.ts +14 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +53 -28
- package/src/modules/messages/components/message-detail/panels/composer-dialogs.tsx +1 -0
- package/src/modules/messages/components/message-detail/types.ts +1 -0
- package/src/modules/messages/lib/constants.ts +4 -0
- package/src/modules/progress/AGENTS.md +6 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +85 -29
- package/src/modules/staff/api/timesheets/time-entries/route.ts +3 -32
- package/src/modules/staff/lib/timesheets/timeEntryListFilters.ts +64 -0
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +1 -2
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +2 -4
- package/src/modules/staff/widgets/injection/timer-sidebar-indicator/widget.tsx +1 -7
- package/src/modules/sync_excel/lib/adapters/customers.ts +6 -6
|
@@ -82,6 +82,12 @@ Use stable, grep-friendly ids:
|
|
|
82
82
|
| DataTable progress result handling | `packages/ui/src/backend/DataTable.tsx` |
|
|
83
83
|
| Progress UI merge/polling | `packages/ui/src/backend/progress/useProgressPoll.ts` and `useProgressSse.ts` |
|
|
84
84
|
|
|
85
|
+
## Environment
|
|
86
|
+
|
|
87
|
+
| Variable | Effect | Default |
|
|
88
|
+
|----------|--------|---------|
|
|
89
|
+
| `OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS` | Coalesces intermediate `progress.job.updated` flush+broadcasts per job: the service flushes and emits only when this many ms elapsed since the last broadcast **or** `progressPercent` advanced by ≥1; sub-threshold updates buffer in memory. Keeps bulk workers from firing one serialized `pg_notify` roundtrip + tenant-wide SSE fan-out per record. Terminal events (`created`/`started`/`completed`/`failed`/`cancelled`) are never throttled, so `ProgressTopBar` still converges and the 60s `STALE_JOB_TIMEOUT_SECONDS` heartbeat window is never exceeded. Set to `0` to restore per-record emission (tests/debugging). | `250` |
|
|
90
|
+
|
|
85
91
|
## Cross-References
|
|
86
92
|
|
|
87
93
|
- **Queue worker rules**: `packages/queue/AGENTS.md`
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { ProgressJob } from '../data/entities'
|
|
3
|
-
import type { ProgressService } from './progressService'
|
|
3
|
+
import type { ProgressService, ProgressServiceContext } from './progressService'
|
|
4
4
|
import { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from './progressService'
|
|
5
5
|
import { PROGRESS_EVENTS } from './events'
|
|
6
6
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
7
7
|
|
|
8
|
+
const DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250
|
|
9
|
+
|
|
10
|
+
// Minimum elapsed time between coalesced `progress.job.updated` flush+broadcasts for a
|
|
11
|
+
// single job. Bulk workers call updateProgress/incrementProgress once per record, and
|
|
12
|
+
// every emit of the `clientBroadcast: true` event pays a serialized pg_notify roundtrip
|
|
13
|
+
// plus a tenant-wide SSE fan-out. Setting the knob to 0 restores per-record emission.
|
|
14
|
+
function resolveBroadcastMinIntervalMs(): number {
|
|
15
|
+
const raw = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS
|
|
16
|
+
if (raw == null || raw.trim() === '') return DEFAULT_BROADCAST_MIN_INTERVAL_MS
|
|
17
|
+
const parsed = Number.parseInt(raw, 10)
|
|
18
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS
|
|
19
|
+
return parsed
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type JobUpdateThrottleEntry = {
|
|
23
|
+
job: ProgressJob
|
|
24
|
+
lastBroadcastAt: number
|
|
25
|
+
lastBroadcastPercent: number
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
function buildJobPayload(job: ProgressJob): Record<string, unknown> {
|
|
9
29
|
return {
|
|
10
30
|
jobId: job.id,
|
|
@@ -24,6 +44,60 @@ function buildJobPayload(job: ProgressJob): Record<string, unknown> {
|
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
export function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {
|
|
47
|
+
const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs()
|
|
48
|
+
// Per-job coalescing state, scoped to this service instance (request/worker scope).
|
|
49
|
+
// The cached managed entity doubles as the in-memory buffer: intermediate updates mutate
|
|
50
|
+
// it without flushing, and a single flush+emit at the throttle boundary persists the
|
|
51
|
+
// accumulated change. Domain commands fork their own EntityManager, so no other operation
|
|
52
|
+
// queries this job on `em` between calls, keeping the buffered changes safe until flush.
|
|
53
|
+
const jobUpdateThrottle = new Map<string, JobUpdateThrottleEntry>()
|
|
54
|
+
|
|
55
|
+
function jobScopeFilter(jobId: string, ctx: ProgressServiceContext) {
|
|
56
|
+
return {
|
|
57
|
+
id: jobId,
|
|
58
|
+
tenantId: ctx.tenantId,
|
|
59
|
+
...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function loadUpdatableJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob> {
|
|
64
|
+
const cached = jobUpdateThrottle.get(jobId)
|
|
65
|
+
if (cached) return cached.job
|
|
66
|
+
return em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function commitJobUpdate(job: ProgressJob, ctx: ProgressServiceContext): Promise<ProgressJob> {
|
|
70
|
+
const now = Date.now()
|
|
71
|
+
const cached = jobUpdateThrottle.get(job.id)
|
|
72
|
+
const shouldBroadcast =
|
|
73
|
+
broadcastMinIntervalMs <= 0 ||
|
|
74
|
+
cached == null ||
|
|
75
|
+
now - cached.lastBroadcastAt >= broadcastMinIntervalMs ||
|
|
76
|
+
Math.abs(job.progressPercent - cached.lastBroadcastPercent) >= 1
|
|
77
|
+
|
|
78
|
+
if (shouldBroadcast) {
|
|
79
|
+
await em.flush()
|
|
80
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
81
|
+
...buildJobPayload(job),
|
|
82
|
+
tenantId: ctx.tenantId,
|
|
83
|
+
organizationId: job.organizationId ?? null,
|
|
84
|
+
})
|
|
85
|
+
jobUpdateThrottle.set(job.id, { job, lastBroadcastAt: now, lastBroadcastPercent: job.progressPercent })
|
|
86
|
+
} else {
|
|
87
|
+
jobUpdateThrottle.set(job.id, {
|
|
88
|
+
job,
|
|
89
|
+
lastBroadcastAt: cached.lastBroadcastAt,
|
|
90
|
+
lastBroadcastPercent: cached.lastBroadcastPercent,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return job
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function forgetJobThrottle(jobId: string) {
|
|
98
|
+
jobUpdateThrottle.delete(jobId)
|
|
99
|
+
}
|
|
100
|
+
|
|
27
101
|
return {
|
|
28
102
|
async createJob(input, ctx) {
|
|
29
103
|
const job = em.create(ProgressJob, {
|
|
@@ -79,12 +153,9 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
79
153
|
},
|
|
80
154
|
|
|
81
155
|
async updateProgress(jobId, input, ctx) {
|
|
82
|
-
const job = await
|
|
83
|
-
id: jobId,
|
|
84
|
-
tenantId: ctx.tenantId,
|
|
85
|
-
...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
|
|
86
|
-
})
|
|
156
|
+
const job = await loadUpdatableJob(jobId, ctx)
|
|
87
157
|
if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
|
|
158
|
+
forgetJobThrottle(jobId)
|
|
88
159
|
return job
|
|
89
160
|
}
|
|
90
161
|
|
|
@@ -112,24 +183,13 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
112
183
|
|
|
113
184
|
job.heartbeatAt = new Date()
|
|
114
185
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
118
|
-
...buildJobPayload(job),
|
|
119
|
-
tenantId: ctx.tenantId,
|
|
120
|
-
organizationId: job.organizationId ?? null,
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
return job
|
|
186
|
+
return commitJobUpdate(job, ctx)
|
|
124
187
|
},
|
|
125
188
|
|
|
126
189
|
async incrementProgress(jobId, delta, ctx) {
|
|
127
|
-
const job = await
|
|
128
|
-
id: jobId,
|
|
129
|
-
tenantId: ctx.tenantId,
|
|
130
|
-
...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
|
|
131
|
-
})
|
|
190
|
+
const job = await loadUpdatableJob(jobId, ctx)
|
|
132
191
|
if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
|
|
192
|
+
forgetJobThrottle(jobId)
|
|
133
193
|
return job
|
|
134
194
|
}
|
|
135
195
|
|
|
@@ -143,15 +203,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
143
203
|
}
|
|
144
204
|
}
|
|
145
205
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
149
|
-
...buildJobPayload(job),
|
|
150
|
-
tenantId: ctx.tenantId,
|
|
151
|
-
organizationId: job.organizationId ?? null,
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
return job
|
|
206
|
+
return commitJobUpdate(job, ctx)
|
|
155
207
|
},
|
|
156
208
|
|
|
157
209
|
async completeJob(jobId, input, ctx) {
|
|
@@ -182,6 +234,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
182
234
|
organizationId: job.organizationId ?? null,
|
|
183
235
|
})
|
|
184
236
|
|
|
237
|
+
forgetJobThrottle(jobId)
|
|
185
238
|
return job
|
|
186
239
|
},
|
|
187
240
|
|
|
@@ -210,6 +263,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
210
263
|
organizationId: job.organizationId ?? null,
|
|
211
264
|
})
|
|
212
265
|
|
|
266
|
+
forgetJobThrottle(jobId)
|
|
213
267
|
return job
|
|
214
268
|
},
|
|
215
269
|
|
|
@@ -238,6 +292,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
238
292
|
organizationId: job.organizationId ?? null,
|
|
239
293
|
})
|
|
240
294
|
|
|
295
|
+
forgetJobThrottle(jobId)
|
|
241
296
|
return job
|
|
242
297
|
},
|
|
243
298
|
|
|
@@ -266,6 +321,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
266
321
|
organizationId: job.organizationId ?? null,
|
|
267
322
|
})
|
|
268
323
|
|
|
324
|
+
forgetJobThrottle(jobId)
|
|
269
325
|
return job
|
|
270
326
|
},
|
|
271
327
|
|
|
@@ -4,6 +4,7 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
4
4
|
import { resolveCrudRecordId, parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
|
|
5
5
|
import { StaffTimeEntry } from '../../../data/entities'
|
|
6
6
|
import { staffTimeEntryCreateSchema, staffTimeEntryUpdateSchema } from '../../../data/validators'
|
|
7
|
+
import { buildTimeEntryListFilters, isParseableDateFilter } from '../../../lib/timesheets/timeEntryListFilters'
|
|
7
8
|
import { createStaffCrudOpenApi, createPagedListResponseSchema, defaultOkResponseSchema } from '../../openapi'
|
|
8
9
|
|
|
9
10
|
const F = {
|
|
@@ -37,13 +38,6 @@ export const metadata = routeMetadata
|
|
|
37
38
|
|
|
38
39
|
const rawBodySchema = z.object({}).passthrough()
|
|
39
40
|
|
|
40
|
-
const isParseableDateFilter = (value: string): boolean => {
|
|
41
|
-
const trimmed = value.trim()
|
|
42
|
-
if (trimmed.length === 0) return true
|
|
43
|
-
if (!/^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(trimmed)) return false
|
|
44
|
-
return !Number.isNaN(new Date(trimmed).getTime())
|
|
45
|
-
}
|
|
46
|
-
|
|
47
41
|
const dateFilterSchema = z
|
|
48
42
|
.string()
|
|
49
43
|
.refine(isParseableDateFilter, { message: 'Invalid date' })
|
|
@@ -58,6 +52,7 @@ const listSchema = z
|
|
|
58
52
|
to: dateFilterSchema,
|
|
59
53
|
projectId: z.string().uuid().optional(),
|
|
60
54
|
ids: z.string().optional(),
|
|
55
|
+
running: z.string().optional(),
|
|
61
56
|
sortField: z.string().optional(),
|
|
62
57
|
sortDir: z.enum(['asc', 'desc']).optional(),
|
|
63
58
|
})
|
|
@@ -100,31 +95,7 @@ const crud = makeCrudRoute({
|
|
|
100
95
|
updatedAt: F.updated_at,
|
|
101
96
|
durationMinutes: F.duration_minutes,
|
|
102
97
|
},
|
|
103
|
-
buildFilters: async (query) =>
|
|
104
|
-
const filters: Record<string, unknown> = {}
|
|
105
|
-
if (typeof query.ids === 'string' && query.ids.trim().length > 0) {
|
|
106
|
-
const ids = query.ids
|
|
107
|
-
.split(',')
|
|
108
|
-
.map((value) => value.trim())
|
|
109
|
-
.filter((value) => value.length > 0)
|
|
110
|
-
if (ids.length > 0) {
|
|
111
|
-
filters[F.id] = { $in: ids }
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
if (typeof query.staffMemberId === 'string' && query.staffMemberId.length > 0) {
|
|
115
|
-
filters[F.staff_member_id] = query.staffMemberId
|
|
116
|
-
}
|
|
117
|
-
if (typeof query.from === 'string' && query.from.length > 0 && isParseableDateFilter(query.from)) {
|
|
118
|
-
filters[F.date] = { ...((filters[F.date] as Record<string, unknown>) ?? {}), $gte: query.from }
|
|
119
|
-
}
|
|
120
|
-
if (typeof query.to === 'string' && query.to.length > 0 && isParseableDateFilter(query.to)) {
|
|
121
|
-
filters[F.date] = { ...((filters[F.date] as Record<string, unknown>) ?? {}), $lte: query.to }
|
|
122
|
-
}
|
|
123
|
-
if (typeof query.projectId === 'string' && query.projectId.length > 0) {
|
|
124
|
-
filters[F.time_project_id] = query.projectId
|
|
125
|
-
}
|
|
126
|
-
return filters
|
|
127
|
-
},
|
|
98
|
+
buildFilters: async (query) => buildTimeEntryListFilters(query),
|
|
128
99
|
},
|
|
129
100
|
actions: {
|
|
130
101
|
create: {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
2
|
+
|
|
3
|
+
export type TimeEntryListFilterQuery = {
|
|
4
|
+
ids?: string
|
|
5
|
+
staffMemberId?: string
|
|
6
|
+
from?: string
|
|
7
|
+
to?: string
|
|
8
|
+
projectId?: string
|
|
9
|
+
running?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ID_COLUMN = 'id'
|
|
13
|
+
const STAFF_MEMBER_COLUMN = 'staff_member_id'
|
|
14
|
+
const DATE_COLUMN = 'date'
|
|
15
|
+
const STARTED_AT_COLUMN = 'started_at'
|
|
16
|
+
const ENDED_AT_COLUMN = 'ended_at'
|
|
17
|
+
const PROJECT_COLUMN = 'time_project_id'
|
|
18
|
+
|
|
19
|
+
export function isParseableDateFilter(value: string): boolean {
|
|
20
|
+
const trimmed = value.trim()
|
|
21
|
+
if (trimmed.length === 0) return true
|
|
22
|
+
if (!/^\d{4}-\d{2}-\d{2}([T ].*)?$/.test(trimmed)) return false
|
|
23
|
+
return !Number.isNaN(new Date(trimmed).getTime())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds the query-engine filter map for the time-entries list route.
|
|
28
|
+
*
|
|
29
|
+
* The `running` flag matches the currently-open timer regardless of its date
|
|
30
|
+
* (`started_at IS NOT NULL AND ended_at IS NULL`). A timer started before
|
|
31
|
+
* midnight is still running the next day, so a `from=today&to=today` lookup
|
|
32
|
+
* would miss it and leave the entry invisible/uncontrollable (issue #3717).
|
|
33
|
+
* This mirrors the single-active-timer check in the `start_timer` command so
|
|
34
|
+
* the UI can always surface and stop an orphaned overnight timer.
|
|
35
|
+
*/
|
|
36
|
+
export function buildTimeEntryListFilters(query: TimeEntryListFilterQuery): Record<string, unknown> {
|
|
37
|
+
const filters: Record<string, unknown> = {}
|
|
38
|
+
if (typeof query.ids === 'string' && query.ids.trim().length > 0) {
|
|
39
|
+
const ids = query.ids
|
|
40
|
+
.split(',')
|
|
41
|
+
.map((value) => value.trim())
|
|
42
|
+
.filter((value) => value.length > 0)
|
|
43
|
+
if (ids.length > 0) {
|
|
44
|
+
filters[ID_COLUMN] = { $in: ids }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (typeof query.staffMemberId === 'string' && query.staffMemberId.length > 0) {
|
|
48
|
+
filters[STAFF_MEMBER_COLUMN] = query.staffMemberId
|
|
49
|
+
}
|
|
50
|
+
if (typeof query.from === 'string' && query.from.length > 0 && isParseableDateFilter(query.from)) {
|
|
51
|
+
filters[DATE_COLUMN] = { ...((filters[DATE_COLUMN] as Record<string, unknown>) ?? {}), $gte: query.from }
|
|
52
|
+
}
|
|
53
|
+
if (typeof query.to === 'string' && query.to.length > 0 && isParseableDateFilter(query.to)) {
|
|
54
|
+
filters[DATE_COLUMN] = { ...((filters[DATE_COLUMN] as Record<string, unknown>) ?? {}), $lte: query.to }
|
|
55
|
+
}
|
|
56
|
+
if (typeof query.projectId === 'string' && query.projectId.length > 0) {
|
|
57
|
+
filters[PROJECT_COLUMN] = query.projectId
|
|
58
|
+
}
|
|
59
|
+
if (parseBooleanToken(query.running ?? null) === true) {
|
|
60
|
+
filters[STARTED_AT_COLUMN] = { $ne: null }
|
|
61
|
+
filters[ENDED_AT_COLUMN] = null
|
|
62
|
+
}
|
|
63
|
+
return filters
|
|
64
|
+
}
|
|
@@ -102,10 +102,9 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
102
102
|
useEffect(() => {
|
|
103
103
|
if (!staffMemberId) return
|
|
104
104
|
|
|
105
|
-
const today = getToday()
|
|
106
105
|
const checkActiveTimer = async () => {
|
|
107
106
|
const response = await apiCall(
|
|
108
|
-
`/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&
|
|
107
|
+
`/api/staff/timesheets/time-entries?staffMemberId=${staffMemberId}&running=true&pageSize=50`,
|
|
109
108
|
)
|
|
110
109
|
if (!response.ok) return
|
|
111
110
|
|
|
@@ -91,9 +91,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
91
91
|
const memberId = selfRes.member?.id ?? null
|
|
92
92
|
setStaffMemberId(memberId)
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Project details depend on the assignment ids and active entries depend on the staff
|
|
94
|
+
// Project details depend on the assignment ids and the active timer depends on the staff
|
|
97
95
|
// member id, but the two requests are independent of each other — fetch them together.
|
|
98
96
|
const [projectsRes, entriesRes] = await Promise.all([
|
|
99
97
|
projectIds.length > 0
|
|
@@ -105,7 +103,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
105
103
|
: Promise.resolve<{ items?: Array<Record<string, unknown>> }>({ items: [] }),
|
|
106
104
|
memberId
|
|
107
105
|
? readApiResultOrThrow<{ items?: Array<Record<string, unknown>> }>(
|
|
108
|
-
`/api/staff/timesheets/time-entries?staffMemberId=${memberId}&
|
|
106
|
+
`/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=100`,
|
|
109
107
|
undefined,
|
|
110
108
|
{ errorMessage: '', fallback: { items: [] } },
|
|
111
109
|
)
|
|
@@ -39,11 +39,6 @@ function formatElapsed(seconds: number): string {
|
|
|
39
39
|
return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function getToday(): string {
|
|
43
|
-
const now = new Date()
|
|
44
|
-
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
45
|
-
}
|
|
46
|
-
|
|
47
42
|
function TimerSidebarIndicator() {
|
|
48
43
|
const t = useT()
|
|
49
44
|
// Initialise from sessionStorage so the indicator doesn't flash away on navigation
|
|
@@ -63,9 +58,8 @@ function TimerSidebarIndicator() {
|
|
|
63
58
|
const memberId = selfRes.result?.member?.id
|
|
64
59
|
if (!memberId) { updateTimer(null); return }
|
|
65
60
|
|
|
66
|
-
const today = getToday()
|
|
67
61
|
const res = await apiCall<{ items?: Array<Record<string, unknown>> }>(
|
|
68
|
-
`/api/staff/timesheets/time-entries?staffMemberId=${memberId}&
|
|
62
|
+
`/api/staff/timesheets/time-entries?staffMemberId=${memberId}&running=true&pageSize=50`,
|
|
69
63
|
)
|
|
70
64
|
const items = (res.result?.items ?? []) as Array<Record<string, unknown>>
|
|
71
65
|
const active = items.find((e) => e.started_at && !e.ended_at)
|
|
@@ -1048,15 +1048,15 @@ export const syncExcelCustomersAdapter: DataSyncAdapter = {
|
|
|
1048
1048
|
const startOffset = cursor?.uploadId === upload.id ? cursor.offset : 0
|
|
1049
1049
|
const commandContext = buildCommandContext(container, input.scope)
|
|
1050
1050
|
const customFieldDefinitions = await loadImportCustomFieldDefinitions(em, input.scope)
|
|
1051
|
+
const emailDedupeIndex = await buildEmailDedupeIndex({
|
|
1052
|
+
em,
|
|
1053
|
+
rows: document.rows.slice(startOffset),
|
|
1054
|
+
mapping: input.mapping,
|
|
1055
|
+
scope: input.scope,
|
|
1056
|
+
})
|
|
1051
1057
|
|
|
1052
1058
|
for (let offset = startOffset, batchIndex = 0; offset < document.rows.length; offset += input.batchSize, batchIndex += 1) {
|
|
1053
1059
|
const batchRows = document.rows.slice(offset, offset + input.batchSize)
|
|
1054
|
-
const emailDedupeIndex = await buildEmailDedupeIndex({
|
|
1055
|
-
em,
|
|
1056
|
-
rows: batchRows,
|
|
1057
|
-
mapping: input.mapping,
|
|
1058
|
-
scope: input.scope,
|
|
1059
|
-
})
|
|
1060
1060
|
const items: ImportItem[] = []
|
|
1061
1061
|
|
|
1062
1062
|
for (let index = 0; index < batchRows.length; index += 1) {
|