noxctl 0.3.0 → 0.4.1

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.
Files changed (67) hide show
  1. package/README.md +56 -6
  2. package/dist/auth.d.ts +6 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +61 -21
  5. package/dist/auth.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +391 -6
  8. package/dist/cli.js.map +1 -1
  9. package/dist/credentials-store.d.ts +1 -1
  10. package/dist/credentials-store.d.ts.map +1 -1
  11. package/dist/credentials-store.js +12 -11
  12. package/dist/credentials-store.js.map +1 -1
  13. package/dist/fortnox-client.d.ts +6 -1
  14. package/dist/fortnox-client.d.ts.map +1 -1
  15. package/dist/fortnox-client.js +150 -62
  16. package/dist/fortnox-client.js.map +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +11 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/operations/absencetransactions.d.ts +21 -0
  21. package/dist/operations/absencetransactions.d.ts.map +1 -0
  22. package/dist/operations/absencetransactions.js +36 -0
  23. package/dist/operations/absencetransactions.js.map +1 -0
  24. package/dist/operations/attendancetransactions.d.ts +21 -0
  25. package/dist/operations/attendancetransactions.d.ts.map +1 -0
  26. package/dist/operations/attendancetransactions.js +40 -0
  27. package/dist/operations/attendancetransactions.js.map +1 -0
  28. package/dist/operations/employees.d.ts +19 -0
  29. package/dist/operations/employees.d.ts.map +1 -0
  30. package/dist/operations/employees.js +34 -0
  31. package/dist/operations/employees.js.map +1 -0
  32. package/dist/operations/salarytransactions.d.ts +21 -0
  33. package/dist/operations/salarytransactions.d.ts.map +1 -0
  34. package/dist/operations/salarytransactions.js +35 -0
  35. package/dist/operations/salarytransactions.js.map +1 -0
  36. package/dist/operations/scheduletimes.d.ts +4 -0
  37. package/dist/operations/scheduletimes.d.ts.map +1 -0
  38. package/dist/operations/scheduletimes.js +24 -0
  39. package/dist/operations/scheduletimes.js.map +1 -0
  40. package/dist/tools/absencetransactions.d.ts +3 -0
  41. package/dist/tools/absencetransactions.d.ts.map +1 -0
  42. package/dist/tools/absencetransactions.js +66 -0
  43. package/dist/tools/absencetransactions.js.map +1 -0
  44. package/dist/tools/attendancetransactions.d.ts +3 -0
  45. package/dist/tools/attendancetransactions.d.ts.map +1 -0
  46. package/dist/tools/attendancetransactions.js +63 -0
  47. package/dist/tools/attendancetransactions.js.map +1 -0
  48. package/dist/tools/employees.d.ts +3 -0
  49. package/dist/tools/employees.d.ts.map +1 -0
  50. package/dist/tools/employees.js +119 -0
  51. package/dist/tools/employees.js.map +1 -0
  52. package/dist/tools/salarytransactions.d.ts +3 -0
  53. package/dist/tools/salarytransactions.d.ts.map +1 -0
  54. package/dist/tools/salarytransactions.js +70 -0
  55. package/dist/tools/salarytransactions.js.map +1 -0
  56. package/dist/tools/scheduletimes.d.ts +3 -0
  57. package/dist/tools/scheduletimes.d.ts.map +1 -0
  58. package/dist/tools/scheduletimes.js +62 -0
  59. package/dist/tools/scheduletimes.js.map +1 -0
  60. package/dist/tools/status.d.ts.map +1 -1
  61. package/dist/tools/status.js +4 -2
  62. package/dist/tools/status.js.map +1 -1
  63. package/dist/views.d.ts +9 -0
  64. package/dist/views.d.ts.map +1 -1
  65. package/dist/views.js +115 -0
  66. package/dist/views.js.map +1 -1
  67. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -7,12 +7,12 @@ import { readActivePointer, readActivePointerOutcome, writeActivePointer, delete
7
7
  import { setResolvedProfile } from './auth.js';
8
8
  import { applyPeriod } from './date-periods.js';
9
9
  import { DEFAULT_PROFILE, InvalidProfileNameError } from './profile-name.js';
10
- import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, financialYearListColumns, financialYearDetailColumns, lockedPeriodDetailColumns, contractListColumns, contractDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, } from './views.js';
10
+ import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, financialYearListColumns, financialYearDetailColumns, lockedPeriodDetailColumns, contractListColumns, contractDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, employeeListColumns, employeeDetailColumns, salaryTransactionListColumns, salaryTransactionDetailColumns, attendanceTransactionListColumns, attendanceTransactionDetailColumns, absenceTransactionListColumns, absenceTransactionDetailColumns, scheduleTimeDetailColumns, } from './views.js';
11
11
  const program = new Command();
12
12
  program
13
13
  .name('noxctl')
14
14
  .description('CLI and MCP server for Fortnox accounting')
15
- .version('0.3.0')
15
+ .version('0.4.1')
16
16
  .addOption(new Option('-o, --output <format>', 'Output format (default: table on TTY, json when piped)')
17
17
  .choices(['json', 'table'])
18
18
  .default(undefined))
@@ -186,9 +186,13 @@ program
186
186
  .command('init')
187
187
  .description('Interactive setup wizard — recommended onboarding path')
188
188
  .option('--profile <name>', 'Profile to create/re-auth (defaults to resolved profile or "default")')
189
+ .option('--with-salary', 'Also request the Lön (salary/payroll) scope — requires the Lön permission enabled on your Fortnox app')
189
190
  .action(async (initOpts) => {
190
- const { loadCredentials, runOAuthSetup } = await import('./auth.js');
191
+ const { loadCredentials, runOAuthSetup, SCOPES, SALARY_SCOPE } = await import('./auth.js');
191
192
  const { validateProfileName } = await import('./profile-name.js');
193
+ // Opt-in salary scope: flag for TTY, env var for non-interactive/CI runs.
194
+ const withSalary = Boolean(initOpts.withSalary) || process.env.FORTNOX_WITH_SALARY === '1';
195
+ const scopes = withSalary ? `${SCOPES} ${SALARY_SCOPE}` : SCOPES;
192
196
  let targetProfile;
193
197
  try {
194
198
  targetProfile = validateProfileName(initOpts.profile ?? resolvedProfileInfo.name);
@@ -238,6 +242,9 @@ program
238
242
  console.log("You'll need a Fortnox app from developer.fortnox.se with:");
239
243
  console.log(' - Redirect URI: http://localhost:9876/callback');
240
244
  console.log(' - Scopes (Behörigheter): Artikel, Bokföring, Faktura, Företagsinformation, Inställningar, Kund, Leverantör, Leverantörsfaktura');
245
+ if (withSalary) {
246
+ console.log(' - Lön (krävs eftersom du kör med --with-salary)');
247
+ }
241
248
  console.log(' - Service account enabled (recommended)');
242
249
  console.log('');
243
250
  console.log('See the README for detailed portal instructions.');
@@ -339,7 +346,7 @@ program
339
346
  }
340
347
  }
341
348
  // Step 6: Run OAuth flow
342
- await runOAuthSetup({ clientId, clientSecret, serviceAccount }, targetProfile);
349
+ await runOAuthSetup({ clientId, clientSecret, serviceAccount }, targetProfile, scopes);
343
350
  // Step 6b: Set the active pointer if this is the first profile or no pointer exists.
344
351
  try {
345
352
  const idx = await readProfileIndex();
@@ -715,7 +722,7 @@ program
715
722
  return;
716
723
  }
717
724
  // 8. Scope validation — probe each scope with a lightweight GET
718
- const { SCOPES } = await import('./auth.js');
725
+ const { effectiveScopes } = await import('./auth.js');
719
726
  const { fortnoxRequest, FortnoxApiError } = await import('./fortnox-client.js');
720
727
  const scopeEndpoints = {
721
728
  article: 'articles?limit=1',
@@ -729,8 +736,9 @@ program
729
736
  settings: 'settings/company',
730
737
  inbox: 'inbox',
731
738
  connectfile: 'voucherfileconnections?limit=1',
739
+ salary: 'employees?limit=1',
732
740
  };
733
- const required = SCOPES.split(' ');
741
+ const required = effectiveScopes(creds).split(' ');
734
742
  const missing = [];
735
743
  for (const scope of required) {
736
744
  const endpoint = scopeEndpoints[scope];
@@ -2105,6 +2113,383 @@ costcenters
2105
2113
  await deleteCostCenter(code);
2106
2114
  outputConfirmation(`Cost center ${code} deleted.`, json(), { Code: code, deleted: true }, undefined, 'CostCenter');
2107
2115
  });
2116
+ // --- employees (payroll / Lön) ---
2117
+ const employees = program
2118
+ .command('employees')
2119
+ .description('Employee operations (requires the Lön scope — see `noxctl init --with-salary`)');
2120
+ employees
2121
+ .command('list')
2122
+ .description('List employees')
2123
+ .option('--page <number>', 'Page number', parseInt)
2124
+ .option('--limit <number>', 'Results per page', parseInt)
2125
+ .option('-a, --all', 'Fetch all pages')
2126
+ .action(async (opts) => {
2127
+ const { listEmployees } = await import('./operations/employees.js');
2128
+ const data = await listEmployees({
2129
+ page: opts.page,
2130
+ limit: opts.limit,
2131
+ all: opts.all,
2132
+ });
2133
+ const envelope = data;
2134
+ outputList(envelope.Employees ?? [], employeeListColumns, json(), data, envelope.MetaInformation);
2135
+ });
2136
+ employees
2137
+ .command('get <employeeId>')
2138
+ .description('Get a single employee')
2139
+ .action(async (employeeId) => {
2140
+ const { getEmployee } = await import('./operations/employees.js');
2141
+ const data = await getEmployee(employeeId);
2142
+ outputDetail(data, employeeDetailColumns, json(), 'Employee');
2143
+ });
2144
+ employees
2145
+ .command('create')
2146
+ .description('Create an employee. Tip: set --employment-form, --personel-type and --salary-form, otherwise Fortnox cannot assign an employment agreement (företagsavtal).')
2147
+ .requiredOption('--first-name <name>', 'First name')
2148
+ .requiredOption('--last-name <name>', 'Last name')
2149
+ .requiredOption('--email <email>', 'Email address')
2150
+ .option('--employment-form <form>', 'Employment form: TV, PRO, TID, SVT, VIK, PRJ, PRA, FER, SES, NEJ')
2151
+ .option('--personel-type <type>', 'Personnel type: TJM (tjänsteman) or ARB (arbetare)')
2152
+ .option('--salary-form <form>', 'Salary form: MAN (monthly) or TIM (hourly)')
2153
+ .option('--employment-date <date>', 'Employment start date (YYYY-MM-DD)')
2154
+ .option('--monthly-salary <amount>', 'Monthly salary')
2155
+ .option('--hourly-pay <amount>', 'Hourly pay')
2156
+ .option('--input <file>', 'Additional employee data as JSON file (or - for stdin)')
2157
+ .option('-y, --yes', 'Skip confirmation prompt')
2158
+ .option('--dry-run', 'Preview the request without sending it')
2159
+ .action(async (opts) => {
2160
+ const { createEmployee } = await import('./operations/employees.js');
2161
+ let input = {};
2162
+ if (opts.input) {
2163
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
2164
+ input = JSON.parse(raw);
2165
+ }
2166
+ const params = {
2167
+ ...input,
2168
+ FirstName: opts.firstName,
2169
+ LastName: opts.lastName,
2170
+ Email: opts.email,
2171
+ };
2172
+ if (opts.employmentForm !== undefined)
2173
+ params.EmploymentForm = opts.employmentForm;
2174
+ if (opts.personelType !== undefined)
2175
+ params.PersonelType = opts.personelType;
2176
+ if (opts.salaryForm !== undefined)
2177
+ params.SalaryForm = opts.salaryForm;
2178
+ if (opts.employmentDate !== undefined)
2179
+ params.EmploymentDate = opts.employmentDate;
2180
+ if (opts.monthlySalary !== undefined)
2181
+ params.MonthlySalary = opts.monthlySalary;
2182
+ if (opts.hourlyPay !== undefined)
2183
+ params.HourlyPay = opts.hourlyPay;
2184
+ if (!(await confirmMutation(`Create employee "${opts.firstName} ${opts.lastName}"`, opts, {
2185
+ Employee: params,
2186
+ }))) {
2187
+ return;
2188
+ }
2189
+ const data = await createEmployee(params);
2190
+ outputDetail(data, employeeDetailColumns, json(), 'Employee');
2191
+ });
2192
+ employees
2193
+ .command('update <employeeId>')
2194
+ .description('Update an employee')
2195
+ .requiredOption('--input <file>', 'Employee data as JSON file (or - for stdin)')
2196
+ .option('-y, --yes', 'Skip confirmation prompt')
2197
+ .option('--dry-run', 'Preview the request without sending it')
2198
+ .action(async (employeeId, opts) => {
2199
+ const { updateEmployee } = await import('./operations/employees.js');
2200
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
2201
+ const fields = JSON.parse(raw);
2202
+ if (!(await confirmMutation(`Update employee ${employeeId}`, opts, { Employee: fields }))) {
2203
+ return;
2204
+ }
2205
+ const data = await updateEmployee(employeeId, fields);
2206
+ outputDetail(data, employeeDetailColumns, json(), 'Employee');
2207
+ });
2208
+ // --- salary transactions (Lön) ---
2209
+ const salarytransactions = program
2210
+ .command('salary-transactions')
2211
+ .description('Salary transaction operations (requires the Lön scope)');
2212
+ salarytransactions
2213
+ .command('list')
2214
+ .description('List salary transactions')
2215
+ .option('--employee <id>', 'Filter by employee ID')
2216
+ .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
2217
+ .option('--page <number>', 'Page number', parseInt)
2218
+ .option('--limit <number>', 'Results per page', parseInt)
2219
+ .option('-a, --all', 'Fetch all pages')
2220
+ .action(async (opts) => {
2221
+ const { listSalaryTransactions } = await import('./operations/salarytransactions.js');
2222
+ const data = await listSalaryTransactions({
2223
+ employeeId: opts.employee,
2224
+ date: opts.date,
2225
+ page: opts.page,
2226
+ limit: opts.limit,
2227
+ all: opts.all,
2228
+ });
2229
+ const envelope = data;
2230
+ outputList(envelope.SalaryTransactions ?? [], salaryTransactionListColumns, json(), data, envelope.MetaInformation);
2231
+ });
2232
+ salarytransactions
2233
+ .command('get <salaryRow>')
2234
+ .description('Get a single salary transaction')
2235
+ .action(async (salaryRow) => {
2236
+ const { getSalaryTransaction } = await import('./operations/salarytransactions.js');
2237
+ const data = await getSalaryTransaction(salaryRow);
2238
+ outputDetail(data, salaryTransactionDetailColumns, json(), 'SalaryTransaction');
2239
+ });
2240
+ salarytransactions
2241
+ .command('create')
2242
+ .description('Create a salary transaction')
2243
+ .requiredOption('--employee <id>', 'Employee ID')
2244
+ .requiredOption('--salary-code <code>', 'Salary code')
2245
+ .requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
2246
+ .option('--amount <amount>', 'Amount')
2247
+ .option('--input <file>', 'Additional data as JSON file (or - for stdin)')
2248
+ .option('-y, --yes', 'Skip confirmation prompt')
2249
+ .option('--dry-run', 'Preview the request without sending it')
2250
+ .action(async (opts) => {
2251
+ const { createSalaryTransaction } = await import('./operations/salarytransactions.js');
2252
+ let input = {};
2253
+ if (opts.input) {
2254
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
2255
+ input = JSON.parse(raw);
2256
+ }
2257
+ const params = {
2258
+ ...input,
2259
+ EmployeeId: opts.employee,
2260
+ SalaryCode: opts.salaryCode,
2261
+ Date: opts.date,
2262
+ };
2263
+ if (opts.amount !== undefined)
2264
+ params.Amount = opts.amount;
2265
+ if (!(await confirmMutation(`Create salary transaction for employee ${opts.employee}`, opts, {
2266
+ SalaryTransaction: params,
2267
+ }))) {
2268
+ return;
2269
+ }
2270
+ const data = await createSalaryTransaction(params);
2271
+ outputDetail(data, salaryTransactionDetailColumns, json(), 'SalaryTransaction');
2272
+ });
2273
+ salarytransactions
2274
+ .command('delete <salaryRow>')
2275
+ .description('Delete a salary transaction')
2276
+ .option('-y, --yes', 'Skip confirmation prompt')
2277
+ .option('--dry-run', 'Preview the request without sending it')
2278
+ .action(async (salaryRow, opts) => {
2279
+ const { deleteSalaryTransaction } = await import('./operations/salarytransactions.js');
2280
+ if (!(await confirmMutation(`Delete salary transaction ${salaryRow}`, opts))) {
2281
+ return;
2282
+ }
2283
+ await deleteSalaryTransaction(salaryRow);
2284
+ outputConfirmation(`Salary transaction ${salaryRow} deleted.`, json(), { SalaryRow: salaryRow, deleted: true }, undefined, 'SalaryTransaction');
2285
+ });
2286
+ // --- attendance transactions (närvaro / Lön) ---
2287
+ const attendancetransactions = program
2288
+ .command('attendance-transactions')
2289
+ .description('Attendance transaction operations (requires the Lön scope)');
2290
+ attendancetransactions
2291
+ .command('list')
2292
+ .description('List attendance transactions')
2293
+ .option('--employee <id>', 'Filter by employee ID')
2294
+ .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
2295
+ .option('--page <number>', 'Page number', parseInt)
2296
+ .option('--limit <number>', 'Results per page', parseInt)
2297
+ .option('-a, --all', 'Fetch all pages')
2298
+ .action(async (opts) => {
2299
+ const { listAttendanceTransactions } = await import('./operations/attendancetransactions.js');
2300
+ const data = await listAttendanceTransactions({
2301
+ employeeId: opts.employee,
2302
+ date: opts.date,
2303
+ page: opts.page,
2304
+ limit: opts.limit,
2305
+ all: opts.all,
2306
+ });
2307
+ const envelope = data;
2308
+ outputList(envelope.AttendanceTransactions ?? [], attendanceTransactionListColumns, json(), data, envelope.MetaInformation);
2309
+ });
2310
+ attendancetransactions
2311
+ .command('get <id>')
2312
+ .description('Get a single attendance transaction')
2313
+ .action(async (id) => {
2314
+ const { getAttendanceTransaction } = await import('./operations/attendancetransactions.js');
2315
+ const data = await getAttendanceTransaction(id);
2316
+ outputDetail(data, attendanceTransactionDetailColumns, json(), 'AttendanceTransaction');
2317
+ });
2318
+ attendancetransactions
2319
+ .command('create')
2320
+ .description('Create an attendance transaction')
2321
+ .requiredOption('--employee <id>', 'Employee ID')
2322
+ .requiredOption('--cause-code <code>', 'Cause code (e.g. ARB, FLX, OT1)')
2323
+ .requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
2324
+ .option('--hours <hours>', 'Hours')
2325
+ .option('--input <file>', 'Additional data as JSON file (or - for stdin)')
2326
+ .option('-y, --yes', 'Skip confirmation prompt')
2327
+ .option('--dry-run', 'Preview the request without sending it')
2328
+ .action(async (opts) => {
2329
+ const { createAttendanceTransaction } = await import('./operations/attendancetransactions.js');
2330
+ let input = {};
2331
+ if (opts.input) {
2332
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
2333
+ input = JSON.parse(raw);
2334
+ }
2335
+ const params = {
2336
+ ...input,
2337
+ EmployeeId: opts.employee,
2338
+ CauseCode: opts.causeCode,
2339
+ Date: opts.date,
2340
+ };
2341
+ if (opts.hours !== undefined)
2342
+ params.Hours = opts.hours;
2343
+ if (!(await confirmMutation(`Create attendance transaction for employee ${opts.employee}`, opts, {
2344
+ AttendanceTransaction: params,
2345
+ }))) {
2346
+ return;
2347
+ }
2348
+ const data = await createAttendanceTransaction(params);
2349
+ outputDetail(data, attendanceTransactionDetailColumns, json(), 'AttendanceTransaction');
2350
+ });
2351
+ attendancetransactions
2352
+ .command('delete <id>')
2353
+ .description('Delete an attendance transaction')
2354
+ .option('-y, --yes', 'Skip confirmation prompt')
2355
+ .option('--dry-run', 'Preview the request without sending it')
2356
+ .action(async (id, opts) => {
2357
+ const { deleteAttendanceTransaction } = await import('./operations/attendancetransactions.js');
2358
+ if (!(await confirmMutation(`Delete attendance transaction ${id}`, opts))) {
2359
+ return;
2360
+ }
2361
+ await deleteAttendanceTransaction(id);
2362
+ outputConfirmation(`Attendance transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AttendanceTransaction');
2363
+ });
2364
+ // --- absence transactions (frånvaro / Lön) ---
2365
+ const absencetransactions = program
2366
+ .command('absence-transactions')
2367
+ .description('Absence transaction operations (requires the Lön scope)');
2368
+ absencetransactions
2369
+ .command('list')
2370
+ .description('List absence transactions')
2371
+ .option('--employee <id>', 'Filter by employee ID')
2372
+ .option('--date <date>', 'Filter by date (YYYY-MM-DD)')
2373
+ .option('--page <number>', 'Page number', parseInt)
2374
+ .option('--limit <number>', 'Results per page', parseInt)
2375
+ .option('-a, --all', 'Fetch all pages')
2376
+ .action(async (opts) => {
2377
+ const { listAbsenceTransactions } = await import('./operations/absencetransactions.js');
2378
+ const data = await listAbsenceTransactions({
2379
+ employeeId: opts.employee,
2380
+ date: opts.date,
2381
+ page: opts.page,
2382
+ limit: opts.limit,
2383
+ all: opts.all,
2384
+ });
2385
+ const envelope = data;
2386
+ outputList(envelope.AbsenceTransactions ?? [], absenceTransactionListColumns, json(), data, envelope.MetaInformation);
2387
+ });
2388
+ absencetransactions
2389
+ .command('get <id>')
2390
+ .description('Get a single absence transaction')
2391
+ .action(async (id) => {
2392
+ const { getAbsenceTransaction } = await import('./operations/absencetransactions.js');
2393
+ const data = await getAbsenceTransaction(id);
2394
+ outputDetail(data, absenceTransactionDetailColumns, json(), 'AbsenceTransaction');
2395
+ });
2396
+ absencetransactions
2397
+ .command('create')
2398
+ .description('Create an absence transaction')
2399
+ .requiredOption('--employee <id>', 'Employee ID')
2400
+ .requiredOption('--cause-code <code>', 'Cause code (e.g. SEM, SJK, VAB)')
2401
+ .requiredOption('--date <date>', 'Date (YYYY-MM-DD)')
2402
+ .option('--hours <hours>', 'Hours (number, e.g. 8)', parseFloat)
2403
+ .option('--extent <extent>', 'Extent / percentage absent (number, e.g. 50)', parseFloat)
2404
+ .option('--input <file>', 'Additional data as JSON file (or - for stdin)')
2405
+ .option('-y, --yes', 'Skip confirmation prompt')
2406
+ .option('--dry-run', 'Preview the request without sending it')
2407
+ .action(async (opts) => {
2408
+ const { createAbsenceTransaction } = await import('./operations/absencetransactions.js');
2409
+ let input = {};
2410
+ if (opts.input) {
2411
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
2412
+ input = JSON.parse(raw);
2413
+ }
2414
+ const params = {
2415
+ ...input,
2416
+ EmployeeId: opts.employee,
2417
+ CauseCode: opts.causeCode,
2418
+ Date: opts.date,
2419
+ };
2420
+ if (opts.hours !== undefined)
2421
+ params.Hours = opts.hours;
2422
+ if (opts.extent !== undefined)
2423
+ params.Extent = opts.extent;
2424
+ if (!(await confirmMutation(`Create absence transaction for employee ${opts.employee}`, opts, {
2425
+ AbsenceTransaction: params,
2426
+ }))) {
2427
+ return;
2428
+ }
2429
+ const data = await createAbsenceTransaction(params);
2430
+ outputDetail(data, absenceTransactionDetailColumns, json(), 'AbsenceTransaction');
2431
+ });
2432
+ absencetransactions
2433
+ .command('delete <id>')
2434
+ .description('Delete an absence transaction')
2435
+ .option('-y, --yes', 'Skip confirmation prompt')
2436
+ .option('--dry-run', 'Preview the request without sending it')
2437
+ .action(async (id, opts) => {
2438
+ const { deleteAbsenceTransaction } = await import('./operations/absencetransactions.js');
2439
+ if (!(await confirmMutation(`Delete absence transaction ${id}`, opts))) {
2440
+ return;
2441
+ }
2442
+ await deleteAbsenceTransaction(id);
2443
+ outputConfirmation(`Absence transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AbsenceTransaction');
2444
+ });
2445
+ // --- schedule times (schematider / Lön) ---
2446
+ const scheduletimes = program
2447
+ .command('schedule-times')
2448
+ .description('Schedule time operations (requires the Lön scope)');
2449
+ scheduletimes
2450
+ .command('get <employeeId> <date>')
2451
+ .description('Get the schedule for an employee on a date (YYYY-MM-DD)')
2452
+ .action(async (employeeId, date) => {
2453
+ const { getScheduleTime } = await import('./operations/scheduletimes.js');
2454
+ const data = await getScheduleTime(employeeId, date);
2455
+ outputDetail(data, scheduleTimeDetailColumns, json(), 'ScheduleTime');
2456
+ });
2457
+ scheduletimes
2458
+ .command('update <employeeId> <date>')
2459
+ .description('Update the schedule for an employee on a date')
2460
+ .requiredOption('--input <file>', 'Schedule data as JSON file (or - for stdin)')
2461
+ .option('-y, --yes', 'Skip confirmation prompt')
2462
+ .option('--dry-run', 'Preview the request without sending it')
2463
+ .action(async (employeeId, date, opts) => {
2464
+ const { updateScheduleTime } = await import('./operations/scheduletimes.js');
2465
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
2466
+ const fields = JSON.parse(raw);
2467
+ if (!(await confirmMutation(`Update schedule for ${employeeId} on ${date}`, opts, {
2468
+ ScheduleTime: fields,
2469
+ }))) {
2470
+ return;
2471
+ }
2472
+ const data = await updateScheduleTime(employeeId, date, fields);
2473
+ outputDetail(data, scheduleTimeDetailColumns, json(), 'ScheduleTime');
2474
+ });
2475
+ scheduletimes
2476
+ .command('reset-day <employeeId> <date>')
2477
+ .description('Update the schedule and reset the day for an employee on a date')
2478
+ .requiredOption('--input <file>', 'Schedule data as JSON file (or - for stdin)')
2479
+ .option('-y, --yes', 'Skip confirmation prompt')
2480
+ .option('--dry-run', 'Preview the request without sending it')
2481
+ .action(async (employeeId, date, opts) => {
2482
+ const { resetScheduleTimeDay } = await import('./operations/scheduletimes.js');
2483
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
2484
+ const fields = JSON.parse(raw);
2485
+ if (!(await confirmMutation(`Reset schedule day for ${employeeId} on ${date}`, opts, {
2486
+ ScheduleTime: fields,
2487
+ }))) {
2488
+ return;
2489
+ }
2490
+ const data = await resetScheduleTimeDay(employeeId, date, fields);
2491
+ outputDetail(data, scheduleTimeDetailColumns, json(), 'ScheduleTime');
2492
+ });
2108
2493
  // --- tax reductions (ROT/RUT) ---
2109
2494
  const taxreductions = program
2110
2495
  .command('tax-reductions')