logwork-cli 1.1.0 → 1.2.0
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/README.md +35 -9
- package/RELEASE.md +1 -0
- package/commands/commits.mjs +57 -6
- package/commands/history.mjs +0 -4
- package/commands/quick.mjs +38 -23
- package/commands/reminder.mjs +41 -2
- package/commands/setup.mjs +157 -19
- package/commands/tui.mjs +1 -0
- package/docs/advanced.md +12 -2
- package/docs/mcp-setup.md +57 -15
- package/docs/operations-guide.md +37 -2
- package/docs/recipes.md +27 -0
- package/docs/security.md +3 -1
- package/examples/mcp/antigravity.mcp_config.json +1 -1
- package/examples/mcp/codex.config.toml +1 -1
- package/examples/mcp/copilot.mcp.json +1 -1
- package/lib/api-auth-forms.mjs +393 -0
- package/lib/api-auth-http.mjs +184 -0
- package/lib/api-auth-support.mjs +101 -0
- package/lib/api-auth-tokens.mjs +129 -0
- package/lib/api-auth.mjs +39 -813
- package/lib/api.mjs +38 -1422
- package/lib/apply-ledger.mjs +1 -1
- package/lib/batch-parser.mjs +23 -0
- package/lib/batch-workflow.mjs +201 -2
- package/lib/commit-workflow.mjs +278 -30
- package/lib/config.mjs +7 -0
- package/lib/diagnostics.mjs +53 -4
- package/lib/jira-api.mjs +25 -3
- package/lib/jira-workflow.mjs +36 -6
- package/lib/keychain.mjs +19 -12
- package/lib/logwork-config.mjs +64 -13
- package/lib/manual-apply-state.mjs +15 -0
- package/lib/manual-commands.mjs +2 -2
- package/lib/manual-ink-app.mjs +108 -1281
- package/lib/manual-ink-draft-actions.mjs +227 -0
- package/lib/manual-ink-interaction.mjs +306 -0
- package/lib/manual-ink-session.mjs +41 -0
- package/lib/manual-ink-task-editor.mjs +338 -0
- package/lib/manual-ink-wizard.mjs +520 -0
- package/lib/manual-mcp.mjs +27 -15
- package/lib/manual-repl.mjs +39 -3
- package/lib/mcp-server.mjs +63 -21
- package/lib/node-runtime.mjs +115 -0
- package/lib/npm-command.mjs +29 -0
- package/lib/paths.mjs +11 -1
- package/lib/project-mapping-workflow.mjs +30 -7
- package/lib/query-workflow.mjs +0 -1
- package/lib/reconciliation-workflow.mjs +1 -1
- package/lib/release-doctor.mjs +4 -2
- package/lib/reminder-config.mjs +38 -2
- package/lib/reminder-scheduler.mjs +54 -3
- package/lib/reminder-service.mjs +28 -3
- package/lib/reminder-workflow.mjs +118 -22
- package/lib/ro-api-diagnostics.mjs +109 -0
- package/lib/ro-api-entries.mjs +359 -0
- package/lib/ro-api-fields.mjs +290 -0
- package/lib/ro-api-http.mjs +68 -0
- package/lib/ro-api-projects.mjs +137 -0
- package/lib/ro-api-tasks.mjs +50 -0
- package/lib/ro-api-timesheet.mjs +413 -0
- package/lib/ro-delete-workflow.mjs +5 -8
- package/lib/ro-edit-workflow.mjs +66 -8
- package/lib/ro-entry-reader.mjs +49 -6
- package/lib/ro-resubmit-workflow.mjs +35 -21
- package/lib/terminal-auth-prompts.mjs +7 -4
- package/lib/ui.mjs +80 -59
- package/lib/update-service.mjs +5 -2
- package/package.json +11 -4
- package/skills/logwork/SKILL.md +6 -4
package/lib/api.mjs
CHANGED
|
@@ -1,1422 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
timeoutMs,
|
|
40
|
-
retryDelayMs,
|
|
41
|
-
retries: retries ?? CONFIG.httpReadRetries,
|
|
42
|
-
idempotent: isIdempotentMethod(method),
|
|
43
|
-
method,
|
|
44
|
-
headers: {
|
|
45
|
-
Authorization: `Bearer ${token}`,
|
|
46
|
-
Accept: 'application/json',
|
|
47
|
-
'Content-Type': 'application/json',
|
|
48
|
-
...requestOptions.headers
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const text = await response.text();
|
|
53
|
-
const data = text ? safeJsonParse(text, text) : null;
|
|
54
|
-
const safeBody = redactedExcerpt(text);
|
|
55
|
-
|
|
56
|
-
if (response.status === 401) {
|
|
57
|
-
throw new ApiError('API request returned 401 Unauthorized.', {
|
|
58
|
-
status: response.status,
|
|
59
|
-
path,
|
|
60
|
-
body: safeBody
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!response.ok) {
|
|
65
|
-
throw new ApiError(`API request failed: ${response.status} ${response.statusText} for ${path}. Body: ${safeBody}`, {
|
|
66
|
-
status: response.status,
|
|
67
|
-
path,
|
|
68
|
-
body: safeBody
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return data;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function isIdempotentMethod(method) {
|
|
76
|
-
return ['GET', 'HEAD', 'OPTIONS'].includes(String(method || 'GET').toUpperCase());
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function getNormalizationDiagnostics(value) {
|
|
80
|
-
return value?.[NORMALIZATION_DIAGNOSTICS_KEY] || emptyNormalizationDiagnostics();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function getProjects(token, userId) {
|
|
84
|
-
const qs = new URLSearchParams({
|
|
85
|
-
f_user_id: String(userId),
|
|
86
|
-
'f_project.stage_id': '!6'
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const data = await apiFetch(token, `${CONFIG.projectMembersPath}?${qs}`);
|
|
90
|
-
return unwrapRecords(data).map(normalizeProject).filter((project) => project.projectMemberId);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function getDayLogs(token, projectId, userId, localDateISO) {
|
|
94
|
-
const qs = new URLSearchParams({
|
|
95
|
-
f_project_id: String(projectId),
|
|
96
|
-
f_user_id: String(userId),
|
|
97
|
-
f_logdate: toApiLogDate(localDateISO)
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const data = await apiFetch(token, `${CONFIG.memberLogtimePath}?${qs}`);
|
|
102
|
-
const logs = unwrapRecords(data);
|
|
103
|
-
const entries = normalizeLogtimeEntries(logs, {
|
|
104
|
-
date: localDateISO,
|
|
105
|
-
projectId
|
|
106
|
-
});
|
|
107
|
-
const normalization = getNormalizationDiagnostics(entries);
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
projectId,
|
|
111
|
-
totalHours: entries.reduce((sum, log) => sum + Number(log.hours || 0), 0),
|
|
112
|
-
entries,
|
|
113
|
-
logs,
|
|
114
|
-
normalization,
|
|
115
|
-
ok: true
|
|
116
|
-
};
|
|
117
|
-
} catch (error) {
|
|
118
|
-
if (error instanceof ApiError && error.status === 401) {
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
projectId,
|
|
124
|
-
totalHours: null,
|
|
125
|
-
logs: [],
|
|
126
|
-
ok: false,
|
|
127
|
-
error: error.message
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function normalizeLogtimeEntries(logs, defaults = {}) {
|
|
133
|
-
const rows = unwrapRecords(logs);
|
|
134
|
-
const diagnostics = createNormalizationDiagnostics('logtime_entries');
|
|
135
|
-
diagnostics.rowsRead = rows.length;
|
|
136
|
-
const entries = [];
|
|
137
|
-
|
|
138
|
-
rows.forEach((log, index) => {
|
|
139
|
-
if (!log || typeof log !== 'object') {
|
|
140
|
-
addDroppedRow(diagnostics, 'invalid_log_entry', { index });
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const context = { index, kind: 'logtime_entry' };
|
|
145
|
-
const date = normalizeDateOnly(log.logdate ?? log.log_date ?? log.date) || defaults.date;
|
|
146
|
-
const projectMemberId = readExplicitProjectMemberId(log) ?? defaults.projectMemberId;
|
|
147
|
-
const projectId = readProjectId(log) ?? defaults.projectId;
|
|
148
|
-
const hours = readNumber(log, ['logtimes', 'hours', 'time'], diagnostics, context);
|
|
149
|
-
|
|
150
|
-
if (!date) {
|
|
151
|
-
addDroppedRow(diagnostics, 'missing_date', { ...context, ...diagnosticProjectFields({ projectMemberId, projectId }) });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (projectMemberId === undefined && projectId === undefined) {
|
|
156
|
-
addDroppedRow(diagnostics, 'missing_project_identity', { ...context, date });
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (hours <= 0) {
|
|
161
|
-
addDroppedRow(diagnostics, 'non_positive_hours', { ...context, date, ...diagnosticProjectFields({ projectMemberId, projectId }) });
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
diagnostics.rowsAccepted += 1;
|
|
166
|
-
entries.push({
|
|
167
|
-
id: log.id ?? log.logtime_id ?? log.logtimeId,
|
|
168
|
-
date,
|
|
169
|
-
projectMemberId,
|
|
170
|
-
projectId,
|
|
171
|
-
projectName: hasProjectName(log) ? readProjectName(log, readProjectId(log), diagnostics, context) : defaults.projectName,
|
|
172
|
-
hours,
|
|
173
|
-
taskName: log.task_name ?? log.taskName ?? log.name ?? log.title ?? ''
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
return attachNormalizationDiagnostics(entries, diagnostics);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export async function getAllDayLogSummaries(token, projects, userId, localDateISO) {
|
|
181
|
-
return mapLimit(projects, CONFIG.dayLogConcurrency, (project) => (
|
|
182
|
-
getDayLogs(token, project.projectId, userId, localDateISO)
|
|
183
|
-
));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function getTimesheetRange(token, userId, { from, to }) {
|
|
187
|
-
const { records } = await getTimesheetRangeResult(token, userId, { from, to });
|
|
188
|
-
return records;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export async function getTimesheetRangeResult(token, userId, { from, to }) {
|
|
192
|
-
const qs = new URLSearchParams({
|
|
193
|
-
f_user_id: String(userId),
|
|
194
|
-
f_from: toApiLogDate(from),
|
|
195
|
-
f_to: toApiLogDate(to),
|
|
196
|
-
f_timesheet_by_week: '0',
|
|
197
|
-
f_time_off: '1'
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const data = await apiFetch(token, `${CONFIG.timesheetPath}?${qs}`);
|
|
201
|
-
return normalizeTimesheetRangeResult(data, { from, to });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function normalizeTimesheetRange(data, { from, to } = {}) {
|
|
205
|
-
return normalizeTimesheetRangeResult(data, { from, to }).records;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function normalizeTimesheetRangeResult(data, { from, to } = {}) {
|
|
209
|
-
const diagnostics = createNormalizationDiagnostics('unknown');
|
|
210
|
-
const explicitRows = expandProjectTimesheetRows(data, { from, to }, diagnostics);
|
|
211
|
-
const rows = explicitRows.length ? explicitRows : findTimesheetRowsInRange(data, { from, to });
|
|
212
|
-
diagnostics.sourceShape = explicitRows.length ? 'project_timesheet' : 'range_scan';
|
|
213
|
-
if (!rows.length && hasObjectPayload(data)) {
|
|
214
|
-
addWarning(diagnostics, 'unknown_timesheet_shape');
|
|
215
|
-
}
|
|
216
|
-
const records = normalizeTimesheetRecords(rows, diagnostics);
|
|
217
|
-
return {
|
|
218
|
-
records,
|
|
219
|
-
normalization: getNormalizationDiagnostics(records)
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export async function getBookedProjectsForDate(token, userId, localDateISO) {
|
|
224
|
-
const { records } = await getTimesheetRangeResult(token, userId, {
|
|
225
|
-
from: localDateISO,
|
|
226
|
-
to: addLocalDays(localDateISO, 1)
|
|
227
|
-
});
|
|
228
|
-
return normalizeTimesheetProjects(records, localDateISO);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export async function getTodayTimesheetProjects(token, userId, localDateISO) {
|
|
232
|
-
return getBookedProjectsForDate(token, userId, localDateISO);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export async function getLogtimeById(token, logtimeId, options = {}) {
|
|
236
|
-
const path = `${CONFIG.memberLogtimePath}/${encodeURIComponent(assertLogtimeId(logtimeId, 'logtimeId'))}`;
|
|
237
|
-
const data = await apiFetch(token, path, options);
|
|
238
|
-
const record = extractLogtimeRecord(data);
|
|
239
|
-
if (!record) {
|
|
240
|
-
throw new ApiError(`Resource Optimiser did not return logwork ${logtimeId}.`, {
|
|
241
|
-
path
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return normalizeEditableLogtime(record);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export async function getWorklogTasks(token, { projectId, search } = {}, options = {}) {
|
|
248
|
-
const path = `${CONFIG.logworkTasksPath}${buildLogworkTasksQuery({ projectId, search })}`;
|
|
249
|
-
const data = await apiFetch(token, path, options);
|
|
250
|
-
return unwrapRecords(data).map(normalizeWorklogTask);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export async function getDefaultWorklogTasks(token, { projectId, search } = {}, options = {}) {
|
|
254
|
-
const path = `${CONFIG.logworkDefaultTasksPath}${buildLogworkTasksQuery({ projectId, search })}`;
|
|
255
|
-
const data = await apiFetch(token, path, options);
|
|
256
|
-
return unwrapRecords(data).map(normalizeWorklogTask);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export async function fetchWorklogTasksForProject(token, projectId, options = {}) {
|
|
260
|
-
const cacheKey = Number(projectId);
|
|
261
|
-
const { cache, ...apiOptions } = options;
|
|
262
|
-
if (cache instanceof Map && cache.has(cacheKey)) {
|
|
263
|
-
return cache.get(cacheKey);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const [projectTasks, defaultTasks] = await Promise.all([
|
|
267
|
-
getWorklogTasks(token, { projectId: cacheKey }, apiOptions),
|
|
268
|
-
getDefaultWorklogTasks(token, { projectId: cacheKey }, apiOptions)
|
|
269
|
-
]);
|
|
270
|
-
const merged = mergeWorklogTaskLists(projectTasks, defaultTasks);
|
|
271
|
-
if (cache instanceof Map) {
|
|
272
|
-
cache.set(cacheKey, merged);
|
|
273
|
-
}
|
|
274
|
-
return merged;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export async function createLogworkEntry(token, {
|
|
278
|
-
worklogTaskId,
|
|
279
|
-
projectId,
|
|
280
|
-
effortHours,
|
|
281
|
-
localDateISO,
|
|
282
|
-
typeOfWork = CONFIG.defaultTypeOfWork,
|
|
283
|
-
description,
|
|
284
|
-
needsProjectId = false
|
|
285
|
-
}, options = {}) {
|
|
286
|
-
const normalizedTaskId = assertLogtimeId(worklogTaskId, 'worklogTaskId');
|
|
287
|
-
const normalizedProjectId = assertPositiveProjectId(projectId);
|
|
288
|
-
const normalizedHours = Number(effortHours);
|
|
289
|
-
const normalizedTypeOfWork = String(typeOfWork || CONFIG.defaultTypeOfWork).trim();
|
|
290
|
-
if (!Number.isFinite(normalizedHours) || normalizedHours <= 0) {
|
|
291
|
-
throw new Error('effortHours must be a positive number.');
|
|
292
|
-
}
|
|
293
|
-
if (!LOGWORK_TYPE_OF_WORK.has(normalizedTypeOfWork)) {
|
|
294
|
-
throw new Error(`typeOfWork must be one of: ${[...LOGWORK_TYPE_OF_WORK].join(', ')}.`);
|
|
295
|
-
}
|
|
296
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(localDateISO || ''))) {
|
|
297
|
-
throw new Error('localDateISO must use YYYY-MM-DD format.');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const path = CONFIG.logworkEntriesPath;
|
|
301
|
-
const body = {
|
|
302
|
-
worklog_task_id: preserveNumericId(normalizedTaskId),
|
|
303
|
-
effort_hours: normalizedHours,
|
|
304
|
-
logdate: localDateISO,
|
|
305
|
-
type_of_work: normalizedTypeOfWork
|
|
306
|
-
};
|
|
307
|
-
if (description !== undefined && description !== null && String(description).trim()) {
|
|
308
|
-
body.description = String(description).trim();
|
|
309
|
-
}
|
|
310
|
-
if (needsProjectId) {
|
|
311
|
-
body.project_id = preserveNumericId(normalizedProjectId);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (process.env.LOGWORK_DRY_RUN === '1') {
|
|
315
|
-
return {
|
|
316
|
-
dryRun: true,
|
|
317
|
-
method: 'POST',
|
|
318
|
-
path,
|
|
319
|
-
body
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
try {
|
|
324
|
-
const response = await apiFetch(token, path, {
|
|
325
|
-
...options,
|
|
326
|
-
method: 'POST',
|
|
327
|
-
body: JSON.stringify(body)
|
|
328
|
-
});
|
|
329
|
-
const record = extractLogtimeRecord(response);
|
|
330
|
-
return {
|
|
331
|
-
dryRun: false,
|
|
332
|
-
entry: record ? normalizeEditableLogtime(record) : null,
|
|
333
|
-
response
|
|
334
|
-
};
|
|
335
|
-
} catch (error) {
|
|
336
|
-
throw mapLogworkEntryApiError(error);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export async function getLogworkDay(token, { date, projectId } = {}, options = {}) {
|
|
341
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ''))) {
|
|
342
|
-
throw new Error('date must use YYYY-MM-DD format.');
|
|
343
|
-
}
|
|
344
|
-
const qs = new URLSearchParams({ date });
|
|
345
|
-
if (projectId !== undefined && projectId !== null && projectId !== '') {
|
|
346
|
-
qs.set('project_id', String(projectId));
|
|
347
|
-
}
|
|
348
|
-
const data = await apiFetch(token, `${CONFIG.logworkDayPath}?${qs}`, options);
|
|
349
|
-
return normalizeLogworkDayPayload(unwrapLogworkObject(data));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export async function getLogworkWeekly(token, { weekStart } = {}, options = {}) {
|
|
353
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(weekStart || ''))) {
|
|
354
|
-
throw new Error('weekStart must use YYYY-MM-DD format.');
|
|
355
|
-
}
|
|
356
|
-
const qs = new URLSearchParams({ week_start: weekStart });
|
|
357
|
-
const data = await apiFetch(token, `${CONFIG.logworkWeeklyPath}?${qs}`, options);
|
|
358
|
-
return normalizeLogworkWeeklyPayload(unwrapLogworkObject(data));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
export async function getLogworkRejected(token, options = {}) {
|
|
362
|
-
const data = await apiFetch(token, CONFIG.logworkRejectedPath, options);
|
|
363
|
-
return unwrapRecords(data).map(normalizeLogworkF03Entry);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export async function resubmitLogworkEntry(token, entryId, {
|
|
367
|
-
effortHours,
|
|
368
|
-
typeOfWork = CONFIG.defaultTypeOfWork,
|
|
369
|
-
noteToPm,
|
|
370
|
-
description
|
|
371
|
-
}, options = {}) {
|
|
372
|
-
const id = assertLogtimeId(entryId, 'entryId');
|
|
373
|
-
const normalizedHours = Number(effortHours);
|
|
374
|
-
const normalizedTypeOfWork = String(typeOfWork || CONFIG.defaultTypeOfWork).trim();
|
|
375
|
-
if (!Number.isFinite(normalizedHours) || normalizedHours <= 0) {
|
|
376
|
-
throw new Error('effortHours must be a positive number.');
|
|
377
|
-
}
|
|
378
|
-
if (!LOGWORK_TYPE_OF_WORK.has(normalizedTypeOfWork)) {
|
|
379
|
-
throw new Error(`typeOfWork must be one of: ${[...LOGWORK_TYPE_OF_WORK].join(', ')}.`);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const path = `${CONFIG.logworkEntriesPath}/${encodeURIComponent(String(id))}/resubmit`;
|
|
383
|
-
const body = {
|
|
384
|
-
effort_hours: normalizedHours,
|
|
385
|
-
type_of_work: normalizedTypeOfWork
|
|
386
|
-
};
|
|
387
|
-
if (noteToPm !== undefined && String(noteToPm).trim()) {
|
|
388
|
-
body.note_to_pm = String(noteToPm).trim();
|
|
389
|
-
}
|
|
390
|
-
if (description !== undefined && String(description).trim()) {
|
|
391
|
-
body.description = String(description).trim();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (process.env.LOGWORK_DRY_RUN === '1') {
|
|
395
|
-
return { dryRun: true, method: 'PATCH', path, body };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
try {
|
|
399
|
-
const response = await apiFetch(token, path, {
|
|
400
|
-
...options,
|
|
401
|
-
method: 'PATCH',
|
|
402
|
-
body: JSON.stringify(body)
|
|
403
|
-
});
|
|
404
|
-
const record = extractLogtimeRecord(response);
|
|
405
|
-
return {
|
|
406
|
-
dryRun: false,
|
|
407
|
-
entry: record ? normalizeLogworkF03Entry(record) : null,
|
|
408
|
-
response
|
|
409
|
-
};
|
|
410
|
-
} catch (error) {
|
|
411
|
-
throw mapLogworkEntryApiError(error);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
export async function deleteLogworkEntry(token, entryId, options = {}) {
|
|
416
|
-
const id = assertLogtimeId(entryId, 'entryId');
|
|
417
|
-
const path = `${CONFIG.logworkEntriesPath}/${encodeURIComponent(String(id))}`;
|
|
418
|
-
|
|
419
|
-
if (process.env.LOGWORK_DRY_RUN === '1') {
|
|
420
|
-
return { dryRun: true, method: 'DELETE', path };
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
try {
|
|
424
|
-
const response = await apiFetch(token, path, {
|
|
425
|
-
...options,
|
|
426
|
-
method: 'DELETE'
|
|
427
|
-
});
|
|
428
|
-
return { dryRun: false, response };
|
|
429
|
-
} catch (error) {
|
|
430
|
-
throw mapLogworkEntryApiError(error);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export function normalizeLogworkF03Entry(record = {}) {
|
|
435
|
-
const base = normalizeEditableLogtime(record);
|
|
436
|
-
return {
|
|
437
|
-
...base,
|
|
438
|
-
typeOfWork: record.type_of_work ?? record.typeOfWork ?? '',
|
|
439
|
-
worklogTaskId: record.worklog_task_id ?? record.worklogTaskId ?? null,
|
|
440
|
-
description: record.description ?? '',
|
|
441
|
-
rejectionComment: record.rejection_comment ?? record.rejectionComment ?? '',
|
|
442
|
-
noteToPm: record.note_to_pm ?? record.noteToPm ?? ''
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function normalizeLogworkDayPayload(payload = {}) {
|
|
447
|
-
const entries = Array.isArray(payload.entries)
|
|
448
|
-
? payload.entries.map(normalizeLogworkF03Entry)
|
|
449
|
-
: [];
|
|
450
|
-
return {
|
|
451
|
-
date: normalizeDateOnly(payload.date) || '',
|
|
452
|
-
entries,
|
|
453
|
-
isLocked: Boolean(payload.isLocked ?? payload.is_locked),
|
|
454
|
-
isProjectClosed: Boolean(payload.isProjectClosed ?? payload.is_project_closed),
|
|
455
|
-
leaveRequests: Array.isArray(payload.leaveRequests) ? payload.leaveRequests : []
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function normalizeLogworkWeeklyPayload(payload = {}) {
|
|
460
|
-
return {
|
|
461
|
-
weekStart: normalizeDateOnly(payload.weekStart ?? payload.week_start) || '',
|
|
462
|
-
isLocked: Boolean(payload.isLocked ?? payload.is_locked),
|
|
463
|
-
projects: Array.isArray(payload.projects) ? payload.projects : [],
|
|
464
|
-
missingDays: Array.isArray(payload.missingDays) ? payload.missingDays : [],
|
|
465
|
-
weeklyTotals: payload.weeklyTotals ?? payload.weekly_totals ?? null
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function unwrapLogworkObject(data) {
|
|
470
|
-
if (data?.data && typeof data.data === 'object' && !Array.isArray(data.data)) {
|
|
471
|
-
return data.data;
|
|
472
|
-
}
|
|
473
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
474
|
-
return data;
|
|
475
|
-
}
|
|
476
|
-
return {};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export async function addLogtime(token, entry, options = {}) {
|
|
480
|
-
const {
|
|
481
|
-
projectMemberId: _projectMemberId,
|
|
482
|
-
projectId,
|
|
483
|
-
logtimes,
|
|
484
|
-
taskName,
|
|
485
|
-
localDateISO,
|
|
486
|
-
typeOfWork,
|
|
487
|
-
description,
|
|
488
|
-
worklogTaskId,
|
|
489
|
-
worklogNeedsProjectId,
|
|
490
|
-
taskCache
|
|
491
|
-
} = entry || {};
|
|
492
|
-
|
|
493
|
-
const normalizedHours = Number(logtimes);
|
|
494
|
-
const normalizedTaskName = String(taskName ?? '').trim();
|
|
495
|
-
if (!Number.isFinite(normalizedHours) || normalizedHours <= 0) {
|
|
496
|
-
throw new Error('logtimes must be a positive number.');
|
|
497
|
-
}
|
|
498
|
-
if (!normalizedTaskName) {
|
|
499
|
-
throw new Error('taskName must not be empty.');
|
|
500
|
-
}
|
|
501
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(localDateISO || ''))) {
|
|
502
|
-
throw new Error('localDateISO must use YYYY-MM-DD format.');
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const normalizedProjectId = assertPositiveProjectId(projectId);
|
|
506
|
-
let resolvedTaskId = worklogTaskId;
|
|
507
|
-
let needsProjectId = worklogNeedsProjectId;
|
|
508
|
-
|
|
509
|
-
if (resolvedTaskId === undefined || resolvedTaskId === null) {
|
|
510
|
-
const tasks = await fetchWorklogTasksForProject(token, normalizedProjectId, {
|
|
511
|
-
cache: taskCache,
|
|
512
|
-
...options
|
|
513
|
-
});
|
|
514
|
-
const resolution = resolveWorklogTask({
|
|
515
|
-
taskName: normalizedTaskName,
|
|
516
|
-
projectId: normalizedProjectId,
|
|
517
|
-
tasks
|
|
518
|
-
});
|
|
519
|
-
resolvedTaskId = resolution.worklogTaskId;
|
|
520
|
-
needsProjectId = resolution.needsProjectId;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return createLogworkEntry(token, {
|
|
524
|
-
worklogTaskId: resolvedTaskId,
|
|
525
|
-
projectId: normalizedProjectId,
|
|
526
|
-
effortHours: normalizedHours,
|
|
527
|
-
localDateISO,
|
|
528
|
-
typeOfWork,
|
|
529
|
-
description,
|
|
530
|
-
needsProjectId: Boolean(needsProjectId)
|
|
531
|
-
}, options);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function buildLogworkTasksQuery({ projectId, search }) {
|
|
535
|
-
const qs = new URLSearchParams();
|
|
536
|
-
if (projectId !== undefined && projectId !== null && projectId !== '') {
|
|
537
|
-
qs.set('project_id', String(projectId));
|
|
538
|
-
}
|
|
539
|
-
if (search) {
|
|
540
|
-
qs.set('search', String(search));
|
|
541
|
-
}
|
|
542
|
-
const query = qs.toString();
|
|
543
|
-
return query ? `?${query}` : '';
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function assertPositiveProjectId(projectId, label = 'projectId') {
|
|
547
|
-
const normalized = Number(projectId);
|
|
548
|
-
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
549
|
-
throw new Error(`${label} must be a positive number.`);
|
|
550
|
-
}
|
|
551
|
-
return normalized;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function mapLogworkEntryApiError(error) {
|
|
555
|
-
if (!(error instanceof ApiError)) {
|
|
556
|
-
return error;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const bodyText = String(error.body || '');
|
|
560
|
-
const knownMessages = [
|
|
561
|
-
['TASK_OR_PROJECT_NOT_FOUND', 'Worklog task or project was not found. Check project_id and worklog_task_id.'],
|
|
562
|
-
['HOLIDAY_BLOCKED', 'Logwork is blocked on this date (holiday).'],
|
|
563
|
-
['NOT_MEMBER_OF_PROJECT', 'You are not a member of this project.'],
|
|
564
|
-
['INVALID_EFFORT', 'Effort hours must be greater than 0.'],
|
|
565
|
-
['EFFORT_EXCEEDS_24', 'Effort cannot exceed 24 hours per entry.'],
|
|
566
|
-
['ENTRY_NOT_FOUND', 'Logwork entry was not found or is already deleted.'],
|
|
567
|
-
['FORBIDDEN', 'You can only delete or change logwork entries you created.']
|
|
568
|
-
];
|
|
569
|
-
|
|
570
|
-
for (const [code, message] of knownMessages) {
|
|
571
|
-
if (bodyText.includes(code)) {
|
|
572
|
-
return new ApiError(message, {
|
|
573
|
-
status: error.status,
|
|
574
|
-
path: error.path,
|
|
575
|
-
body: error.body
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
if (bodyText.includes('Can only delete unprocessed entries')) {
|
|
581
|
-
return new ApiError('Only submitted or approved entries can be deleted. Rejected entries must be resubmitted or edited.', {
|
|
582
|
-
status: error.status,
|
|
583
|
-
path: error.path,
|
|
584
|
-
body: error.body
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (bodyText.includes('LOCKED') || bodyText.includes('locked')) {
|
|
589
|
-
return new ApiError('This log date is locked and cannot be edited.', {
|
|
590
|
-
status: error.status,
|
|
591
|
-
path: error.path,
|
|
592
|
-
body: error.body
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
return error;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
export function normalizeEditableLogtime(record = {}) {
|
|
600
|
-
const hours = Number(record.logtimes ?? record.hours ?? record.effort_hours ?? record.time);
|
|
601
|
-
return {
|
|
602
|
-
id: record.id ?? record.logtime_id ?? record.logtimeId,
|
|
603
|
-
projectMemberId: record.project_member_id ?? record.projectMemberId,
|
|
604
|
-
projectId: record.project_id ?? record.projectId ?? record.project?.id,
|
|
605
|
-
projectName: record.project_name ?? record.projectName ?? record.project?.name ?? '',
|
|
606
|
-
date: normalizeDateOnly(record.logdate ?? record.log_date ?? record.date),
|
|
607
|
-
hours: Number.isFinite(hours) ? hours : 0,
|
|
608
|
-
taskName: record.task_name ?? record.taskName ?? record.name ?? record.title ?? '',
|
|
609
|
-
createdBy: record.created_by ?? record.createdBy,
|
|
610
|
-
updatedBy: record.updated_by ?? record.updatedBy,
|
|
611
|
-
updatedDate: record.updated_date ?? record.updatedDate ?? '',
|
|
612
|
-
jiraLogworkId: record.jira_logwork_id ?? record.jiraLogworkId ?? null,
|
|
613
|
-
referrerUrl: record.referrer_url ?? record.referrerUrl ?? null,
|
|
614
|
-
source: record.source ?? '',
|
|
615
|
-
status: record.status ?? '',
|
|
616
|
-
isLocked: Boolean(record.is_locked ?? record.isLocked),
|
|
617
|
-
raw: record
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function normalizeProject(rec) {
|
|
622
|
-
const projectId = rec.project_id ?? rec.project?.id ?? rec.project?.project_id;
|
|
623
|
-
const projectMemberId = rec.id ?? rec.project_member_id;
|
|
624
|
-
|
|
625
|
-
return {
|
|
626
|
-
projectMemberId,
|
|
627
|
-
projectId,
|
|
628
|
-
projectName: rec.project?.name ?? rec.project_name ?? rec.name ?? projectFallbackName(projectId ?? projectMemberId),
|
|
629
|
-
workloadPercent: rec.workload ?? rec.workload_percent ?? rec.assigned_percent ?? 0,
|
|
630
|
-
raw: rec
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function normalizeTimesheetProjects(rows, localDateISO) {
|
|
635
|
-
const byMember = new Map();
|
|
636
|
-
|
|
637
|
-
for (const row of rows) {
|
|
638
|
-
const projectMemberId = readProjectMemberId(row);
|
|
639
|
-
const projectId = readProjectId(row);
|
|
640
|
-
|
|
641
|
-
if (!projectMemberId && !projectId) {
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const key = String(projectMemberId ?? projectId);
|
|
646
|
-
const projectName = readProjectName(row, projectId ?? projectMemberId);
|
|
647
|
-
const assignedHours = Number(row.bookedHours ?? readAssignedHours(row));
|
|
648
|
-
const loggedHours = Number(row.loggedHours ?? readLoggedHours(row));
|
|
649
|
-
const existing = byMember.get(key);
|
|
650
|
-
|
|
651
|
-
if (existing) {
|
|
652
|
-
existing.assignedHours += assignedHours;
|
|
653
|
-
existing.loggedHours += loggedHours;
|
|
654
|
-
existing.workloadPercent = hoursToAssignPercent(existing.assignedHours);
|
|
655
|
-
existing.raw.push(row);
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
byMember.set(key, {
|
|
660
|
-
projectMemberId: projectMemberId ?? row.project_member_id ?? row.id,
|
|
661
|
-
projectId,
|
|
662
|
-
projectName,
|
|
663
|
-
assignedHours,
|
|
664
|
-
loggedHours,
|
|
665
|
-
workloadPercent: hoursToAssignPercent(assignedHours),
|
|
666
|
-
localDateISO,
|
|
667
|
-
raw: [row]
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
return [...byMember.values()].filter((project) => project.assignedHours > 0);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function normalizeTimesheetRecords(rows, diagnostics = createNormalizationDiagnostics('range_scan')) {
|
|
675
|
-
const byDayProject = new Map();
|
|
676
|
-
diagnostics.rowsRead = rows.length;
|
|
677
|
-
|
|
678
|
-
rows.forEach((row, index) => {
|
|
679
|
-
const date = readDateLike(row);
|
|
680
|
-
const projectMemberId = readProjectMemberId(row);
|
|
681
|
-
const projectId = readProjectId(row);
|
|
682
|
-
const context = {
|
|
683
|
-
index,
|
|
684
|
-
kind: 'timesheet_row',
|
|
685
|
-
...diagnosticProjectFields({ projectMemberId, projectId, projectName: hasProjectName(row) ? readProjectName(row, projectId ?? projectMemberId) : undefined })
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
if (!date) {
|
|
689
|
-
addDroppedRow(diagnostics, 'missing_date', context);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (projectMemberId === undefined && projectId === undefined) {
|
|
694
|
-
addDroppedRow(diagnostics, 'missing_project_identity', { ...context, date });
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
const key = `${date}:${projectMemberId ?? projectId}`;
|
|
699
|
-
const existing = byDayProject.get(key);
|
|
700
|
-
const bookedHours = readTimesheetHours(row, ['bookedHours'], readAssignedHours, diagnostics, { ...context, date, metric: 'bookedHours' });
|
|
701
|
-
const loggedHours = readTimesheetHours(row, ['loggedHours'], readLoggedHours, diagnostics, { ...context, date, metric: 'loggedHours' });
|
|
702
|
-
const entries = readLogEntries(row, diagnostics, { ...context, date });
|
|
703
|
-
diagnostics.rowsAccepted += 1;
|
|
704
|
-
|
|
705
|
-
if (existing) {
|
|
706
|
-
existing.bookedHours += bookedHours;
|
|
707
|
-
existing.loggedHours += loggedHours;
|
|
708
|
-
existing.entries.push(...entries);
|
|
709
|
-
existing.raw.push(row);
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
byDayProject.set(key, {
|
|
714
|
-
date,
|
|
715
|
-
projectMemberId,
|
|
716
|
-
projectId,
|
|
717
|
-
projectName: readProjectName(row, projectId ?? projectMemberId, diagnostics, { ...context, date }),
|
|
718
|
-
bookedHours,
|
|
719
|
-
loggedHours,
|
|
720
|
-
entries,
|
|
721
|
-
raw: [row]
|
|
722
|
-
});
|
|
723
|
-
});
|
|
724
|
-
|
|
725
|
-
return attachNormalizationDiagnostics([...byDayProject.values()].map((record) => ({
|
|
726
|
-
...record,
|
|
727
|
-
bookedHours: Number(record.bookedHours.toFixed(2)),
|
|
728
|
-
loggedHours: Number(record.loggedHours.toFixed(2))
|
|
729
|
-
})), diagnostics);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function findTimesheetRowsForDate(data, localDateISO) {
|
|
733
|
-
const rows = [];
|
|
734
|
-
collectTimesheetRows(data, localDateISO, rows, new Set());
|
|
735
|
-
return rows;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function expandProjectTimesheetRows(data, range, diagnostics = null) {
|
|
739
|
-
const assignments = [];
|
|
740
|
-
collectProjectAssignments(data, assignments, new Set());
|
|
741
|
-
const rows = [];
|
|
742
|
-
|
|
743
|
-
assignments.forEach((assignment, assignmentIndex) => {
|
|
744
|
-
assignment.timesheet.forEach((day, dayIndex) => {
|
|
745
|
-
const date = normalizeDateOnly(day.logdate ?? day.log_date ?? day.date);
|
|
746
|
-
if (!isDateInRange(date, range)) {
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const context = {
|
|
751
|
-
index: rows.length,
|
|
752
|
-
assignmentIndex,
|
|
753
|
-
dayIndex,
|
|
754
|
-
kind: 'project_timesheet_row',
|
|
755
|
-
date,
|
|
756
|
-
...diagnosticProjectFields({
|
|
757
|
-
projectMemberId: assignment.id,
|
|
758
|
-
projectId: assignment.project?.id,
|
|
759
|
-
projectName: assignment.project?.name
|
|
760
|
-
})
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
rows.push({
|
|
764
|
-
date,
|
|
765
|
-
projectMemberId: assignment.id,
|
|
766
|
-
project_member_id: assignment.id,
|
|
767
|
-
projectId: assignment.project?.id,
|
|
768
|
-
project_id: assignment.project?.id,
|
|
769
|
-
projectName: assignment.project?.name,
|
|
770
|
-
project_name: assignment.project?.name,
|
|
771
|
-
bookedHours: readAssignedHours(day, diagnostics, { ...context, metric: 'bookedHours' }),
|
|
772
|
-
loggedHours: readNumber(day, ['logtimes'], diagnostics, { ...context, metric: 'loggedHours' }),
|
|
773
|
-
overtime: readNumber(day, ['overtime'], diagnostics, { ...context, metric: 'overtime' }),
|
|
774
|
-
source: 'timesheet'
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
return rows;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function collectProjectAssignments(value, assignments, seen) {
|
|
783
|
-
if (!value || typeof value !== 'object') {
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (seen.has(value)) {
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
seen.add(value);
|
|
791
|
-
|
|
792
|
-
if (Array.isArray(value)) {
|
|
793
|
-
for (const item of value) {
|
|
794
|
-
collectProjectAssignments(item, assignments, seen);
|
|
795
|
-
}
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
if (
|
|
800
|
-
value.project &&
|
|
801
|
-
typeof value.project === 'object' &&
|
|
802
|
-
Array.isArray(value.timesheet)
|
|
803
|
-
) {
|
|
804
|
-
assignments.push(value);
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
for (const [key, child] of Object.entries(value)) {
|
|
809
|
-
if (key === 'overall') {
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
collectProjectAssignments(child, assignments, seen);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function findTimesheetRowsInRange(data, { from, to } = {}) {
|
|
817
|
-
const rows = [];
|
|
818
|
-
collectTimesheetRowsInRange(data, { from, to }, rows, new Set());
|
|
819
|
-
return rows;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function collectTimesheetRowsInRange(value, range, rows, seen) {
|
|
823
|
-
if (!value || typeof value !== 'object') {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
if (seen.has(value)) {
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
seen.add(value);
|
|
831
|
-
|
|
832
|
-
if (Array.isArray(value)) {
|
|
833
|
-
for (const item of value) {
|
|
834
|
-
collectTimesheetRowsInRange(item, range, rows, seen);
|
|
835
|
-
}
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const directDate = readDateLike(value);
|
|
840
|
-
if (isDateInRange(directDate, range) && looksLikeTimesheetRow(value)) {
|
|
841
|
-
rows.push(value);
|
|
842
|
-
} else if (!directDate && looksLikeTimesheetRow(value)) {
|
|
843
|
-
rows.push(value);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
for (const [key, child] of Object.entries(value)) {
|
|
847
|
-
if (Array.isArray(child)) {
|
|
848
|
-
const keyDate = normalizeDateOnly(key);
|
|
849
|
-
if (isDateInRange(keyDate, range)) {
|
|
850
|
-
for (const item of child) {
|
|
851
|
-
if (item && typeof item === 'object') {
|
|
852
|
-
rows.push({ ...item, date: readDateLike(item) || keyDate });
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
for (const item of child) {
|
|
859
|
-
if (item && typeof item === 'object') {
|
|
860
|
-
const itemDate = readDateLike(item);
|
|
861
|
-
if (isDateInRange(itemDate, range) && looksLikeTimesheetRow(item)) {
|
|
862
|
-
rows.push(item);
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
collectTimesheetRowsInRange(item, range, rows, seen);
|
|
868
|
-
}
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
if (child && typeof child === 'object') {
|
|
873
|
-
const keyDate = normalizeDateOnly(key);
|
|
874
|
-
if (isDateInRange(keyDate, range)) {
|
|
875
|
-
rows.push({ ...child, date: readDateLike(child) || keyDate });
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
collectTimesheetRowsInRange(child, range, rows, seen);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function collectTimesheetRows(value, localDateISO, rows, seen) {
|
|
885
|
-
if (!value || typeof value !== 'object') {
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
if (seen.has(value)) {
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
seen.add(value);
|
|
893
|
-
|
|
894
|
-
if (Array.isArray(value)) {
|
|
895
|
-
for (const item of value) {
|
|
896
|
-
collectTimesheetRows(item, localDateISO, rows, seen);
|
|
897
|
-
}
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const directDate = readDateLike(value);
|
|
902
|
-
if (directDate === localDateISO && looksLikeTimesheetRow(value)) {
|
|
903
|
-
rows.push(value);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
for (const [key, child] of Object.entries(value)) {
|
|
907
|
-
if (Array.isArray(child)) {
|
|
908
|
-
if (isDateKey(key, localDateISO)) {
|
|
909
|
-
for (const item of child) {
|
|
910
|
-
if (item && typeof item === 'object') {
|
|
911
|
-
rows.push({ ...value, ...item });
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
for (const item of child) {
|
|
918
|
-
if (item && typeof item === 'object' && readDateLike(item) === localDateISO && looksLikeTimesheetRow({ ...value, ...item })) {
|
|
919
|
-
rows.push({ ...value, ...item });
|
|
920
|
-
continue;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
collectTimesheetRows(item, localDateISO, rows, seen);
|
|
924
|
-
}
|
|
925
|
-
continue;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if (child && typeof child === 'object') {
|
|
929
|
-
if (isDateKey(key, localDateISO)) {
|
|
930
|
-
rows.push({ ...value, ...child });
|
|
931
|
-
continue;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
collectTimesheetRows(child, localDateISO, rows, seen);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function looksLikeTimesheetRow(row) {
|
|
940
|
-
return readAssignedHours(row) > 0 || readLoggedHours(row) > 0 || Boolean(pickFirst(row, [
|
|
941
|
-
'project_member_id',
|
|
942
|
-
'projectMemberId',
|
|
943
|
-
'project_id',
|
|
944
|
-
'projectId'
|
|
945
|
-
]));
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
function readProjectMemberId(row) {
|
|
949
|
-
const explicit = readExplicitProjectMemberId(row);
|
|
950
|
-
|
|
951
|
-
if (explicit !== undefined) {
|
|
952
|
-
return explicit;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (looksLikeProjectMemberRow(row)) {
|
|
956
|
-
return row.id;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
return undefined;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
function readExplicitProjectMemberId(row) {
|
|
963
|
-
return pickFirst(row, [
|
|
964
|
-
'project_member_id',
|
|
965
|
-
'projectMemberId',
|
|
966
|
-
'project_member.id',
|
|
967
|
-
'projectMember.id'
|
|
968
|
-
]);
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function readProjectId(row) {
|
|
972
|
-
return pickFirst(row, [
|
|
973
|
-
'project_id',
|
|
974
|
-
'projectId',
|
|
975
|
-
'project.id',
|
|
976
|
-
'project.project_id',
|
|
977
|
-
'projectMember.project_id',
|
|
978
|
-
'project_member.project_id'
|
|
979
|
-
]);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
function readProjectName(row, fallbackId, diagnostics = null, context = {}) {
|
|
983
|
-
const projectName = pickFirst(row, [
|
|
984
|
-
'project.name',
|
|
985
|
-
'project_name',
|
|
986
|
-
'projectName',
|
|
987
|
-
'name',
|
|
988
|
-
'projectMember.project.name',
|
|
989
|
-
'project_member.project.name'
|
|
990
|
-
]);
|
|
991
|
-
|
|
992
|
-
if (projectName !== undefined) {
|
|
993
|
-
return projectName;
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
if (fallbackId !== undefined && fallbackId !== null && String(fallbackId).trim() !== '') {
|
|
997
|
-
addWarning(diagnostics, 'fallback_project_name', {
|
|
998
|
-
...context,
|
|
999
|
-
...diagnosticProjectFields({ projectMemberId: readProjectMemberId(row), projectId: readProjectId(row) })
|
|
1000
|
-
});
|
|
1001
|
-
return projectFallbackName(fallbackId);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
addWarning(diagnostics, 'fallback_project_name', context);
|
|
1005
|
-
return 'Unknown project';
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function projectFallbackName(fallbackId) {
|
|
1009
|
-
return fallbackId !== undefined && fallbackId !== null && String(fallbackId).trim() !== ''
|
|
1010
|
-
? `Project ${fallbackId}`
|
|
1011
|
-
: 'Unknown project';
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function hasProjectName(row) {
|
|
1015
|
-
return pickFirst(row, [
|
|
1016
|
-
'project.name',
|
|
1017
|
-
'project_name',
|
|
1018
|
-
'projectName',
|
|
1019
|
-
'name',
|
|
1020
|
-
'projectMember.project.name',
|
|
1021
|
-
'project_member.project.name'
|
|
1022
|
-
]) !== undefined;
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function looksLikeProjectMemberRow(row) {
|
|
1026
|
-
return row && typeof row === 'object' && row.id !== undefined && (
|
|
1027
|
-
row.project_id !== undefined ||
|
|
1028
|
-
row.projectId !== undefined ||
|
|
1029
|
-
row.project !== undefined ||
|
|
1030
|
-
row.project_name !== undefined ||
|
|
1031
|
-
row.projectName !== undefined ||
|
|
1032
|
-
row.assign_hours !== undefined ||
|
|
1033
|
-
row.assigned_hours !== undefined ||
|
|
1034
|
-
row.booked_hours !== undefined
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function readAssignedHours(row, diagnostics = null, context = {}) {
|
|
1039
|
-
return readNumber(row, [
|
|
1040
|
-
'assign_hours',
|
|
1041
|
-
'assigned_hours',
|
|
1042
|
-
'assignedHours',
|
|
1043
|
-
'booking_hours',
|
|
1044
|
-
'booked_hours',
|
|
1045
|
-
'bookedHours',
|
|
1046
|
-
'workload_hours',
|
|
1047
|
-
'workloadHours',
|
|
1048
|
-
'assign_percent',
|
|
1049
|
-
'assignPercent',
|
|
1050
|
-
'hours_per_day',
|
|
1051
|
-
'hoursPerDay',
|
|
1052
|
-
'timesheet',
|
|
1053
|
-
'logtime',
|
|
1054
|
-
'logtimes_plan',
|
|
1055
|
-
'planned_hours',
|
|
1056
|
-
'plannedHours',
|
|
1057
|
-
'effort',
|
|
1058
|
-
'man_day'
|
|
1059
|
-
], diagnostics, context);
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
function readLoggedHours(row, diagnostics = null, context = {}) {
|
|
1063
|
-
return readNumber(row, [
|
|
1064
|
-
'logged_hours',
|
|
1065
|
-
'loggedHours',
|
|
1066
|
-
'actual_hours',
|
|
1067
|
-
'actualHours',
|
|
1068
|
-
'logtimes',
|
|
1069
|
-
'log_times',
|
|
1070
|
-
'logTimes',
|
|
1071
|
-
'logged',
|
|
1072
|
-
'total_logtimes',
|
|
1073
|
-
'totalLoggedHours',
|
|
1074
|
-
'total_logged_hours',
|
|
1075
|
-
'spent_hours',
|
|
1076
|
-
'spentHours'
|
|
1077
|
-
], diagnostics, context);
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function readLogEntries(row, diagnostics = null, context = {}) {
|
|
1081
|
-
const rawEntries = pickFirst(row, [
|
|
1082
|
-
'logs',
|
|
1083
|
-
'logtimes_data',
|
|
1084
|
-
'logtimesData',
|
|
1085
|
-
'log_times',
|
|
1086
|
-
'logTimes',
|
|
1087
|
-
'items',
|
|
1088
|
-
'children'
|
|
1089
|
-
]);
|
|
1090
|
-
|
|
1091
|
-
if (!Array.isArray(rawEntries)) {
|
|
1092
|
-
return [];
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
const entries = [];
|
|
1096
|
-
rawEntries.forEach((entry, entryIndex) => {
|
|
1097
|
-
if (!entry || typeof entry !== 'object') {
|
|
1098
|
-
addDroppedRow(diagnostics, 'invalid_log_entry', {
|
|
1099
|
-
...context,
|
|
1100
|
-
entryIndex
|
|
1101
|
-
});
|
|
1102
|
-
return;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
const entryContext = {
|
|
1106
|
-
...context,
|
|
1107
|
-
entryIndex,
|
|
1108
|
-
kind: 'embedded_log_entry'
|
|
1109
|
-
};
|
|
1110
|
-
const date = normalizeDateOnly(entry.logdate ?? entry.log_date ?? entry.date ?? row.logdate ?? row.date);
|
|
1111
|
-
const projectMemberId = readProjectMemberId(entry) ?? readProjectMemberId(row);
|
|
1112
|
-
const projectId = readProjectId(entry) ?? readProjectId(row);
|
|
1113
|
-
const hours = readNumber(entry, ['logtimes', 'hours', 'time'], diagnostics, { ...entryContext, metric: 'hours' });
|
|
1114
|
-
|
|
1115
|
-
if (!date) {
|
|
1116
|
-
addDroppedRow(diagnostics, 'missing_date', { ...entryContext, ...diagnosticProjectFields({ projectMemberId, projectId }) });
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
if (projectMemberId === undefined && projectId === undefined) {
|
|
1121
|
-
addDroppedRow(diagnostics, 'missing_project_identity', { ...entryContext, date });
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
if (hours <= 0) {
|
|
1126
|
-
addDroppedRow(diagnostics, 'non_positive_hours', { ...entryContext, date, ...diagnosticProjectFields({ projectMemberId, projectId }) });
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
entries.push({
|
|
1131
|
-
id: entry.id ?? entry.logtime_id ?? entry.logtimeId,
|
|
1132
|
-
date,
|
|
1133
|
-
projectMemberId,
|
|
1134
|
-
projectId,
|
|
1135
|
-
projectName: hasProjectName(entry) ? readProjectName(entry, readProjectId(entry), diagnostics, entryContext) : readProjectName(row, readProjectId(row) ?? readProjectMemberId(row), diagnostics, entryContext),
|
|
1136
|
-
hours,
|
|
1137
|
-
taskName: entry.task_name ?? entry.taskName ?? entry.name ?? entry.title ?? '',
|
|
1138
|
-
raw: entry
|
|
1139
|
-
});
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
return entries;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
function readTimesheetHours(row, directPaths, fallbackReader, diagnostics, context) {
|
|
1146
|
-
const directValue = pickFirst(row, directPaths);
|
|
1147
|
-
if (directValue !== undefined) {
|
|
1148
|
-
return coerceHours(directValue, diagnostics, {
|
|
1149
|
-
...context,
|
|
1150
|
-
field: directPaths[0]
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
return fallbackReader(row, diagnostics, context);
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function readNumber(row, paths, diagnostics = null, context = {}) {
|
|
1158
|
-
for (const path of paths) {
|
|
1159
|
-
const value = pickFirst(row, [path]);
|
|
1160
|
-
if (value !== undefined) {
|
|
1161
|
-
const number = Number(value);
|
|
1162
|
-
if (Number.isFinite(number) && number > 0) {
|
|
1163
|
-
return number;
|
|
1164
|
-
}
|
|
1165
|
-
if (Number.isFinite(number) && number === 0) {
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
addWarning(diagnostics, 'invalid_hours', {
|
|
1169
|
-
...context,
|
|
1170
|
-
field: path
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
return 0;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function coerceHours(value, diagnostics = null, context = {}) {
|
|
1179
|
-
const number = Number(value);
|
|
1180
|
-
if (Number.isFinite(number) && number >= 0) {
|
|
1181
|
-
return number;
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
addWarning(diagnostics, 'invalid_hours', context);
|
|
1185
|
-
return 0;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function hoursToAssignPercent(hours) {
|
|
1189
|
-
return Math.round((Number(hours || 0) / CONFIG.standardDayHours) * 100);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function readDateLike(row) {
|
|
1193
|
-
const value = pickFirst(row, [
|
|
1194
|
-
'logdate',
|
|
1195
|
-
'log_date',
|
|
1196
|
-
'date',
|
|
1197
|
-
'day',
|
|
1198
|
-
'workdate',
|
|
1199
|
-
'work_date',
|
|
1200
|
-
'timesheet_date'
|
|
1201
|
-
]);
|
|
1202
|
-
|
|
1203
|
-
return normalizeDateOnly(value);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function normalizeDateOnly(value) {
|
|
1207
|
-
if (!value) {
|
|
1208
|
-
return '';
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
const text = String(value);
|
|
1212
|
-
const match = text.match(/\d{4}-\d{2}-\d{2}/);
|
|
1213
|
-
return match ? match[0] : '';
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function isDateKey(key, localDateISO) {
|
|
1217
|
-
return normalizeDateOnly(key) === localDateISO;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
function isDateInRange(localDateISO, { from, to } = {}) {
|
|
1221
|
-
if (!localDateISO) {
|
|
1222
|
-
return false;
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
if (from && localDateISO < from) {
|
|
1226
|
-
return false;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
if (to && localDateISO >= to) {
|
|
1230
|
-
return false;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
return true;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
function pickFirst(row, paths) {
|
|
1237
|
-
for (const path of paths) {
|
|
1238
|
-
const value = getPath(row, path);
|
|
1239
|
-
if (value !== undefined && value !== null && value !== '') {
|
|
1240
|
-
return value;
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
return undefined;
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
function getPath(row, path) {
|
|
1248
|
-
return path.split('.').reduce((current, part) => {
|
|
1249
|
-
if (current === undefined || current === null) {
|
|
1250
|
-
return undefined;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
return current[part];
|
|
1254
|
-
}, row);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function unwrapRecords(data) {
|
|
1258
|
-
if (Array.isArray(data)) {
|
|
1259
|
-
return data;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
if (Array.isArray(data?.data)) {
|
|
1263
|
-
return data.data;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
if (Array.isArray(data?.data?.data)) {
|
|
1267
|
-
return data.data.data;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
if (Array.isArray(data?.records)) {
|
|
1271
|
-
return data.records;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
if (Array.isArray(data?.items)) {
|
|
1275
|
-
return data.items;
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
return [];
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
function extractLogtimeRecord(data, collectionKey) {
|
|
1282
|
-
const candidates = collectionKey
|
|
1283
|
-
? [data?.data?.[collectionKey], data?.[collectionKey]]
|
|
1284
|
-
: [data?.data?.data, data?.data, data?.record, data?.item, data];
|
|
1285
|
-
|
|
1286
|
-
for (const candidate of candidates) {
|
|
1287
|
-
if (Array.isArray(candidate)) {
|
|
1288
|
-
const record = candidate.find((item) => item && typeof item === 'object');
|
|
1289
|
-
if (record) {
|
|
1290
|
-
return record;
|
|
1291
|
-
}
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
if (candidate && typeof candidate === 'object' && (
|
|
1295
|
-
candidate.id !== undefined ||
|
|
1296
|
-
candidate.logtime_id !== undefined ||
|
|
1297
|
-
candidate.logtimeId !== undefined
|
|
1298
|
-
)) {
|
|
1299
|
-
return candidate;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
return null;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function assertLogtimeId(value, name) {
|
|
1307
|
-
const normalized = String(value ?? '').trim();
|
|
1308
|
-
if (!normalized || !/^[A-Za-z0-9_-]+$/.test(normalized)) {
|
|
1309
|
-
throw new Error(`${name} must be a valid Resource Optimiser identifier.`);
|
|
1310
|
-
}
|
|
1311
|
-
return normalized;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
function preserveNumericId(value) {
|
|
1315
|
-
return /^\d+$/.test(value) && Number.isSafeInteger(Number(value))
|
|
1316
|
-
? Number(value)
|
|
1317
|
-
: value;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function createNormalizationDiagnostics(sourceShape = 'unknown') {
|
|
1321
|
-
return {
|
|
1322
|
-
status: 'ok',
|
|
1323
|
-
sourceShape,
|
|
1324
|
-
rowsRead: 0,
|
|
1325
|
-
rowsAccepted: 0,
|
|
1326
|
-
droppedRowCount: 0,
|
|
1327
|
-
warningCount: 0,
|
|
1328
|
-
warnings: [],
|
|
1329
|
-
droppedRows: []
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
function emptyNormalizationDiagnostics() {
|
|
1334
|
-
return createNormalizationDiagnostics('unknown');
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
function attachNormalizationDiagnostics(value, diagnostics) {
|
|
1338
|
-
Object.defineProperty(value, NORMALIZATION_DIAGNOSTICS_KEY, {
|
|
1339
|
-
value: finalizeNormalizationDiagnostics(diagnostics),
|
|
1340
|
-
enumerable: false,
|
|
1341
|
-
configurable: true
|
|
1342
|
-
});
|
|
1343
|
-
return value;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
function finalizeNormalizationDiagnostics(diagnostics) {
|
|
1347
|
-
const warningCount = diagnostics.warnings.length;
|
|
1348
|
-
const droppedRowCount = diagnostics.droppedRows.length;
|
|
1349
|
-
return {
|
|
1350
|
-
...diagnostics,
|
|
1351
|
-
status: warningCount || droppedRowCount ? 'warning' : 'ok',
|
|
1352
|
-
warningCount,
|
|
1353
|
-
droppedRowCount
|
|
1354
|
-
};
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
export function combineNormalizationDiagnostics(...items) {
|
|
1358
|
-
const diagnostics = createNormalizationDiagnostics('combined');
|
|
1359
|
-
for (const item of items) {
|
|
1360
|
-
const current = item?.warnings || item?.droppedRows
|
|
1361
|
-
? item
|
|
1362
|
-
: getNormalizationDiagnostics(item);
|
|
1363
|
-
if (!current) {
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
diagnostics.rowsRead += Number(current.rowsRead || 0);
|
|
1367
|
-
diagnostics.rowsAccepted += Number(current.rowsAccepted || 0);
|
|
1368
|
-
for (const warning of current.warnings || []) {
|
|
1369
|
-
pushDiagnostic(diagnostics.warnings, warning);
|
|
1370
|
-
}
|
|
1371
|
-
for (const dropped of current.droppedRows || []) {
|
|
1372
|
-
pushDiagnostic(diagnostics.droppedRows, dropped);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
return finalizeNormalizationDiagnostics(diagnostics);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function addWarning(diagnostics, reason, details = {}) {
|
|
1379
|
-
if (!diagnostics) {
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
pushDiagnostic(diagnostics.warnings, compactDiagnostic({
|
|
1383
|
-
reason,
|
|
1384
|
-
...details
|
|
1385
|
-
}));
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
function addDroppedRow(diagnostics, reason, details = {}) {
|
|
1389
|
-
if (!diagnostics) {
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
pushDiagnostic(diagnostics.droppedRows, compactDiagnostic({
|
|
1393
|
-
reason,
|
|
1394
|
-
...details
|
|
1395
|
-
}));
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
function pushDiagnostic(items, item) {
|
|
1399
|
-
if (items.length < MAX_DIAGNOSTIC_ITEMS) {
|
|
1400
|
-
items.push(item);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
function compactDiagnostic(details) {
|
|
1405
|
-
return Object.fromEntries(Object.entries(details).filter(([, value]) => (
|
|
1406
|
-
value !== undefined &&
|
|
1407
|
-
value !== null &&
|
|
1408
|
-
value !== ''
|
|
1409
|
-
)));
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
function diagnosticProjectFields({ projectMemberId, projectId, projectName } = {}) {
|
|
1413
|
-
return compactDiagnostic({
|
|
1414
|
-
projectMemberId,
|
|
1415
|
-
projectId,
|
|
1416
|
-
projectName
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
function hasObjectPayload(data) {
|
|
1421
|
-
return Boolean(data && typeof data === 'object' && Object.keys(data).length > 0);
|
|
1422
|
-
}
|
|
1
|
+
// Resource Optimiser API facade. The implementation lives in the ro-api-* modules;
|
|
2
|
+
// this module keeps every public name importable from its original path.
|
|
3
|
+
export { ApiError, apiFetch } from './ro-api-http.mjs';
|
|
4
|
+
export {
|
|
5
|
+
combineNormalizationDiagnostics,
|
|
6
|
+
getNormalizationDiagnostics
|
|
7
|
+
} from './ro-api-diagnostics.mjs';
|
|
8
|
+
export {
|
|
9
|
+
getAllDayLogSummaries,
|
|
10
|
+
getDayLogs,
|
|
11
|
+
getProjects,
|
|
12
|
+
normalizeLogtimeEntries
|
|
13
|
+
} from './ro-api-projects.mjs';
|
|
14
|
+
export {
|
|
15
|
+
getBookedProjectsForDate,
|
|
16
|
+
getTimesheetRange,
|
|
17
|
+
getTimesheetRangeResult,
|
|
18
|
+
getTodayTimesheetProjects,
|
|
19
|
+
normalizeTimesheetRange,
|
|
20
|
+
normalizeTimesheetRangeResult
|
|
21
|
+
} from './ro-api-timesheet.mjs';
|
|
22
|
+
export {
|
|
23
|
+
fetchWorklogTasksForProject,
|
|
24
|
+
getDefaultWorklogTasks,
|
|
25
|
+
getWorklogTasks
|
|
26
|
+
} from './ro-api-tasks.mjs';
|
|
27
|
+
export {
|
|
28
|
+
addLogtime,
|
|
29
|
+
createLogworkEntry,
|
|
30
|
+
deleteLogworkEntry,
|
|
31
|
+
getLogtimeById,
|
|
32
|
+
getLogworkDay,
|
|
33
|
+
getLogworkRejected,
|
|
34
|
+
getLogworkWeekly,
|
|
35
|
+
normalizeEditableLogtime,
|
|
36
|
+
normalizeLogworkF03Entry,
|
|
37
|
+
resubmitLogworkEntry
|
|
38
|
+
} from './ro-api-entries.mjs';
|