@markwharton/liquidplanner 1.0.0 → 1.1.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 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 } from './types.js';
10
10
  /**
11
11
  * LiquidPlanner API Client
12
12
  *
@@ -132,4 +132,39 @@ export declare class LPClient {
132
132
  * @param updates - Fields to update (merged with existing)
133
133
  */
134
134
  updateTimesheetEntry(entryId: number, existingEntry: LPTimesheetEntryWithId, updates: Partial<LPTimesheetEntry>): Promise<LPSyncResult>;
135
+ /**
136
+ * Create or update a timesheet entry (upsert)
137
+ *
138
+ * Checks for an existing entry first. If found, updates it (accumulating
139
+ * hours by default). If not found, creates a new entry.
140
+ *
141
+ * This follows the proven "fetch first" pattern from the CLI.
142
+ *
143
+ * By default, hours are accumulated (added to existing). Set `accumulate: false`
144
+ * to replace existing hours instead.
145
+ *
146
+ * **Note behavior:** Notes are appended to existing notes by the LP API.
147
+ * Pass an empty string to avoid adding notes on update.
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * // Accumulate hours (default)
152
+ * await client.upsertTimesheetEntry({
153
+ * date: '2026-01-29',
154
+ * itemId: 12345,
155
+ * hours: 1.5,
156
+ * note: 'Timer session'
157
+ * });
158
+ *
159
+ * // Replace hours instead of accumulating
160
+ * await client.upsertTimesheetEntry(
161
+ * { date: '2026-01-29', itemId: 12345, hours: 4.0 },
162
+ * { accumulate: false }
163
+ * );
164
+ * ```
165
+ *
166
+ * @param entry - The timesheet entry to create or update
167
+ * @param options - Upsert options (accumulate defaults to true)
168
+ */
169
+ upsertTimesheetEntry(entry: LPTimesheetEntry, options?: LPUpsertOptions): Promise<LPSyncResult>;
135
170
  }
package/dist/client.js CHANGED
@@ -321,4 +321,67 @@ export class LPClient {
321
321
  return { success: false, error: getErrorMessage(error) };
322
322
  }
323
323
  }
324
+ /**
325
+ * Create or update a timesheet entry (upsert)
326
+ *
327
+ * Checks for an existing entry first. If found, updates it (accumulating
328
+ * hours by default). If not found, creates a new entry.
329
+ *
330
+ * This follows the proven "fetch first" pattern from the CLI.
331
+ *
332
+ * By default, hours are accumulated (added to existing). Set `accumulate: false`
333
+ * to replace existing hours instead.
334
+ *
335
+ * **Note behavior:** Notes are appended to existing notes by the LP API.
336
+ * Pass an empty string to avoid adding notes on update.
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * // Accumulate hours (default)
341
+ * await client.upsertTimesheetEntry({
342
+ * date: '2026-01-29',
343
+ * itemId: 12345,
344
+ * hours: 1.5,
345
+ * note: 'Timer session'
346
+ * });
347
+ *
348
+ * // Replace hours instead of accumulating
349
+ * await client.upsertTimesheetEntry(
350
+ * { date: '2026-01-29', itemId: 12345, hours: 4.0 },
351
+ * { accumulate: false }
352
+ * );
353
+ * ```
354
+ *
355
+ * @param entry - The timesheet entry to create or update
356
+ * @param options - Upsert options (accumulate defaults to true)
357
+ */
358
+ async upsertTimesheetEntry(entry, options = {}) {
359
+ const { accumulate = true } = options;
360
+ // Fetch existing entries for this date/item first
361
+ const { entries, error: fetchError } = await this.getTimesheetEntries(entry.date, entry.itemId);
362
+ if (fetchError) {
363
+ return { success: false, error: fetchError };
364
+ }
365
+ // Find matching entry
366
+ // If no costCodeId specified, match any entry (LP uses assignment's default)
367
+ // If costCodeId specified, match exactly
368
+ const existingEntry = entries?.find((e) => {
369
+ if (entry.costCodeId === undefined || entry.costCodeId === null) {
370
+ return true;
371
+ }
372
+ return e.costCodeId === entry.costCodeId;
373
+ });
374
+ if (existingEntry) {
375
+ // Update existing entry
376
+ const newHours = accumulate
377
+ ? existingEntry.hours + entry.hours
378
+ : entry.hours;
379
+ return this.updateTimesheetEntry(existingEntry.id, existingEntry, {
380
+ hours: newHours,
381
+ note: entry.note,
382
+ });
383
+ }
384
+ // No existing entry, create new
385
+ return this.createTimesheetEntry(entry);
386
+ }
324
387
  }
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, } 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,14 @@ 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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markwharton/liquidplanner",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "LiquidPlanner API client for timesheet integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",