@go-labs-sg/bb 1.15.0 → 1.16.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 +1 -1
- package/dist/commands.js +202 -5
- package/dist/index.js +13 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Global options and flags use `--key=value` or `--key value` (see `bb help`).
|
|
|
72
72
|
| **Bills / claims** | `list-bills` (`--isClaimable false` for bills, `--isClaimable true` for claims, omit for both), `list-claims` (claims only), `create-bill` (`--payload`; set `isClaimable=false` for a bill or `isClaimable=true` for a claim; non-legacy supplier bills from 1 Jul 2026 00:00 SGT require approved quotation coverage), `update-bill` (`--payload`), `delete-bill`, `create-bill-approval` (also sends approval request emails), `update-bill-status`, `patch-bill-payment` (PAID bills: `--paymentTrackingUrl`, `--paymentReference`, `--quickbooksBillId`, `--paymentDate` ISO; clear with `--clearPaymentTrackingUrl` / `--clearPaymentReference` / `--clearQuickbooksBillId` / `--clearPaymentDate` `true`), `patch-bill-invoice-number`, `get-bill-attachments`, `upload-bill-attachment` (`<billId>` + local path), `get-bill-details` |
|
|
73
73
|
| **Quotations** | `list-quotations`, `get-quotation`, `upload-quotation-attachment` (`<projectId>` + local PDF path; returns attachment JSON for payload use), `create-quotation` (`--payload` for `quotation.createDraft`), `submit-quotation`, `approve-quotation`, `reject-quotation`, `download-quotation-pdf` (`original`, `staff`, or `final`) |
|
|
74
74
|
| **Approvals** | `list-approvals` / `get-pending-approvals` (`--type budget\|supplier\|bill\|quotation\|all`), `approve-bill` / `reject-bill` (send reply email), `approve-budget` / `reject-budget` (send reply email), `approve-supplier` / `reject-supplier` (send reply email), `approve-quotation` / `reject-quotation` |
|
|
75
|
-
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--
|
|
75
|
+
| **Companies & projects** | `list-companies`, `get-company`, `create-company`, `update-company` (`--payload`), `delete-company`, `list-projects`, `get-project`, `create-project` (required: `--name`, `--companyId`, `--contactPersonId`, `--insideSalesId`, `--businessDevelopmentId`, `--venue`, `--startDate` as ISO datetime for project/window start; when `--asanaTaskId` is omitted, the CLI searches open Asana lead tasks in the Deals project, prompts for one of the top five matches, and resolves Slack channel fields from the selected deal card; optional `--asanaSearch`, `--pax`, `--endDate`, `--description`; always requests QBO project import like the web app), `update-project` (`--payload` with `dateRange.from` / `dateRange.to` for the project window; optional `requestQboAccountantNotification` in JSON), `delete-project`, `update-project-status` (`<id>` `<status>`: `PITCH` \| `WON` \| `COMPLETED` \| `LOST`; for `PITCH` → `WON` also pass `--projectManagerId` or `--projectManagerName`; when marking `WON` without an accepted/closed budget or proof, pass `--wonOverrideReason`) |
|
|
76
76
|
| **Contacts** | `list-contacts`, `create-contact-person` (`--payload`), `update-contact-person` (`--payload`) |
|
|
77
77
|
| **Suppliers & items** | `list-suppliers` (defaults to active suppliers, `--perPage 10`, sorted by `createdAt` desc; supports `--name`, `--sortBy` for scalar supplier fields, `--sortDir`, `--createdBy`, `--gstRegistered`, `--status`, `--supplierTags`, `--active false` for archived suppliers), `create-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `update-supplier` (`--payload`; when supplier status is `PENDING_APPROVAL`, also runs `supplier.createSupplierApproval` and `email.sendSupplierApprovalRequestEmail`), `delete-suppliers` (`--ids` CSV; admin; archives/deactivates related items), `reactivate-suppliers` (`--ids` CSV; admin; reactivates related items), `create-certification` / `create-payment-method` / `create-supplier-role` / `create-supplier-tag` (`--name`), `get-supplier-details` (includes `supplierApprovalSummary` for pending approvers, superseded approvers, and the actual responder/respondedAt metadata), `get-supplier-analytics`, `list-items`, `create-item` (`--payload`), `update-item` (`--payload`), `delete-item`, `get-item`, `list-item-categories`, `create-item-category`, `update-item-category`, `delete-item-categories` (`--ids` CSV; admin) |
|
|
78
78
|
| **Dashboard & users** | `list-users`, `get-user-performance`, `get-dashboard`, `get-monthly-metrics`, `get-system-overview`, `get-estimate-performance`, `get-financial-overview` |
|
package/dist/commands.js
CHANGED
|
@@ -13,17 +13,200 @@ const BUDGET = "BUDGET";
|
|
|
13
13
|
const BILL = "BILL";
|
|
14
14
|
const SUPPLIER = "SUPPLIER";
|
|
15
15
|
const QUOTATION = "QUOTATION";
|
|
16
|
+
const ASANA_WON_LOST_SECTION_GIDS = new Set([
|
|
17
|
+
"1211678338364908",
|
|
18
|
+
"1211678338364907",
|
|
19
|
+
]);
|
|
20
|
+
const MAX_ASANA_LEAD_CANDIDATES = 5;
|
|
21
|
+
const ASANA_SEARCH_DETAIL_LIMIT = 15;
|
|
16
22
|
function isPendingRequest(n) {
|
|
17
23
|
return n.notificationType === "request" && n.status === "PENDING_APPROVAL";
|
|
18
24
|
}
|
|
19
25
|
const out = (data) => {
|
|
20
26
|
console.log(JSON.stringify(data, null, 2));
|
|
21
27
|
};
|
|
28
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
29
|
+
const stringField = (record, key) => {
|
|
30
|
+
const value = record[key];
|
|
31
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
32
|
+
};
|
|
33
|
+
const asanaSectionsForTask = (task) => {
|
|
34
|
+
if (!isRecord(task) || !Array.isArray(task.memberships))
|
|
35
|
+
return [];
|
|
36
|
+
return task.memberships.flatMap((membership) => {
|
|
37
|
+
if (!isRecord(membership) || !isRecord(membership.section))
|
|
38
|
+
return [];
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
gid: stringField(membership.section, "gid"),
|
|
42
|
+
name: stringField(membership.section, "name"),
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const isWonOrLostAsanaTask = (task) => asanaSectionsForTask(task).some((section) => {
|
|
48
|
+
if (section.gid && ASANA_WON_LOST_SECTION_GIDS.has(section.gid)) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const name = section.name?.trim().toLowerCase();
|
|
52
|
+
return Boolean(name && /\b(won|lost)\b/.test(name));
|
|
53
|
+
});
|
|
54
|
+
const asanaCandidateFromTask = (task, details) => {
|
|
55
|
+
const source = details ?? task;
|
|
56
|
+
const sourceRecord = isRecord(source) ? source : undefined;
|
|
57
|
+
const gid = (sourceRecord ? stringField(sourceRecord, "gid") : undefined) ?? task.gid;
|
|
58
|
+
const name = (sourceRecord ? stringField(sourceRecord, "name") : undefined) ??
|
|
59
|
+
task.name ??
|
|
60
|
+
gid;
|
|
61
|
+
if (!gid || isWonOrLostAsanaTask(source))
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
gid,
|
|
65
|
+
name,
|
|
66
|
+
modifiedAt: (sourceRecord ? stringField(sourceRecord, "modified_at") : undefined) ??
|
|
67
|
+
task.modified_at,
|
|
68
|
+
createdAt: (sourceRecord ? stringField(sourceRecord, "created_at") : undefined) ??
|
|
69
|
+
task.created_at,
|
|
70
|
+
sections: asanaSectionsForTask(source),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const uniqueSearchQueries = (...queries) => {
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const unique = [];
|
|
76
|
+
for (const query of queries) {
|
|
77
|
+
const normalized = query?.trim() ?? "";
|
|
78
|
+
if (!normalized)
|
|
79
|
+
continue;
|
|
80
|
+
const key = normalized.toLowerCase();
|
|
81
|
+
if (seen.has(key))
|
|
82
|
+
continue;
|
|
83
|
+
seen.add(key);
|
|
84
|
+
unique.push(normalized);
|
|
85
|
+
}
|
|
86
|
+
return unique;
|
|
87
|
+
};
|
|
88
|
+
const searchOpenAsanaLeadCandidates = async ({ projectName, searchQuery, }) => {
|
|
89
|
+
const queries = uniqueSearchQueries(projectName, searchQuery);
|
|
90
|
+
const candidates = [];
|
|
91
|
+
const seenTaskIds = new Set();
|
|
92
|
+
for (const query of queries) {
|
|
93
|
+
const results = await api.asana.searchTasks.query({ query });
|
|
94
|
+
const tasks = results.data.slice(0, ASANA_SEARCH_DETAIL_LIMIT);
|
|
95
|
+
const detailsByTaskId = new Map();
|
|
96
|
+
await Promise.all(tasks.map(async (task) => {
|
|
97
|
+
if (!task.gid)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
const details = await api.asana.getTaskById.query({ id: task.gid });
|
|
101
|
+
detailsByTaskId.set(task.gid, details.data);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Keep the compact search result if detail lookup fails for one task.
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
for (const task of tasks) {
|
|
108
|
+
if (!task.gid || seenTaskIds.has(task.gid))
|
|
109
|
+
continue;
|
|
110
|
+
const candidate = asanaCandidateFromTask(task, detailsByTaskId.get(task.gid));
|
|
111
|
+
if (!candidate)
|
|
112
|
+
continue;
|
|
113
|
+
seenTaskIds.add(candidate.gid);
|
|
114
|
+
candidates.push(candidate);
|
|
115
|
+
if (candidates.length >= MAX_ASANA_LEAD_CANDIDATES)
|
|
116
|
+
return candidates;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return candidates;
|
|
120
|
+
};
|
|
121
|
+
const formatAsanaCandidateMetadata = (candidate) => {
|
|
122
|
+
const sectionText = candidate.sections
|
|
123
|
+
.map((section) => section.name ?? section.gid)
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.join(", ") || "stage unavailable";
|
|
126
|
+
const dateText = candidate.modifiedAt
|
|
127
|
+
? `modified ${new Date(candidate.modifiedAt).toISOString()}`
|
|
128
|
+
: candidate.createdAt
|
|
129
|
+
? `created ${new Date(candidate.createdAt).toISOString()}`
|
|
130
|
+
: "date unavailable";
|
|
131
|
+
return `${sectionText}; ${dateText}; ${candidate.gid}`;
|
|
132
|
+
};
|
|
133
|
+
const promptForAsanaLeadCandidate = async (candidates) => {
|
|
134
|
+
if (candidates.length === 0) {
|
|
135
|
+
throw new Error("No open Asana lead tasks were found in the Deals project. Pass --asanaTaskId explicitly or update the Deals project first.");
|
|
136
|
+
}
|
|
137
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
138
|
+
throw new Error("create-project requires --asanaTaskId in non-interactive runs. Interactive runs can omit it to choose from the top Asana lead task matches.");
|
|
139
|
+
}
|
|
140
|
+
console.error("Select the Asana lead task to link to this project:");
|
|
141
|
+
candidates.forEach((candidate, index) => {
|
|
142
|
+
console.error(`${index + 1}. ${candidate.name} (${formatAsanaCandidateMetadata(candidate)})`);
|
|
143
|
+
});
|
|
144
|
+
const rl = createInterface({
|
|
145
|
+
input: process.stdin,
|
|
146
|
+
output: process.stderr,
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
while (true) {
|
|
150
|
+
const answer = await rl.question(`Choose 1-${candidates.length}, or q to abort: `);
|
|
151
|
+
const trimmed = answer.trim().toLowerCase();
|
|
152
|
+
if (trimmed === "q" || trimmed === "quit") {
|
|
153
|
+
throw new Error("Aborted.");
|
|
154
|
+
}
|
|
155
|
+
const selected = Number.parseInt(trimmed, 10);
|
|
156
|
+
if (Number.isInteger(selected) &&
|
|
157
|
+
selected >= 1 &&
|
|
158
|
+
selected <= candidates.length) {
|
|
159
|
+
const candidate = candidates[selected - 1];
|
|
160
|
+
if (candidate)
|
|
161
|
+
return candidate;
|
|
162
|
+
}
|
|
163
|
+
console.error(`Enter a number from 1 to ${candidates.length}.`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
rl.close();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const resolveAsanaDealSlackChannel = async (asanaTaskId) => {
|
|
171
|
+
const result = await api.asana.getDealSlackChannel.query({ id: asanaTaskId });
|
|
172
|
+
if (result.status !== "resolved") {
|
|
173
|
+
throw new Error(result.message);
|
|
174
|
+
}
|
|
175
|
+
console.error(`Resolved Slack channel from Asana deal: #${result.channel.slackChannelName} (${result.channel.slackChannelId})`);
|
|
176
|
+
return result.channel;
|
|
177
|
+
};
|
|
178
|
+
const resolveCreateProjectLinks = async (opts) => {
|
|
179
|
+
const asanaTaskId = opts.asanaTaskId?.trim() ||
|
|
180
|
+
(await promptForAsanaLeadCandidate(await searchOpenAsanaLeadCandidates({
|
|
181
|
+
projectName: opts.name,
|
|
182
|
+
searchQuery: opts.asanaSearch,
|
|
183
|
+
}))).gid;
|
|
184
|
+
const hasCompleteManualSlackChannel = Boolean(opts.slackChannelId && opts.slackChannelUrl && opts.slackChannelName);
|
|
185
|
+
if (hasCompleteManualSlackChannel) {
|
|
186
|
+
return {
|
|
187
|
+
asanaTaskId,
|
|
188
|
+
slackChannelId: opts.slackChannelId,
|
|
189
|
+
slackChannelUrl: opts.slackChannelUrl,
|
|
190
|
+
slackChannelName: opts.slackChannelName,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const slackChannel = await resolveAsanaDealSlackChannel(asanaTaskId);
|
|
194
|
+
return {
|
|
195
|
+
asanaTaskId,
|
|
196
|
+
...slackChannel,
|
|
197
|
+
};
|
|
198
|
+
};
|
|
22
199
|
const combineDateAndTime = (day, time) => {
|
|
23
200
|
const d = new Date(day);
|
|
24
201
|
d.setHours(time.getHours(), time.getMinutes(), time.getSeconds(), 0);
|
|
25
202
|
return d;
|
|
26
203
|
};
|
|
204
|
+
const formatOptionalProjectPaxInput = (pax) => {
|
|
205
|
+
if (typeof pax !== "number" || !Number.isFinite(pax) || pax <= 0) {
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
return String(pax);
|
|
209
|
+
};
|
|
27
210
|
/** Extension-based MIME, like browser `File.type` when the OS provides it */
|
|
28
211
|
const contentTypeHeaderForFileName = (fileName) => {
|
|
29
212
|
const ct = contentType(fileName);
|
|
@@ -139,6 +322,9 @@ export async function updateBudgetStatus(budgetId, status, opts) {
|
|
|
139
322
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
140
323
|
id: budgetId,
|
|
141
324
|
status,
|
|
325
|
+
...(opts?.projectManagerId !== undefined && {
|
|
326
|
+
projectManagerId: opts.projectManagerId,
|
|
327
|
+
}),
|
|
142
328
|
...(opts?.markProjectWon !== undefined && {
|
|
143
329
|
markProjectWon: opts.markProjectWon,
|
|
144
330
|
}),
|
|
@@ -200,7 +386,7 @@ const runBudgetPreflight = async (budgetId, preflight) => {
|
|
|
200
386
|
insideSalesId: project.insideSalesId ?? "",
|
|
201
387
|
businessDevelopmentId: project.businessDevelopmentId ?? "",
|
|
202
388
|
venue: project.venue ?? "",
|
|
203
|
-
pax:
|
|
389
|
+
pax: formatOptionalProjectPaxInput(project.pax),
|
|
204
390
|
...(project.slackChannelId &&
|
|
205
391
|
project.slackChannelUrl &&
|
|
206
392
|
project.slackChannelName
|
|
@@ -513,6 +699,9 @@ export async function uploadBudgetWinProofFromPath(budgetId, filePath, opts) {
|
|
|
513
699
|
const result = await api.budget.updateBudgetStatus.mutate({
|
|
514
700
|
id: budgetId,
|
|
515
701
|
status: "ESTIMATE_ACCEPTED",
|
|
702
|
+
...(opts?.projectManagerId !== undefined && {
|
|
703
|
+
projectManagerId: opts.projectManagerId,
|
|
704
|
+
}),
|
|
516
705
|
...(opts?.markProjectWon !== undefined && {
|
|
517
706
|
markProjectWon: opts.markProjectWon,
|
|
518
707
|
}),
|
|
@@ -1220,6 +1409,14 @@ export async function getProject(id) {
|
|
|
1220
1409
|
out({ project, budgetsOverview, billsOverview });
|
|
1221
1410
|
}
|
|
1222
1411
|
export async function createProject(opts) {
|
|
1412
|
+
const links = await resolveCreateProjectLinks({
|
|
1413
|
+
name: opts.name,
|
|
1414
|
+
asanaTaskId: opts.asanaTaskId,
|
|
1415
|
+
asanaSearch: opts.asanaSearch,
|
|
1416
|
+
slackChannelId: opts.slackChannelId,
|
|
1417
|
+
slackChannelUrl: opts.slackChannelUrl,
|
|
1418
|
+
slackChannelName: opts.slackChannelName,
|
|
1419
|
+
});
|
|
1223
1420
|
const result = await api.project.createProject.mutate({
|
|
1224
1421
|
name: opts.name,
|
|
1225
1422
|
description: opts.description,
|
|
@@ -1229,10 +1426,10 @@ export async function createProject(opts) {
|
|
|
1229
1426
|
businessDevelopmentId: opts.businessDevelopmentId,
|
|
1230
1427
|
venue: opts.venue,
|
|
1231
1428
|
pax: opts.pax,
|
|
1232
|
-
asanaTaskId:
|
|
1233
|
-
slackChannelId:
|
|
1234
|
-
slackChannelUrl:
|
|
1235
|
-
slackChannelName:
|
|
1429
|
+
asanaTaskId: links.asanaTaskId,
|
|
1430
|
+
slackChannelId: links.slackChannelId,
|
|
1431
|
+
slackChannelUrl: links.slackChannelUrl,
|
|
1432
|
+
slackChannelName: links.slackChannelName,
|
|
1236
1433
|
dateRange: {
|
|
1237
1434
|
from: new Date(opts.startDate),
|
|
1238
1435
|
to: opts.endDate ? new Date(opts.endDate) : undefined,
|
package/dist/index.js
CHANGED
|
@@ -133,12 +133,12 @@ Budgets
|
|
|
133
133
|
get-budget-details <id> Full detail (margins, suppliers, etc.)
|
|
134
134
|
get-budget-categories
|
|
135
135
|
get-budget-versions <budgetId>
|
|
136
|
-
update-budget-status <id> <status> [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
136
|
+
update-budget-status <id> <status> [--projectManagerId <id>] [--markProjectWon true|false] [--projectStatusOnCommercialRejection PITCH|LOST]
|
|
137
137
|
Budget-changing commands require interactive "yes" confirmation when the current budget is ESTIMATE_CREATED, ESTIMATE_SENT, ESTIMATE_ACCEPTED, ESTIMATE_REJECTED, or ESTIMATE_CLOSED.
|
|
138
|
-
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, and auto-creates the Asana project/section when enabled.
|
|
138
|
+
ESTIMATE_ACCEPTED requires win proof, auto-marks PITCH or LOST projects WON, requires --projectManagerId when the project has no PM, and auto-creates the Asana project/section when enabled.
|
|
139
139
|
ESTIMATE_REJECTED requires --projectStatusOnCommercialRejection when rejecting the only accepted/closed budget on a commercial project.
|
|
140
140
|
status: ${budgetStatusesForHelp.join(", ")}
|
|
141
|
-
mark-budget-won <budgetId> <filePath> [--markProjectWon true|false] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED; PITCH or LOST projects become WON automatically
|
|
141
|
+
mark-budget-won <budgetId> <filePath> [--projectManagerId <id>] [--markProjectWon true|false] Upload signed quote/PO proof and set status to ESTIMATE_ACCEPTED; PITCH or LOST projects become WON automatically and require PM when none is assigned
|
|
142
142
|
create-budget --payload '<json>' (budget.createBudget; Asana deal card is on the project)
|
|
143
143
|
update-budget --payload '<json>' (budget.updateBudget; must include id)
|
|
144
144
|
delete-budget <budgetId>
|
|
@@ -203,7 +203,7 @@ Companies & projects
|
|
|
203
203
|
list-projects [--companyId] [--name] [--status] [--active true|false] [--page] [--perPage]
|
|
204
204
|
Omit --active to include both active and inactive projects.
|
|
205
205
|
get-project <id>
|
|
206
|
-
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --asanaTaskId --slackChannelId --slackChannelUrl --slackChannelName
|
|
206
|
+
create-project --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate <ISO> [--asanaTaskId] [--asanaSearch <text>] [--slackChannelId --slackChannelUrl --slackChannelName] [--pax] [--endDate <ISO>] [--description]
|
|
207
207
|
update-project --payload '<json>' (project.updateProject; optional requestQboAccountantNotification; default notify like web)
|
|
208
208
|
delete-project <id>
|
|
209
209
|
update-project-status <id> <status> [--projectManagerId <id>] [--projectManagerName <name>] [--wonOverrideReason <text>]
|
|
@@ -336,6 +336,9 @@ async function main() {
|
|
|
336
336
|
throw new Error("mark-budget-won requires <budgetId> <filePath>");
|
|
337
337
|
}
|
|
338
338
|
await markBudgetWonWithProof(budgetId, filePath, {
|
|
339
|
+
projectManagerId: getFlag(flags, "projectManagerId") !== undefined
|
|
340
|
+
? String(getFlag(flags, "projectManagerId"))
|
|
341
|
+
: undefined,
|
|
339
342
|
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
340
343
|
});
|
|
341
344
|
break;
|
|
@@ -374,6 +377,9 @@ async function main() {
|
|
|
374
377
|
throw new Error(`Invalid status. Use one of: ${budgetStatusesForHelp.join(", ")}`);
|
|
375
378
|
}
|
|
376
379
|
await updateBudgetStatus(id, status, {
|
|
380
|
+
projectManagerId: getFlag(flags, "projectManagerId") !== undefined
|
|
381
|
+
? String(getFlag(flags, "projectManagerId"))
|
|
382
|
+
: undefined,
|
|
377
383
|
markProjectWon: parseOptionalBoolFlag(flags, "markProjectWon"),
|
|
378
384
|
projectStatusOnCommercialRejection: parseCommercialRejectionProjectStatus(flags),
|
|
379
385
|
});
|
|
@@ -908,6 +914,7 @@ async function main() {
|
|
|
908
914
|
const venue = getFlag(flags, "venue");
|
|
909
915
|
const pax = getFlag(flags, "pax");
|
|
910
916
|
const asanaTaskId = getFlag(flags, "asanaTaskId");
|
|
917
|
+
const asanaSearch = getFlag(flags, "asanaSearch");
|
|
911
918
|
const slackChannelId = getFlag(flags, "slackChannelId");
|
|
912
919
|
const slackChannelUrl = getFlag(flags, "slackChannelUrl");
|
|
913
920
|
const slackChannelName = getFlag(flags, "slackChannelName");
|
|
@@ -921,12 +928,8 @@ async function main() {
|
|
|
921
928
|
!insideSalesId ||
|
|
922
929
|
!businessDevelopmentId ||
|
|
923
930
|
!venue ||
|
|
924
|
-
!asanaTaskId ||
|
|
925
|
-
!slackChannelId ||
|
|
926
|
-
!slackChannelUrl ||
|
|
927
|
-
!slackChannelName ||
|
|
928
931
|
!startDate) {
|
|
929
|
-
throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --
|
|
932
|
+
throw new Error("create-project requires --name --companyId --contactPersonId --insideSalesId --businessDevelopmentId --venue --startDate (ISO/datetime flags as appropriate); optional --asanaTaskId, --asanaSearch, --pax and --endDate");
|
|
930
933
|
}
|
|
931
934
|
await createProject({
|
|
932
935
|
name,
|
|
@@ -937,6 +940,7 @@ async function main() {
|
|
|
937
940
|
venue,
|
|
938
941
|
pax,
|
|
939
942
|
asanaTaskId,
|
|
943
|
+
asanaSearch,
|
|
940
944
|
slackChannelId,
|
|
941
945
|
slackChannelUrl,
|
|
942
946
|
slackChannelName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@go-labs-sg/bb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Budget Builder CLI for AI agents — manage budgets, bills, and claims; bill records use isClaimable=false for bills and isClaimable=true for claims.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|