@dayofweek/dcli 1.11.0 → 1.12.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 +29 -0
- package/dist/bin/dcli.js +121 -0
- package/dist/bundle/dcli.cjs +125 -1
- package/dist/client.d.ts +36 -0
- package/dist/client.js +53 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,6 +153,35 @@ dcli data get <dataset> --limit 100 --json
|
|
|
153
153
|
Responses are `{ dataset, total, truncated, rows }`. The CLI has no built-in
|
|
154
154
|
dataset names; new datasets appear in the listing without a CLI update.
|
|
155
155
|
|
|
156
|
+
## Time tracking (staff)
|
|
157
|
+
|
|
158
|
+
Hours registered against time projects. The server owns every rule — project
|
|
159
|
+
and participation periods, hour frames, self-financing, the reportable basis,
|
|
160
|
+
who may approve — and the CLI only carries JSON.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
dcli time projects list --json
|
|
164
|
+
dcli time projects show <projectId> --json
|
|
165
|
+
dcli time projects upsert --file project.json # create, or update with projectId
|
|
166
|
+
|
|
167
|
+
dcli time entries list --project <id> --from 2026-06-01 --to 2026-09-30 --json
|
|
168
|
+
dcli time entries import --project <id> --file entries.json --dry-run
|
|
169
|
+
dcli time entries import --project <id> --file entries.json --approved
|
|
170
|
+
|
|
171
|
+
dcli time months list --json
|
|
172
|
+
dcli time months submit --project <id> --month 2026-08
|
|
173
|
+
dcli time months approve --project <id> --month 2026-08 --user-email someone@example.com
|
|
174
|
+
dcli time months return --project <id> --month 2026-08 --user-email ... --comment "..."
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`entries import` is idempotent: each entry carries an import key (explicit, or
|
|
178
|
+
derived from person + date + activity + description), so re-running a file
|
|
179
|
+
updates rather than duplicates. Approved rows are locked and reported as
|
|
180
|
+
`skip_locked`; any invalid entry aborts the whole batch with nothing written.
|
|
181
|
+
Because real rows are written, the command refuses to run without `--dry-run`
|
|
182
|
+
first and `--approved` afterwards. Payload shapes are documented in the
|
|
183
|
+
`time-tracking` reference that `dcli skill install` fetches for staff.
|
|
184
|
+
|
|
156
185
|
## Feedback backlog
|
|
157
186
|
|
|
158
187
|
The customer feedback backlog that humans and coding agents work together.
|
package/dist/bin/dcli.js
CHANGED
|
@@ -1351,6 +1351,127 @@ feedback
|
|
|
1351
1351
|
rejectedReason: opts.rejectedReason,
|
|
1352
1352
|
}));
|
|
1353
1353
|
});
|
|
1354
|
+
// ── Time tracking ────────────────────────────────────────────────────────────
|
|
1355
|
+
//
|
|
1356
|
+
// Hours per project. The server owns the rules (project and participation
|
|
1357
|
+
// periods, hour frames, self-financing, the reportable basis, approvals);
|
|
1358
|
+
// this group parses flags, reads JSON files and prints the response. The
|
|
1359
|
+
// only local rule is the irreversibility gate on import: real rows are
|
|
1360
|
+
// written, so --dry-run first, then --approved.
|
|
1361
|
+
const time = program.command("time").description("Hours per project: projects, entries, monthly sheets");
|
|
1362
|
+
const timeProjects = time.command("projects").description("Time projects: setup and overview");
|
|
1363
|
+
timeProjects
|
|
1364
|
+
.command("list")
|
|
1365
|
+
.description("List time projects with totals and frame usage")
|
|
1366
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
1367
|
+
.action(async (opts) => {
|
|
1368
|
+
output(await getClient().listTimeProjects(opts.org));
|
|
1369
|
+
});
|
|
1370
|
+
timeProjects
|
|
1371
|
+
.command("show <projectId>")
|
|
1372
|
+
.description("One project: setup, person×activity×month matrix, entries with computed split, monthly sheets")
|
|
1373
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
1374
|
+
.action(async (projectId, opts) => {
|
|
1375
|
+
output(await getClient().getTimeProject(projectId, opts.org));
|
|
1376
|
+
});
|
|
1377
|
+
timeProjects
|
|
1378
|
+
.command("upsert")
|
|
1379
|
+
.description("Create or update a time project from a JSON file (include projectId to update)")
|
|
1380
|
+
.requiredOption("--file <path>", "JSON payload with the project setup (- for stdin)")
|
|
1381
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
1382
|
+
.action(async (opts) => {
|
|
1383
|
+
const raw = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
1384
|
+
const payload = JSON.parse(raw);
|
|
1385
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
1386
|
+
throw new Error("Payload must be a JSON object with the project setup");
|
|
1387
|
+
}
|
|
1388
|
+
output(await getClient().upsertTimeProject(payload, opts.org));
|
|
1389
|
+
});
|
|
1390
|
+
const timeEntries = time.command("entries").description("Registered hours");
|
|
1391
|
+
timeEntries
|
|
1392
|
+
.command("list")
|
|
1393
|
+
.description("Flat rows with the computed split (counted, self-financed, reportable, reasons)")
|
|
1394
|
+
.option("--project <projectId>", "Only this project")
|
|
1395
|
+
.option("--user <userId>", "Only this person (owners and admins)")
|
|
1396
|
+
.option("--from <YYYY-MM-DD>", "First work date")
|
|
1397
|
+
.option("--to <YYYY-MM-DD>", "Last work date")
|
|
1398
|
+
.option("--status <status>", "draft | submitted | approved")
|
|
1399
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
1400
|
+
.action(async (opts) => {
|
|
1401
|
+
output(await getClient().listTimeEntries({
|
|
1402
|
+
project: opts.project,
|
|
1403
|
+
user: opts.user,
|
|
1404
|
+
from: opts.from,
|
|
1405
|
+
to: opts.to,
|
|
1406
|
+
status: opts.status,
|
|
1407
|
+
org: opts.org,
|
|
1408
|
+
}));
|
|
1409
|
+
});
|
|
1410
|
+
timeEntries
|
|
1411
|
+
.command("import")
|
|
1412
|
+
.description("Idempotent batch import of hours into one project. Requires --dry-run or --approved")
|
|
1413
|
+
.requiredOption("--project <projectId>", "Target time project")
|
|
1414
|
+
.requiredOption("--file <path>", "JSON: { entries: [...] } or a bare array (- for stdin)")
|
|
1415
|
+
.option("--dry-run", "Validate and report what would happen, write nothing")
|
|
1416
|
+
.option("--approved", "The operator approved the dry-run result")
|
|
1417
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
1418
|
+
.action(async (opts) => {
|
|
1419
|
+
if (!opts.dryRun && !opts.approved) {
|
|
1420
|
+
throw new Error("Refusing to write: run with --dry-run first, show the operator the summary, " +
|
|
1421
|
+
"then repeat with --approved. This writes real hours, not proposals.");
|
|
1422
|
+
}
|
|
1423
|
+
const raw = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
1424
|
+
const payload = JSON.parse(raw);
|
|
1425
|
+
const entries = Array.isArray(payload) ? payload : payload?.entries;
|
|
1426
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
1427
|
+
throw new Error("Payload needs { entries: [...] } with at least one entry");
|
|
1428
|
+
}
|
|
1429
|
+
output(await getClient().importTimeEntries({
|
|
1430
|
+
projectId: opts.project,
|
|
1431
|
+
entries,
|
|
1432
|
+
dryRun: Boolean(opts.dryRun),
|
|
1433
|
+
org: opts.org,
|
|
1434
|
+
}));
|
|
1435
|
+
});
|
|
1436
|
+
const timeMonths = time.command("months").description("Monthly sheets: submit, approve, return, reopen");
|
|
1437
|
+
timeMonths
|
|
1438
|
+
.command("list")
|
|
1439
|
+
.description("Months waiting for your approval (project owner), or every submitted month (admin)")
|
|
1440
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
1441
|
+
.action(async (opts) => {
|
|
1442
|
+
output(await getClient().listPendingTimeMonths(opts.org));
|
|
1443
|
+
});
|
|
1444
|
+
for (const action of ["submit", "approve", "return", "reopen"]) {
|
|
1445
|
+
const descriptions = {
|
|
1446
|
+
submit: "Submit a month for approval (your own; admins may pass --user-email)",
|
|
1447
|
+
approve: "Approve a submitted month — locks its rows (project owner or admin)",
|
|
1448
|
+
return: "Return a submitted month as drafts, --comment required (project owner or admin)",
|
|
1449
|
+
reopen: "Reopen an approved month for correction; it needs a new approval",
|
|
1450
|
+
};
|
|
1451
|
+
timeMonths
|
|
1452
|
+
.command(action)
|
|
1453
|
+
.description(descriptions[action])
|
|
1454
|
+
.requiredOption("--project <projectId>", "Time project")
|
|
1455
|
+
.requiredOption("--month <YYYY-MM>", "Month")
|
|
1456
|
+
.option("--user-email <email>", "Whose month (defaults to yourself)")
|
|
1457
|
+
.option("--user <userId>", "Whose month, by user id")
|
|
1458
|
+
.option("--comment <text>", "Comment (required for return)")
|
|
1459
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
1460
|
+
.action(async (opts) => {
|
|
1461
|
+
if (action === "return" && !opts.comment) {
|
|
1462
|
+
throw new Error("--comment is required when returning a month");
|
|
1463
|
+
}
|
|
1464
|
+
output(await getClient().actOnTimeMonth({
|
|
1465
|
+
projectId: opts.project,
|
|
1466
|
+
month: opts.month,
|
|
1467
|
+
action,
|
|
1468
|
+
userId: opts.user,
|
|
1469
|
+
userEmail: opts.userEmail,
|
|
1470
|
+
comment: opts.comment,
|
|
1471
|
+
org: opts.org,
|
|
1472
|
+
}));
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1354
1475
|
// ── Admin Commands ───────────────────────────────────────────────────────────
|
|
1355
1476
|
//
|
|
1356
1477
|
// These are admin-only. They're registered as hidden subcommands when the
|
package/dist/bundle/dcli.cjs
CHANGED
|
@@ -4773,6 +4773,52 @@ var DayOfWeekClient = class {
|
|
|
4773
4773
|
async archiveSharedSkill(skillId) {
|
|
4774
4774
|
return this.delete(`/brain/skills/${encodeURIComponent(skillId)}`);
|
|
4775
4775
|
}
|
|
4776
|
+
// ── Time tracking ─────────────────────────────────────────────────────────
|
|
4777
|
+
//
|
|
4778
|
+
// Hours registered against time projects. Every rule (periods, frames,
|
|
4779
|
+
// self-financing, the reportable basis, who may approve) is evaluated by
|
|
4780
|
+
// the server; the client only carries JSON and prints what comes back.
|
|
4781
|
+
async listTimeProjects(org) {
|
|
4782
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
4783
|
+
return this.get(`/time/projects${qs}`);
|
|
4784
|
+
}
|
|
4785
|
+
async getTimeProject(projectId, org) {
|
|
4786
|
+
const params = new URLSearchParams({ id: projectId });
|
|
4787
|
+
if (org) params.set("org", org);
|
|
4788
|
+
return this.get(`/time/projects?${params.toString()}`);
|
|
4789
|
+
}
|
|
4790
|
+
async upsertTimeProject(payload, org) {
|
|
4791
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
4792
|
+
return this.post(`/time/projects${qs}`, payload);
|
|
4793
|
+
}
|
|
4794
|
+
async listTimeEntries(opts) {
|
|
4795
|
+
const params = new URLSearchParams();
|
|
4796
|
+
if (opts?.project) params.set("project", opts.project);
|
|
4797
|
+
if (opts?.user) params.set("user", opts.user);
|
|
4798
|
+
if (opts?.from) params.set("from", opts.from);
|
|
4799
|
+
if (opts?.to) params.set("to", opts.to);
|
|
4800
|
+
if (opts?.status) params.set("status", opts.status);
|
|
4801
|
+
if (opts?.org) params.set("org", opts.org);
|
|
4802
|
+
const qs = params.toString();
|
|
4803
|
+
return this.get(`/time/entries${qs ? `?${qs}` : ""}`);
|
|
4804
|
+
}
|
|
4805
|
+
async importTimeEntries(input) {
|
|
4806
|
+
const qs = input.org ? `?org=${encodeURIComponent(input.org)}` : "";
|
|
4807
|
+
return this.post(`/time/entries${qs}`, {
|
|
4808
|
+
projectId: input.projectId,
|
|
4809
|
+
dryRun: input.dryRun,
|
|
4810
|
+
entries: input.entries
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
async listPendingTimeMonths(org) {
|
|
4814
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
4815
|
+
return this.get(`/time/months${qs}`);
|
|
4816
|
+
}
|
|
4817
|
+
async actOnTimeMonth(input) {
|
|
4818
|
+
const { org, ...body } = input;
|
|
4819
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
4820
|
+
return this.post(`/time/months${qs}`, body);
|
|
4821
|
+
}
|
|
4776
4822
|
// ── Schema ────────────────────────────────────────────────────────────────
|
|
4777
4823
|
async getSchema() {
|
|
4778
4824
|
return this.get("/schema");
|
|
@@ -5221,7 +5267,7 @@ var import_promises4 = require("node:readline/promises");
|
|
|
5221
5267
|
// package.json
|
|
5222
5268
|
var package_default = {
|
|
5223
5269
|
name: "@dayofweek/dcli",
|
|
5224
|
-
version: "1.
|
|
5270
|
+
version: "1.12.0",
|
|
5225
5271
|
description: "CLI for the Day of Week AgTech platform \u2014 read data and submit proposals for review",
|
|
5226
5272
|
license: "MIT",
|
|
5227
5273
|
type: "module",
|
|
@@ -6219,6 +6265,84 @@ feedback.command("status <itemId>").description("Set status and/or priority on a
|
|
|
6219
6265
|
rejectedReason: opts.rejectedReason
|
|
6220
6266
|
}));
|
|
6221
6267
|
});
|
|
6268
|
+
var time = program2.command("time").description("Hours per project: projects, entries, monthly sheets");
|
|
6269
|
+
var timeProjects = time.command("projects").description("Time projects: setup and overview");
|
|
6270
|
+
timeProjects.command("list").description("List time projects with totals and frame usage").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
|
|
6271
|
+
output(await getClient().listTimeProjects(opts.org));
|
|
6272
|
+
});
|
|
6273
|
+
timeProjects.command("show <projectId>").description("One project: setup, person\xD7activity\xD7month matrix, entries with computed split, monthly sheets").option("--org <org>", "Organization slug or id (admin only)").action(async (projectId, opts) => {
|
|
6274
|
+
output(await getClient().getTimeProject(projectId, opts.org));
|
|
6275
|
+
});
|
|
6276
|
+
timeProjects.command("upsert").description("Create or update a time project from a JSON file (include projectId to update)").requiredOption("--file <path>", "JSON payload with the project setup (- for stdin)").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
|
|
6277
|
+
const raw = opts.file === "-" ? (0, import_node_fs8.readFileSync)(0, "utf8") : (0, import_node_fs8.readFileSync)(opts.file, "utf8");
|
|
6278
|
+
const payload = JSON.parse(raw);
|
|
6279
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
6280
|
+
throw new Error("Payload must be a JSON object with the project setup");
|
|
6281
|
+
}
|
|
6282
|
+
output(await getClient().upsertTimeProject(payload, opts.org));
|
|
6283
|
+
});
|
|
6284
|
+
var timeEntries = time.command("entries").description("Registered hours");
|
|
6285
|
+
timeEntries.command("list").description("Flat rows with the computed split (counted, self-financed, reportable, reasons)").option("--project <projectId>", "Only this project").option("--user <userId>", "Only this person (owners and admins)").option("--from <YYYY-MM-DD>", "First work date").option("--to <YYYY-MM-DD>", "Last work date").option("--status <status>", "draft | submitted | approved").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
|
|
6286
|
+
output(
|
|
6287
|
+
await getClient().listTimeEntries({
|
|
6288
|
+
project: opts.project,
|
|
6289
|
+
user: opts.user,
|
|
6290
|
+
from: opts.from,
|
|
6291
|
+
to: opts.to,
|
|
6292
|
+
status: opts.status,
|
|
6293
|
+
org: opts.org
|
|
6294
|
+
})
|
|
6295
|
+
);
|
|
6296
|
+
});
|
|
6297
|
+
timeEntries.command("import").description("Idempotent batch import of hours into one project. Requires --dry-run or --approved").requiredOption("--project <projectId>", "Target time project").requiredOption("--file <path>", "JSON: { entries: [...] } or a bare array (- for stdin)").option("--dry-run", "Validate and report what would happen, write nothing").option("--approved", "The operator approved the dry-run result").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
|
|
6298
|
+
if (!opts.dryRun && !opts.approved) {
|
|
6299
|
+
throw new Error(
|
|
6300
|
+
"Refusing to write: run with --dry-run first, show the operator the summary, then repeat with --approved. This writes real hours, not proposals."
|
|
6301
|
+
);
|
|
6302
|
+
}
|
|
6303
|
+
const raw = opts.file === "-" ? (0, import_node_fs8.readFileSync)(0, "utf8") : (0, import_node_fs8.readFileSync)(opts.file, "utf8");
|
|
6304
|
+
const payload = JSON.parse(raw);
|
|
6305
|
+
const entries = Array.isArray(payload) ? payload : payload?.entries;
|
|
6306
|
+
if (!Array.isArray(entries) || entries.length === 0) {
|
|
6307
|
+
throw new Error("Payload needs { entries: [...] } with at least one entry");
|
|
6308
|
+
}
|
|
6309
|
+
output(
|
|
6310
|
+
await getClient().importTimeEntries({
|
|
6311
|
+
projectId: opts.project,
|
|
6312
|
+
entries,
|
|
6313
|
+
dryRun: Boolean(opts.dryRun),
|
|
6314
|
+
org: opts.org
|
|
6315
|
+
})
|
|
6316
|
+
);
|
|
6317
|
+
});
|
|
6318
|
+
var timeMonths = time.command("months").description("Monthly sheets: submit, approve, return, reopen");
|
|
6319
|
+
timeMonths.command("list").description("Months waiting for your approval (project owner), or every submitted month (admin)").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
|
|
6320
|
+
output(await getClient().listPendingTimeMonths(opts.org));
|
|
6321
|
+
});
|
|
6322
|
+
for (const action of ["submit", "approve", "return", "reopen"]) {
|
|
6323
|
+
const descriptions = {
|
|
6324
|
+
submit: "Submit a month for approval (your own; admins may pass --user-email)",
|
|
6325
|
+
approve: "Approve a submitted month \u2014 locks its rows (project owner or admin)",
|
|
6326
|
+
return: "Return a submitted month as drafts, --comment required (project owner or admin)",
|
|
6327
|
+
reopen: "Reopen an approved month for correction; it needs a new approval"
|
|
6328
|
+
};
|
|
6329
|
+
timeMonths.command(action).description(descriptions[action]).requiredOption("--project <projectId>", "Time project").requiredOption("--month <YYYY-MM>", "Month").option("--user-email <email>", "Whose month (defaults to yourself)").option("--user <userId>", "Whose month, by user id").option("--comment <text>", "Comment (required for return)").option("--org <org>", "Organization slug or id (admin only)").action(async (opts) => {
|
|
6330
|
+
if (action === "return" && !opts.comment) {
|
|
6331
|
+
throw new Error("--comment is required when returning a month");
|
|
6332
|
+
}
|
|
6333
|
+
output(
|
|
6334
|
+
await getClient().actOnTimeMonth({
|
|
6335
|
+
projectId: opts.project,
|
|
6336
|
+
month: opts.month,
|
|
6337
|
+
action,
|
|
6338
|
+
userId: opts.user,
|
|
6339
|
+
userEmail: opts.userEmail,
|
|
6340
|
+
comment: opts.comment,
|
|
6341
|
+
org: opts.org
|
|
6342
|
+
})
|
|
6343
|
+
);
|
|
6344
|
+
});
|
|
6345
|
+
}
|
|
6222
6346
|
function registerAdminCommands() {
|
|
6223
6347
|
const cfg = loadConfig();
|
|
6224
6348
|
if (!cfg.isAdmin) return;
|
package/dist/client.d.ts
CHANGED
|
@@ -474,6 +474,42 @@ export declare class DayOfWeekClient {
|
|
|
474
474
|
}>;
|
|
475
475
|
}): Promise<SharedSkillSummary>;
|
|
476
476
|
archiveSharedSkill(skillId: string): Promise<SharedSkillSummary>;
|
|
477
|
+
listTimeProjects(org?: string): Promise<{
|
|
478
|
+
projects: any[];
|
|
479
|
+
}>;
|
|
480
|
+
getTimeProject(projectId: string, org?: string): Promise<any>;
|
|
481
|
+
upsertTimeProject(payload: Record<string, unknown>, org?: string): Promise<{
|
|
482
|
+
projectId: string;
|
|
483
|
+
}>;
|
|
484
|
+
listTimeEntries(opts?: {
|
|
485
|
+
project?: string;
|
|
486
|
+
user?: string;
|
|
487
|
+
from?: string;
|
|
488
|
+
to?: string;
|
|
489
|
+
status?: string;
|
|
490
|
+
org?: string;
|
|
491
|
+
}): Promise<{
|
|
492
|
+
total: number;
|
|
493
|
+
rows: any[];
|
|
494
|
+
}>;
|
|
495
|
+
importTimeEntries(input: {
|
|
496
|
+
projectId: string;
|
|
497
|
+
entries: unknown[];
|
|
498
|
+
dryRun: boolean;
|
|
499
|
+
org?: string;
|
|
500
|
+
}): Promise<any>;
|
|
501
|
+
listPendingTimeMonths(org?: string): Promise<{
|
|
502
|
+
approvals: any[];
|
|
503
|
+
}>;
|
|
504
|
+
actOnTimeMonth(input: {
|
|
505
|
+
projectId: string;
|
|
506
|
+
month: string;
|
|
507
|
+
action: "submit" | "approve" | "return" | "reopen";
|
|
508
|
+
userId?: string;
|
|
509
|
+
userEmail?: string;
|
|
510
|
+
comment?: string;
|
|
511
|
+
org?: string;
|
|
512
|
+
}): Promise<any>;
|
|
477
513
|
getSchema(): Promise<any>;
|
|
478
514
|
private get;
|
|
479
515
|
private post;
|
package/dist/client.js
CHANGED
|
@@ -678,6 +678,59 @@ export class DayOfWeekClient {
|
|
|
678
678
|
async archiveSharedSkill(skillId) {
|
|
679
679
|
return this.delete(`/brain/skills/${encodeURIComponent(skillId)}`);
|
|
680
680
|
}
|
|
681
|
+
// ── Time tracking ─────────────────────────────────────────────────────────
|
|
682
|
+
//
|
|
683
|
+
// Hours registered against time projects. Every rule (periods, frames,
|
|
684
|
+
// self-financing, the reportable basis, who may approve) is evaluated by
|
|
685
|
+
// the server; the client only carries JSON and prints what comes back.
|
|
686
|
+
async listTimeProjects(org) {
|
|
687
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
688
|
+
return this.get(`/time/projects${qs}`);
|
|
689
|
+
}
|
|
690
|
+
async getTimeProject(projectId, org) {
|
|
691
|
+
const params = new URLSearchParams({ id: projectId });
|
|
692
|
+
if (org)
|
|
693
|
+
params.set("org", org);
|
|
694
|
+
return this.get(`/time/projects?${params.toString()}`);
|
|
695
|
+
}
|
|
696
|
+
async upsertTimeProject(payload, org) {
|
|
697
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
698
|
+
return this.post(`/time/projects${qs}`, payload);
|
|
699
|
+
}
|
|
700
|
+
async listTimeEntries(opts) {
|
|
701
|
+
const params = new URLSearchParams();
|
|
702
|
+
if (opts?.project)
|
|
703
|
+
params.set("project", opts.project);
|
|
704
|
+
if (opts?.user)
|
|
705
|
+
params.set("user", opts.user);
|
|
706
|
+
if (opts?.from)
|
|
707
|
+
params.set("from", opts.from);
|
|
708
|
+
if (opts?.to)
|
|
709
|
+
params.set("to", opts.to);
|
|
710
|
+
if (opts?.status)
|
|
711
|
+
params.set("status", opts.status);
|
|
712
|
+
if (opts?.org)
|
|
713
|
+
params.set("org", opts.org);
|
|
714
|
+
const qs = params.toString();
|
|
715
|
+
return this.get(`/time/entries${qs ? `?${qs}` : ""}`);
|
|
716
|
+
}
|
|
717
|
+
async importTimeEntries(input) {
|
|
718
|
+
const qs = input.org ? `?org=${encodeURIComponent(input.org)}` : "";
|
|
719
|
+
return this.post(`/time/entries${qs}`, {
|
|
720
|
+
projectId: input.projectId,
|
|
721
|
+
dryRun: input.dryRun,
|
|
722
|
+
entries: input.entries,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
async listPendingTimeMonths(org) {
|
|
726
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
727
|
+
return this.get(`/time/months${qs}`);
|
|
728
|
+
}
|
|
729
|
+
async actOnTimeMonth(input) {
|
|
730
|
+
const { org, ...body } = input;
|
|
731
|
+
const qs = org ? `?org=${encodeURIComponent(org)}` : "";
|
|
732
|
+
return this.post(`/time/months${qs}`, body);
|
|
733
|
+
}
|
|
681
734
|
// ── Schema ────────────────────────────────────────────────────────────────
|
|
682
735
|
async getSchema() {
|
|
683
736
|
return this.get("/schema");
|