@markwharton/liquidplanner 1.0.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/dist/client.d.ts +52 -1
- package/dist/client.js +112 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +23 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @see https://api-docs.liquidplanner.com/
|
|
8
8
|
*/
|
|
9
|
-
import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId } from './types.js';
|
|
9
|
+
import type { LPConfig, LPWorkspace, LPMember, LPItem, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPUpsertOptions, LPAssignmentWithContext } from './types.js';
|
|
10
10
|
/**
|
|
11
11
|
* LiquidPlanner API Client
|
|
12
12
|
*
|
|
@@ -83,6 +83,22 @@ export declare class LPClient {
|
|
|
83
83
|
assignments?: LPItem[];
|
|
84
84
|
error?: string;
|
|
85
85
|
}>;
|
|
86
|
+
/**
|
|
87
|
+
* Get assignments for a member with parent task names resolved
|
|
88
|
+
*
|
|
89
|
+
* This is a convenience method that fetches assignments and enriches
|
|
90
|
+
* them with parent task names in a single call (batched internally).
|
|
91
|
+
*
|
|
92
|
+
* @param memberId - The member ID to get assignments for
|
|
93
|
+
* @param options - Options for including additional context
|
|
94
|
+
* @param options.includeProject - If true, also fetch grandparent project names
|
|
95
|
+
*/
|
|
96
|
+
getMyAssignmentsWithContext(memberId: number, options?: {
|
|
97
|
+
includeProject?: boolean;
|
|
98
|
+
}): Promise<{
|
|
99
|
+
assignments?: LPAssignmentWithContext[];
|
|
100
|
+
error?: string;
|
|
101
|
+
}>;
|
|
86
102
|
/**
|
|
87
103
|
* Get all cost codes in the workspace (with pagination)
|
|
88
104
|
*/
|
|
@@ -132,4 +148,39 @@ export declare class LPClient {
|
|
|
132
148
|
* @param updates - Fields to update (merged with existing)
|
|
133
149
|
*/
|
|
134
150
|
updateTimesheetEntry(entryId: number, existingEntry: LPTimesheetEntryWithId, updates: Partial<LPTimesheetEntry>): Promise<LPSyncResult>;
|
|
151
|
+
/**
|
|
152
|
+
* Create or update a timesheet entry (upsert)
|
|
153
|
+
*
|
|
154
|
+
* Checks for an existing entry first. If found, updates it (accumulating
|
|
155
|
+
* hours by default). If not found, creates a new entry.
|
|
156
|
+
*
|
|
157
|
+
* This follows the proven "fetch first" pattern from the CLI.
|
|
158
|
+
*
|
|
159
|
+
* By default, hours are accumulated (added to existing). Set `accumulate: false`
|
|
160
|
+
* to replace existing hours instead.
|
|
161
|
+
*
|
|
162
|
+
* **Note behavior:** Notes are appended to existing notes by the LP API.
|
|
163
|
+
* Pass an empty string to avoid adding notes on update.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* // Accumulate hours (default)
|
|
168
|
+
* await client.upsertTimesheetEntry({
|
|
169
|
+
* date: '2026-01-29',
|
|
170
|
+
* itemId: 12345,
|
|
171
|
+
* hours: 1.5,
|
|
172
|
+
* note: 'Timer session'
|
|
173
|
+
* });
|
|
174
|
+
*
|
|
175
|
+
* // Replace hours instead of accumulating
|
|
176
|
+
* await client.upsertTimesheetEntry(
|
|
177
|
+
* { date: '2026-01-29', itemId: 12345, hours: 4.0 },
|
|
178
|
+
* { accumulate: false }
|
|
179
|
+
* );
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @param entry - The timesheet entry to create or update
|
|
183
|
+
* @param options - Upsert options (accumulate defaults to true)
|
|
184
|
+
*/
|
|
185
|
+
upsertTimesheetEntry(entry: LPTimesheetEntry, options?: LPUpsertOptions): Promise<LPSyncResult>;
|
|
135
186
|
}
|
package/dist/client.js
CHANGED
|
@@ -183,6 +183,55 @@ export class LPClient {
|
|
|
183
183
|
});
|
|
184
184
|
return error ? { error } : { assignments: results };
|
|
185
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Get assignments for a member with parent task names resolved
|
|
188
|
+
*
|
|
189
|
+
* This is a convenience method that fetches assignments and enriches
|
|
190
|
+
* them with parent task names in a single call (batched internally).
|
|
191
|
+
*
|
|
192
|
+
* @param memberId - The member ID to get assignments for
|
|
193
|
+
* @param options - Options for including additional context
|
|
194
|
+
* @param options.includeProject - If true, also fetch grandparent project names
|
|
195
|
+
*/
|
|
196
|
+
async getMyAssignmentsWithContext(memberId, options) {
|
|
197
|
+
// 1. Get raw assignments
|
|
198
|
+
const { assignments, error } = await this.getMyAssignments(memberId);
|
|
199
|
+
if (error || !assignments)
|
|
200
|
+
return { error };
|
|
201
|
+
if (assignments.length === 0)
|
|
202
|
+
return { assignments: [] };
|
|
203
|
+
// 2. Extract unique parent IDs (tasks)
|
|
204
|
+
const taskIds = [...new Set(assignments.map(a => a.parentId).filter((id) => id !== undefined))];
|
|
205
|
+
// 3. Batch fetch all parent tasks
|
|
206
|
+
const taskResults = await Promise.all(taskIds.map(id => this.getItem(id)));
|
|
207
|
+
const taskMap = new Map();
|
|
208
|
+
for (const result of taskResults) {
|
|
209
|
+
if (result.item)
|
|
210
|
+
taskMap.set(result.item.id, result.item);
|
|
211
|
+
}
|
|
212
|
+
// 4. Optionally fetch grandparent projects
|
|
213
|
+
let projectMap = new Map();
|
|
214
|
+
if (options?.includeProject) {
|
|
215
|
+
const projectIds = [...new Set([...taskMap.values()].map(t => t.parentId).filter((id) => id !== undefined))];
|
|
216
|
+
const projectResults = await Promise.all(projectIds.map(id => this.getItem(id)));
|
|
217
|
+
for (const result of projectResults) {
|
|
218
|
+
if (result.item)
|
|
219
|
+
projectMap.set(result.item.id, result.item);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// 5. Merge context into assignments
|
|
223
|
+
return {
|
|
224
|
+
assignments: assignments.map(a => {
|
|
225
|
+
const task = a.parentId ? taskMap.get(a.parentId) : undefined;
|
|
226
|
+
const project = task?.parentId ? projectMap.get(task.parentId) : undefined;
|
|
227
|
+
return {
|
|
228
|
+
...a,
|
|
229
|
+
taskName: task?.name ?? '-',
|
|
230
|
+
projectName: project?.name,
|
|
231
|
+
};
|
|
232
|
+
}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
186
235
|
// ============================================================================
|
|
187
236
|
// Cost Codes
|
|
188
237
|
// ============================================================================
|
|
@@ -321,4 +370,67 @@ export class LPClient {
|
|
|
321
370
|
return { success: false, error: getErrorMessage(error) };
|
|
322
371
|
}
|
|
323
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Create or update a timesheet entry (upsert)
|
|
375
|
+
*
|
|
376
|
+
* Checks for an existing entry first. If found, updates it (accumulating
|
|
377
|
+
* hours by default). If not found, creates a new entry.
|
|
378
|
+
*
|
|
379
|
+
* This follows the proven "fetch first" pattern from the CLI.
|
|
380
|
+
*
|
|
381
|
+
* By default, hours are accumulated (added to existing). Set `accumulate: false`
|
|
382
|
+
* to replace existing hours instead.
|
|
383
|
+
*
|
|
384
|
+
* **Note behavior:** Notes are appended to existing notes by the LP API.
|
|
385
|
+
* Pass an empty string to avoid adding notes on update.
|
|
386
|
+
*
|
|
387
|
+
* @example
|
|
388
|
+
* ```typescript
|
|
389
|
+
* // Accumulate hours (default)
|
|
390
|
+
* await client.upsertTimesheetEntry({
|
|
391
|
+
* date: '2026-01-29',
|
|
392
|
+
* itemId: 12345,
|
|
393
|
+
* hours: 1.5,
|
|
394
|
+
* note: 'Timer session'
|
|
395
|
+
* });
|
|
396
|
+
*
|
|
397
|
+
* // Replace hours instead of accumulating
|
|
398
|
+
* await client.upsertTimesheetEntry(
|
|
399
|
+
* { date: '2026-01-29', itemId: 12345, hours: 4.0 },
|
|
400
|
+
* { accumulate: false }
|
|
401
|
+
* );
|
|
402
|
+
* ```
|
|
403
|
+
*
|
|
404
|
+
* @param entry - The timesheet entry to create or update
|
|
405
|
+
* @param options - Upsert options (accumulate defaults to true)
|
|
406
|
+
*/
|
|
407
|
+
async upsertTimesheetEntry(entry, options = {}) {
|
|
408
|
+
const { accumulate = true } = options;
|
|
409
|
+
// Fetch existing entries for this date/item first
|
|
410
|
+
const { entries, error: fetchError } = await this.getTimesheetEntries(entry.date, entry.itemId);
|
|
411
|
+
if (fetchError) {
|
|
412
|
+
return { success: false, error: fetchError };
|
|
413
|
+
}
|
|
414
|
+
// Find matching entry
|
|
415
|
+
// If no costCodeId specified, match any entry (LP uses assignment's default)
|
|
416
|
+
// If costCodeId specified, match exactly
|
|
417
|
+
const existingEntry = entries?.find((e) => {
|
|
418
|
+
if (entry.costCodeId === undefined || entry.costCodeId === null) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
return e.costCodeId === entry.costCodeId;
|
|
422
|
+
});
|
|
423
|
+
if (existingEntry) {
|
|
424
|
+
// Update existing entry
|
|
425
|
+
const newHours = accumulate
|
|
426
|
+
? existingEntry.hours + entry.hours
|
|
427
|
+
: entry.hours;
|
|
428
|
+
return this.updateTimesheetEntry(existingEntry.id, existingEntry, {
|
|
429
|
+
hours: newHours,
|
|
430
|
+
note: entry.note,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// No existing entry, create new
|
|
434
|
+
return this.createTimesheetEntry(entry);
|
|
435
|
+
}
|
|
324
436
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
export { LPClient } from './client.js';
|
|
30
30
|
export { resolveTaskToAssignment } from './workflows.js';
|
|
31
|
-
export type { LPConfig, LPItemType, LPItem, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPResult, } from './types.js';
|
|
31
|
+
export type { LPConfig, LPItemType, LPItem, LPWorkspace, LPMember, LPCostCode, LPSyncResult, LPTimesheetEntry, LPTimesheetEntryWithId, LPTaskResolution, LPResult, LPUpsertOptions, LPAssignmentWithContext, } from './types.js';
|
|
32
32
|
export { hoursToMinutes, normalizeItemType, buildAuthHeader, filterIs, filterIn, paginatedFetch, } from './utils.js';
|
|
33
33
|
export type { PaginateOptions } from './utils.js';
|
|
34
34
|
export { LP_API_BASE, DEFAULT_ITEM_NAME, DEFAULT_ASSIGNMENT_NAME, } from './constants.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -136,3 +136,26 @@ export interface LPResult<T> {
|
|
|
136
136
|
/** Error message if failed */
|
|
137
137
|
error?: string;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Options for upsert timesheet entry operation
|
|
141
|
+
*/
|
|
142
|
+
export interface LPUpsertOptions {
|
|
143
|
+
/**
|
|
144
|
+
* Whether to accumulate hours with existing entry (default: true)
|
|
145
|
+
* - true: Add new hours to existing hours
|
|
146
|
+
* - false: Replace existing hours with new hours
|
|
147
|
+
*/
|
|
148
|
+
accumulate?: boolean;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Assignment with resolved parent context
|
|
152
|
+
*
|
|
153
|
+
* Extends LPItem with additional fields for the parent task name,
|
|
154
|
+
* grandparent project name, and cost code name.
|
|
155
|
+
*/
|
|
156
|
+
export interface LPAssignmentWithContext extends LPItem {
|
|
157
|
+
/** Parent task name */
|
|
158
|
+
taskName?: string;
|
|
159
|
+
/** Grandparent project name (if requested) */
|
|
160
|
+
projectName?: string;
|
|
161
|
+
}
|