@startanaicompany/crm 2.0.0 → 2.3.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.
- package/index.js +969 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -99,7 +99,7 @@ const program = new Command();
|
|
|
99
99
|
program
|
|
100
100
|
.name('saac_crm')
|
|
101
101
|
.description('AI-first CRM CLI — manage leads and API keys')
|
|
102
|
-
.version('
|
|
102
|
+
.version('2.3.0')
|
|
103
103
|
.option('--api-key <key>', 'API key (overrides SAAC_CRM_API_KEY env and config)')
|
|
104
104
|
.option('--url <url>', 'API base URL (overrides config)');
|
|
105
105
|
|
|
@@ -766,7 +766,7 @@ callsCmd
|
|
|
766
766
|
.requiredOption('--direction <dir>', 'inbound or outbound')
|
|
767
767
|
.option('--lead-id <id>', 'Lead ID associated with this call')
|
|
768
768
|
.option('--contact-id <id>', 'Contact ID associated with this call')
|
|
769
|
-
.option('--outcome <outcome>', 'connected |
|
|
769
|
+
.option('--outcome <outcome>', 'connected | no_answer | voicemail | busy | failed | callback_requested | not_interested | wrong_number | left_message_with_gatekeeper | disconnected | scheduled')
|
|
770
770
|
.option('--duration <seconds>', 'Duration in seconds')
|
|
771
771
|
.option('--notes <notes>', 'Call notes')
|
|
772
772
|
.option('--call-time <iso>', 'Call time (ISO8601, defaults to now)')
|
|
@@ -917,4 +917,971 @@ leadsStageCmd
|
|
|
917
917
|
});
|
|
918
918
|
|
|
919
919
|
|
|
920
|
+
// ============================================================
|
|
921
|
+
// MEETINGS
|
|
922
|
+
// ============================================================
|
|
923
|
+
|
|
924
|
+
const meetingsCmd = program
|
|
925
|
+
.command('meetings')
|
|
926
|
+
.description('Manage booked meetings');
|
|
927
|
+
|
|
928
|
+
meetingsCmd
|
|
929
|
+
.command('create')
|
|
930
|
+
.description('Log a new meeting')
|
|
931
|
+
.requiredOption('--title <title>', 'Meeting title')
|
|
932
|
+
.requiredOption('--scheduled-at <iso>', 'Scheduled datetime (ISO8601, e.g. 2026-03-01T14:00:00Z)')
|
|
933
|
+
.option('--contact-id <id>', 'Contact ID')
|
|
934
|
+
.option('--lead-id <id>', 'Lead ID')
|
|
935
|
+
.option('--duration <minutes>', 'Duration in minutes')
|
|
936
|
+
.option('--location <location>', 'Physical location or address')
|
|
937
|
+
.option('--video-link <url>', 'Video call link (Zoom, Meet, etc.)')
|
|
938
|
+
.option('--notes <notes>', 'Meeting notes or agenda')
|
|
939
|
+
.option('--outcome <outcome>', 'scheduled | completed | cancelled | no_show')
|
|
940
|
+
.option('--sentiment <sentiment>', 'positive | neutral | negative | unknown')
|
|
941
|
+
.action(async (opts) => {
|
|
942
|
+
const globalOpts = program.opts();
|
|
943
|
+
const client = getClient(globalOpts);
|
|
944
|
+
try {
|
|
945
|
+
const body = {
|
|
946
|
+
title: opts.title,
|
|
947
|
+
scheduled_at: opts.scheduledAt,
|
|
948
|
+
};
|
|
949
|
+
if (opts.contactId) body.contact_id = opts.contactId;
|
|
950
|
+
if (opts.leadId) body.lead_id = opts.leadId;
|
|
951
|
+
if (opts.duration) body.duration_minutes = parseInt(opts.duration);
|
|
952
|
+
if (opts.location) body.location = opts.location;
|
|
953
|
+
if (opts.videoLink) body.video_link = opts.videoLink;
|
|
954
|
+
if (opts.notes) body.notes = opts.notes;
|
|
955
|
+
if (opts.outcome) body.outcome = opts.outcome;
|
|
956
|
+
if (opts.sentiment) body.sentiment = opts.sentiment;
|
|
957
|
+
const res = await client.post('/meetings', body);
|
|
958
|
+
printJSON(res.data);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
handleError(err);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
meetingsCmd
|
|
965
|
+
.command('list')
|
|
966
|
+
.description('List meetings')
|
|
967
|
+
.option('--contact-id <id>', 'Filter by contact ID')
|
|
968
|
+
.option('--lead-id <id>', 'Filter by lead ID')
|
|
969
|
+
.option('--outcome <outcome>', 'Filter by outcome')
|
|
970
|
+
.option('--scheduled-after <iso>', 'Filter meetings scheduled after date')
|
|
971
|
+
.option('--scheduled-before <iso>', 'Filter meetings scheduled before date')
|
|
972
|
+
.option('--page <n>', 'Page number', '1')
|
|
973
|
+
.option('--per-page <n>', 'Results per page', '20')
|
|
974
|
+
.action(async (opts) => {
|
|
975
|
+
const globalOpts = program.opts();
|
|
976
|
+
const client = getClient(globalOpts);
|
|
977
|
+
try {
|
|
978
|
+
const params = { page: opts.page, per_page: opts.perPage };
|
|
979
|
+
if (opts.contactId) params.contact_id = opts.contactId;
|
|
980
|
+
if (opts.leadId) params.lead_id = opts.leadId;
|
|
981
|
+
if (opts.outcome) params.outcome = opts.outcome;
|
|
982
|
+
if (opts.scheduledAfter) params.scheduled_after = opts.scheduledAfter;
|
|
983
|
+
if (opts.scheduledBefore) params.scheduled_before = opts.scheduledBefore;
|
|
984
|
+
const res = await client.get('/meetings', { params });
|
|
985
|
+
printJSON(res.data);
|
|
986
|
+
} catch (err) {
|
|
987
|
+
handleError(err);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
meetingsCmd
|
|
992
|
+
.command('get <id>')
|
|
993
|
+
.description('Get a meeting by ID')
|
|
994
|
+
.action(async (id) => {
|
|
995
|
+
const globalOpts = program.opts();
|
|
996
|
+
const client = getClient(globalOpts);
|
|
997
|
+
try {
|
|
998
|
+
const res = await client.get(`/meetings/${id}`);
|
|
999
|
+
printJSON(res.data);
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
handleError(err);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
meetingsCmd
|
|
1006
|
+
.command('update <id>')
|
|
1007
|
+
.description('Update a meeting')
|
|
1008
|
+
.option('--title <title>', 'New title')
|
|
1009
|
+
.option('--scheduled-at <iso>', 'New scheduled time (ISO8601)')
|
|
1010
|
+
.option('--duration <minutes>', 'Duration in minutes')
|
|
1011
|
+
.option('--location <location>', 'Location')
|
|
1012
|
+
.option('--video-link <url>', 'Video link')
|
|
1013
|
+
.option('--notes <notes>', 'Notes')
|
|
1014
|
+
.option('--outcome <outcome>', 'scheduled | completed | cancelled | no_show')
|
|
1015
|
+
.action(async (id, opts) => {
|
|
1016
|
+
const globalOpts = program.opts();
|
|
1017
|
+
const client = getClient(globalOpts);
|
|
1018
|
+
try {
|
|
1019
|
+
const body = {};
|
|
1020
|
+
if (opts.title !== undefined) body.title = opts.title;
|
|
1021
|
+
if (opts.scheduledAt !== undefined) body.scheduled_at = opts.scheduledAt;
|
|
1022
|
+
if (opts.duration !== undefined) body.duration_minutes = parseInt(opts.duration);
|
|
1023
|
+
if (opts.location !== undefined) body.location = opts.location;
|
|
1024
|
+
if (opts.videoLink !== undefined) body.video_link = opts.videoLink;
|
|
1025
|
+
if (opts.notes !== undefined) body.notes = opts.notes;
|
|
1026
|
+
if (opts.outcome !== undefined) body.outcome = opts.outcome;
|
|
1027
|
+
const res = await client.patch(`/meetings/${id}`, body);
|
|
1028
|
+
printJSON(res.data);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
handleError(err);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
meetingsCmd
|
|
1035
|
+
.command('cancel <id>')
|
|
1036
|
+
.description('Cancel a meeting')
|
|
1037
|
+
.option('--reason <reason>', 'Cancellation reason')
|
|
1038
|
+
.action(async (id, opts) => {
|
|
1039
|
+
const globalOpts = program.opts();
|
|
1040
|
+
const client = getClient(globalOpts);
|
|
1041
|
+
try {
|
|
1042
|
+
const body = {};
|
|
1043
|
+
if (opts.reason) body.reason = opts.reason;
|
|
1044
|
+
const res = await client.delete(`/meetings/${id}`, { data: body });
|
|
1045
|
+
printJSON(res.data);
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
handleError(err);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
// ============================================================
|
|
1052
|
+
// EMAILS
|
|
1053
|
+
// ============================================================
|
|
1054
|
+
|
|
1055
|
+
const emailsCmd = program
|
|
1056
|
+
.command('emails')
|
|
1057
|
+
.description('Log and retrieve email interactions');
|
|
1058
|
+
|
|
1059
|
+
emailsCmd
|
|
1060
|
+
.command('log')
|
|
1061
|
+
.description('Log an email interaction (sent or received)')
|
|
1062
|
+
.requiredOption('--direction <dir>', 'sent or received')
|
|
1063
|
+
.requiredOption('--subject <subject>', 'Email subject line')
|
|
1064
|
+
.option('--contact-id <id>', 'Contact ID')
|
|
1065
|
+
.option('--lead-id <id>', 'Lead ID')
|
|
1066
|
+
.option('--body-summary <summary>', 'Body summary (max 1000 chars)')
|
|
1067
|
+
.option('--thread-id <id>', 'Thread ID for grouping related emails')
|
|
1068
|
+
.option('--email-timestamp <iso>', 'When the email was sent/received (ISO8601, defaults to now)')
|
|
1069
|
+
.action(async (opts) => {
|
|
1070
|
+
const globalOpts = program.opts();
|
|
1071
|
+
const client = getClient(globalOpts);
|
|
1072
|
+
try {
|
|
1073
|
+
const body = {
|
|
1074
|
+
direction: opts.direction,
|
|
1075
|
+
subject: opts.subject,
|
|
1076
|
+
};
|
|
1077
|
+
if (opts.contactId) body.contact_id = opts.contactId;
|
|
1078
|
+
if (opts.leadId) body.lead_id = opts.leadId;
|
|
1079
|
+
if (opts.bodySummary) body.body_summary = opts.bodySummary;
|
|
1080
|
+
if (opts.threadId) body.thread_id = opts.threadId;
|
|
1081
|
+
if (opts.emailTimestamp) body.email_timestamp = opts.emailTimestamp;
|
|
1082
|
+
const res = await client.post('/emails', body);
|
|
1083
|
+
printJSON(res.data);
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
handleError(err);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
emailsCmd
|
|
1090
|
+
.command('list')
|
|
1091
|
+
.description('List email logs')
|
|
1092
|
+
.option('--contact-id <id>', 'Filter by contact ID')
|
|
1093
|
+
.option('--lead-id <id>', 'Filter by lead ID')
|
|
1094
|
+
.option('--direction <dir>', 'Filter by direction (sent/received)')
|
|
1095
|
+
.option('--thread-id <id>', 'Filter by thread ID')
|
|
1096
|
+
.option('--after <iso>', 'Filter emails after this date')
|
|
1097
|
+
.option('--before <iso>', 'Filter emails before this date')
|
|
1098
|
+
.option('--page <n>', 'Page number', '1')
|
|
1099
|
+
.option('--per-page <n>', 'Results per page', '20')
|
|
1100
|
+
.action(async (opts) => {
|
|
1101
|
+
const globalOpts = program.opts();
|
|
1102
|
+
const client = getClient(globalOpts);
|
|
1103
|
+
try {
|
|
1104
|
+
const params = { page: opts.page, per_page: opts.perPage };
|
|
1105
|
+
if (opts.contactId) params.contact_id = opts.contactId;
|
|
1106
|
+
if (opts.leadId) params.lead_id = opts.leadId;
|
|
1107
|
+
if (opts.direction) params.direction = opts.direction;
|
|
1108
|
+
if (opts.threadId) params.thread_id = opts.threadId;
|
|
1109
|
+
if (opts.after) params.after = opts.after;
|
|
1110
|
+
if (opts.before) params.before = opts.before;
|
|
1111
|
+
const res = await client.get('/emails', { params });
|
|
1112
|
+
printJSON(res.data);
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
handleError(err);
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
emailsCmd
|
|
1119
|
+
.command('get <id>')
|
|
1120
|
+
.description('Get an email log entry by ID')
|
|
1121
|
+
.action(async (id) => {
|
|
1122
|
+
const globalOpts = program.opts();
|
|
1123
|
+
const client = getClient(globalOpts);
|
|
1124
|
+
try {
|
|
1125
|
+
const res = await client.get(`/emails/${id}`);
|
|
1126
|
+
printJSON(res.data);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
handleError(err);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
emailsCmd
|
|
1133
|
+
.command('thread <thread-id>')
|
|
1134
|
+
.description('Get all emails in a thread by thread ID')
|
|
1135
|
+
.action(async (threadId) => {
|
|
1136
|
+
const globalOpts = program.opts();
|
|
1137
|
+
const client = getClient(globalOpts);
|
|
1138
|
+
try {
|
|
1139
|
+
const res = await client.get(`/emails/thread/${threadId}`);
|
|
1140
|
+
printJSON(res.data);
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
handleError(err);
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
// ============================================================
|
|
1149
|
+
// QUOTES commands
|
|
1150
|
+
// ============================================================
|
|
1151
|
+
const quotesCmd = program.command('quotes').description('Manage quotes');
|
|
1152
|
+
|
|
1153
|
+
quotesCmd
|
|
1154
|
+
.command('create')
|
|
1155
|
+
.description('Create a new quote')
|
|
1156
|
+
.requiredOption('--title <title>', 'Quote title')
|
|
1157
|
+
.option('--lead-id <id>', 'Lead ID to link')
|
|
1158
|
+
.option('--contact-id <id>', 'Contact ID to link')
|
|
1159
|
+
.option('--currency <code>', 'Currency code (default: USD)')
|
|
1160
|
+
.option('--validity-date <date>', 'Validity date (YYYY-MM-DD)')
|
|
1161
|
+
.option('--notes <notes>', 'Notes')
|
|
1162
|
+
.action(async (opts) => {
|
|
1163
|
+
const globalOpts = program.opts();
|
|
1164
|
+
const client = getClient(globalOpts);
|
|
1165
|
+
try {
|
|
1166
|
+
|
|
1167
|
+
const res = await client.post('/quotes', {
|
|
1168
|
+
title: opts.title,
|
|
1169
|
+
lead_id: opts.leadId,
|
|
1170
|
+
contact_id: opts.contactId,
|
|
1171
|
+
currency: opts.currency,
|
|
1172
|
+
validity_date: opts.validityDate,
|
|
1173
|
+
notes: opts.notes
|
|
1174
|
+
});
|
|
1175
|
+
printJSON(res.data);
|
|
1176
|
+
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
handleError(err);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
quotesCmd
|
|
1183
|
+
.command('list')
|
|
1184
|
+
.description('List quotes')
|
|
1185
|
+
.option('--status <status>', 'Filter by status (draft/sent/accepted/rejected/expired)')
|
|
1186
|
+
.option('--lead-id <id>', 'Filter by lead ID')
|
|
1187
|
+
.option('--contact-id <id>', 'Filter by contact ID')
|
|
1188
|
+
.action(async (opts) => {
|
|
1189
|
+
const globalOpts = program.opts();
|
|
1190
|
+
const client = getClient(globalOpts);
|
|
1191
|
+
try {
|
|
1192
|
+
|
|
1193
|
+
const params = {};
|
|
1194
|
+
if (opts.status) params.status = opts.status;
|
|
1195
|
+
if (opts.leadId) params.lead_id = opts.leadId;
|
|
1196
|
+
if (opts.contactId) params.contact_id = opts.contactId;
|
|
1197
|
+
const res = await client.get('/quotes', { params });
|
|
1198
|
+
printJSON(res.data);
|
|
1199
|
+
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
handleError(err);
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
quotesCmd
|
|
1206
|
+
.command('get <id>')
|
|
1207
|
+
.description('Get a quote with all line items and totals')
|
|
1208
|
+
.action(async (id) => {
|
|
1209
|
+
const globalOpts = program.opts();
|
|
1210
|
+
const client = getClient(globalOpts);
|
|
1211
|
+
try {
|
|
1212
|
+
|
|
1213
|
+
const res = await client.get(`/quotes/${id}`);
|
|
1214
|
+
printJSON(res.data);
|
|
1215
|
+
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
handleError(err);
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
quotesCmd
|
|
1222
|
+
.command('update <id>')
|
|
1223
|
+
.description('Update quote metadata')
|
|
1224
|
+
.option('--title <title>', 'New title')
|
|
1225
|
+
.option('--currency <code>', 'Currency code')
|
|
1226
|
+
.option('--validity-date <date>', 'Validity date (YYYY-MM-DD)')
|
|
1227
|
+
.option('--notes <notes>', 'Notes')
|
|
1228
|
+
.action(async (id, opts) => {
|
|
1229
|
+
const globalOpts = program.opts();
|
|
1230
|
+
const client = getClient(globalOpts);
|
|
1231
|
+
try {
|
|
1232
|
+
|
|
1233
|
+
const body = {};
|
|
1234
|
+
if (opts.title) body.title = opts.title;
|
|
1235
|
+
if (opts.currency) body.currency = opts.currency;
|
|
1236
|
+
if (opts.validityDate) body.validity_date = opts.validityDate;
|
|
1237
|
+
if (opts.notes) body.notes = opts.notes;
|
|
1238
|
+
const res = await client.patch(`/quotes/${id}`, body);
|
|
1239
|
+
printJSON(res.data);
|
|
1240
|
+
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
handleError(err);
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
quotesCmd
|
|
1247
|
+
.command('add-line <quote-id>')
|
|
1248
|
+
.description('Add a line item to a quote')
|
|
1249
|
+
.requiredOption('--description <desc>', 'Line item description')
|
|
1250
|
+
.requiredOption('--unit-price <price>', 'Unit price', parseFloat)
|
|
1251
|
+
.option('--quantity <qty>', 'Quantity (default: 1)', parseFloat)
|
|
1252
|
+
.option('--discount <pct>', 'Discount percentage 0-100 (default: 0)', parseFloat)
|
|
1253
|
+
.option('--sort-order <n>', 'Sort order (default: 0)', parseInt)
|
|
1254
|
+
.action(async (quoteId, opts) => {
|
|
1255
|
+
const globalOpts = program.opts();
|
|
1256
|
+
const client = getClient(globalOpts);
|
|
1257
|
+
try {
|
|
1258
|
+
|
|
1259
|
+
const res = await client.post(`/quotes/${quoteId}/lines`, {
|
|
1260
|
+
description: opts.description,
|
|
1261
|
+
unit_price: opts.unitPrice,
|
|
1262
|
+
quantity: opts.quantity || 1,
|
|
1263
|
+
discount_percentage: opts.discount || 0,
|
|
1264
|
+
sort_order: opts.sortOrder || 0
|
|
1265
|
+
});
|
|
1266
|
+
printJSON(res.data);
|
|
1267
|
+
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
handleError(err);
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
quotesCmd
|
|
1274
|
+
.command('remove-line <quote-id> <line-id>')
|
|
1275
|
+
.description('Remove a line item from a quote')
|
|
1276
|
+
.action(async (quoteId, lineId) => {
|
|
1277
|
+
const globalOpts = program.opts();
|
|
1278
|
+
const client = getClient(globalOpts);
|
|
1279
|
+
try {
|
|
1280
|
+
|
|
1281
|
+
const res = await client.delete(`/quotes/${quoteId}/lines/${lineId}`);
|
|
1282
|
+
printJSON(res.data);
|
|
1283
|
+
|
|
1284
|
+
} catch (err) {
|
|
1285
|
+
handleError(err);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
quotesCmd
|
|
1290
|
+
.command('send <id>')
|
|
1291
|
+
.description('Mark a quote as sent')
|
|
1292
|
+
.action(async (id) => {
|
|
1293
|
+
const globalOpts = program.opts();
|
|
1294
|
+
const client = getClient(globalOpts);
|
|
1295
|
+
try {
|
|
1296
|
+
|
|
1297
|
+
const res = await client.post(`/quotes/${id}/status`, { status: 'sent' });
|
|
1298
|
+
printJSON(res.data);
|
|
1299
|
+
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
handleError(err);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
quotesCmd
|
|
1306
|
+
.command('accept <id>')
|
|
1307
|
+
.description('Mark a quote as accepted (auto-creates draft contract)')
|
|
1308
|
+
.action(async (id) => {
|
|
1309
|
+
const globalOpts = program.opts();
|
|
1310
|
+
const client = getClient(globalOpts);
|
|
1311
|
+
try {
|
|
1312
|
+
|
|
1313
|
+
const res = await client.post(`/quotes/${id}/status`, { status: 'accepted' });
|
|
1314
|
+
printJSON(res.data);
|
|
1315
|
+
|
|
1316
|
+
} catch (err) {
|
|
1317
|
+
handleError(err);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
quotesCmd
|
|
1322
|
+
.command('reject <id>')
|
|
1323
|
+
.description('Mark a quote as rejected')
|
|
1324
|
+
.action(async (id) => {
|
|
1325
|
+
const globalOpts = program.opts();
|
|
1326
|
+
const client = getClient(globalOpts);
|
|
1327
|
+
try {
|
|
1328
|
+
|
|
1329
|
+
const res = await client.post(`/quotes/${id}/status`, { status: 'rejected' });
|
|
1330
|
+
printJSON(res.data);
|
|
1331
|
+
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
handleError(err);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
quotesCmd
|
|
1338
|
+
.command('version <id>')
|
|
1339
|
+
.description('Create a new version of a quote (copies all line items)')
|
|
1340
|
+
.action(async (id) => {
|
|
1341
|
+
const globalOpts = program.opts();
|
|
1342
|
+
const client = getClient(globalOpts);
|
|
1343
|
+
try {
|
|
1344
|
+
|
|
1345
|
+
const res = await client.post(`/quotes/${id}/version`);
|
|
1346
|
+
printJSON(res.data);
|
|
1347
|
+
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
handleError(err);
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
quotesCmd
|
|
1354
|
+
.command('delete <id>')
|
|
1355
|
+
.description('Soft-delete a quote')
|
|
1356
|
+
.action(async (id) => {
|
|
1357
|
+
const globalOpts = program.opts();
|
|
1358
|
+
const client = getClient(globalOpts);
|
|
1359
|
+
try {
|
|
1360
|
+
|
|
1361
|
+
const res = await client.delete(`/quotes/${id}`);
|
|
1362
|
+
printJSON(res.data);
|
|
1363
|
+
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
handleError(err);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
// ============================================================
|
|
1370
|
+
// CONTRACTS commands
|
|
1371
|
+
// ============================================================
|
|
1372
|
+
const contractsCmd = program.command('contracts').description('Manage contracts');
|
|
1373
|
+
|
|
1374
|
+
contractsCmd
|
|
1375
|
+
.command('create')
|
|
1376
|
+
.description('Create a new contract')
|
|
1377
|
+
.requiredOption('--title <title>', 'Contract title')
|
|
1378
|
+
.option('--lead-id <id>', 'Lead ID to link')
|
|
1379
|
+
.option('--contact-id <id>', 'Contact ID to link')
|
|
1380
|
+
.option('--quote-id <id>', 'Quote ID to link')
|
|
1381
|
+
.option('--value <amount>', 'Contract value', parseFloat)
|
|
1382
|
+
.option('--currency <code>', 'Currency code (default: USD)')
|
|
1383
|
+
.option('--start-date <date>', 'Start date (YYYY-MM-DD)')
|
|
1384
|
+
.option('--end-date <date>', 'End date (YYYY-MM-DD)')
|
|
1385
|
+
.option('--document-url <url>', 'Document URL reference')
|
|
1386
|
+
.option('--notes <notes>', 'Notes')
|
|
1387
|
+
.action(async (opts) => {
|
|
1388
|
+
const globalOpts = program.opts();
|
|
1389
|
+
const client = getClient(globalOpts);
|
|
1390
|
+
try {
|
|
1391
|
+
|
|
1392
|
+
const res = await client.post('/contracts', {
|
|
1393
|
+
title: opts.title,
|
|
1394
|
+
lead_id: opts.leadId,
|
|
1395
|
+
contact_id: opts.contactId,
|
|
1396
|
+
quote_id: opts.quoteId,
|
|
1397
|
+
value: opts.value,
|
|
1398
|
+
currency: opts.currency,
|
|
1399
|
+
start_date: opts.startDate,
|
|
1400
|
+
end_date: opts.endDate,
|
|
1401
|
+
document_url: opts.documentUrl,
|
|
1402
|
+
notes: opts.notes
|
|
1403
|
+
});
|
|
1404
|
+
printJSON(res.data);
|
|
1405
|
+
|
|
1406
|
+
} catch (err) {
|
|
1407
|
+
handleError(err);
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
contractsCmd
|
|
1412
|
+
.command('list')
|
|
1413
|
+
.description('List contracts')
|
|
1414
|
+
.option('--status <status>', 'Filter by status (draft/sent/signed/expired/void)')
|
|
1415
|
+
.option('--lead-id <id>', 'Filter by lead ID')
|
|
1416
|
+
.option('--contact-id <id>', 'Filter by contact ID')
|
|
1417
|
+
.option('--quote-id <id>', 'Filter by quote ID')
|
|
1418
|
+
.action(async (opts) => {
|
|
1419
|
+
const globalOpts = program.opts();
|
|
1420
|
+
const client = getClient(globalOpts);
|
|
1421
|
+
try {
|
|
1422
|
+
|
|
1423
|
+
const params = {};
|
|
1424
|
+
if (opts.status) params.status = opts.status;
|
|
1425
|
+
if (opts.leadId) params.lead_id = opts.leadId;
|
|
1426
|
+
if (opts.contactId) params.contact_id = opts.contactId;
|
|
1427
|
+
if (opts.quoteId) params.quote_id = opts.quoteId;
|
|
1428
|
+
const res = await client.get('/contracts', { params });
|
|
1429
|
+
printJSON(res.data);
|
|
1430
|
+
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
handleError(err);
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
contractsCmd
|
|
1437
|
+
.command('get <id>')
|
|
1438
|
+
.description('Get a contract by ID')
|
|
1439
|
+
.action(async (id) => {
|
|
1440
|
+
const globalOpts = program.opts();
|
|
1441
|
+
const client = getClient(globalOpts);
|
|
1442
|
+
try {
|
|
1443
|
+
|
|
1444
|
+
const res = await client.get(`/contracts/${id}`);
|
|
1445
|
+
printJSON(res.data);
|
|
1446
|
+
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
handleError(err);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
contractsCmd
|
|
1453
|
+
.command('update <id>')
|
|
1454
|
+
.description('Update contract metadata')
|
|
1455
|
+
.option('--title <title>', 'New title')
|
|
1456
|
+
.option('--value <amount>', 'Contract value', parseFloat)
|
|
1457
|
+
.option('--currency <code>', 'Currency code')
|
|
1458
|
+
.option('--start-date <date>', 'Start date')
|
|
1459
|
+
.option('--end-date <date>', 'End date')
|
|
1460
|
+
.option('--notes <notes>', 'Notes')
|
|
1461
|
+
.action(async (id, opts) => {
|
|
1462
|
+
const globalOpts = program.opts();
|
|
1463
|
+
const client = getClient(globalOpts);
|
|
1464
|
+
try {
|
|
1465
|
+
|
|
1466
|
+
const body = {};
|
|
1467
|
+
if (opts.title) body.title = opts.title;
|
|
1468
|
+
if (opts.value !== undefined) body.value = opts.value;
|
|
1469
|
+
if (opts.currency) body.currency = opts.currency;
|
|
1470
|
+
if (opts.startDate) body.start_date = opts.startDate;
|
|
1471
|
+
if (opts.endDate) body.end_date = opts.endDate;
|
|
1472
|
+
if (opts.notes) body.notes = opts.notes;
|
|
1473
|
+
const res = await client.patch(`/contracts/${id}`, body);
|
|
1474
|
+
printJSON(res.data);
|
|
1475
|
+
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
handleError(err);
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
contractsCmd
|
|
1482
|
+
.command('send <id>')
|
|
1483
|
+
.description('Mark a contract as sent')
|
|
1484
|
+
.action(async (id) => {
|
|
1485
|
+
const globalOpts = program.opts();
|
|
1486
|
+
const client = getClient(globalOpts);
|
|
1487
|
+
try {
|
|
1488
|
+
|
|
1489
|
+
const res = await client.post(`/contracts/${id}/status`, { status: 'sent' });
|
|
1490
|
+
printJSON(res.data);
|
|
1491
|
+
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
handleError(err);
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
contractsCmd
|
|
1498
|
+
.command('sign <id>')
|
|
1499
|
+
.description('Sign a contract (auto-moves linked lead to closed_won)')
|
|
1500
|
+
.option('--signed-by <name>', 'Name of signatory')
|
|
1501
|
+
.action(async (id, opts) => {
|
|
1502
|
+
const globalOpts = program.opts();
|
|
1503
|
+
const client = getClient(globalOpts);
|
|
1504
|
+
try {
|
|
1505
|
+
|
|
1506
|
+
const res = await client.post(`/contracts/${id}/status`, { status: 'signed', signed_by: opts.signedBy });
|
|
1507
|
+
printJSON(res.data);
|
|
1508
|
+
|
|
1509
|
+
} catch (err) {
|
|
1510
|
+
handleError(err);
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
contractsCmd
|
|
1515
|
+
.command('void <id>')
|
|
1516
|
+
.description('Void a contract')
|
|
1517
|
+
.action(async (id) => {
|
|
1518
|
+
const globalOpts = program.opts();
|
|
1519
|
+
const client = getClient(globalOpts);
|
|
1520
|
+
try {
|
|
1521
|
+
|
|
1522
|
+
const res = await client.post(`/contracts/${id}/status`, { status: 'void' });
|
|
1523
|
+
printJSON(res.data);
|
|
1524
|
+
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
handleError(err);
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
contractsCmd
|
|
1531
|
+
.command('attach-doc <id>')
|
|
1532
|
+
.description('Attach or update document URL on a contract')
|
|
1533
|
+
.requiredOption('--url <url>', 'Document URL')
|
|
1534
|
+
.action(async (id, opts) => {
|
|
1535
|
+
const globalOpts = program.opts();
|
|
1536
|
+
const client = getClient(globalOpts);
|
|
1537
|
+
try {
|
|
1538
|
+
|
|
1539
|
+
const res = await client.patch(`/contracts/${id}/document`, { document_url: opts.url });
|
|
1540
|
+
printJSON(res.data);
|
|
1541
|
+
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
handleError(err);
|
|
1544
|
+
}
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
contractsCmd
|
|
1548
|
+
.command('delete <id>')
|
|
1549
|
+
.description('Soft-delete a contract')
|
|
1550
|
+
.action(async (id) => {
|
|
1551
|
+
const globalOpts = program.opts();
|
|
1552
|
+
const client = getClient(globalOpts);
|
|
1553
|
+
try {
|
|
1554
|
+
|
|
1555
|
+
const res = await client.delete(`/contracts/${id}`);
|
|
1556
|
+
printJSON(res.data);
|
|
1557
|
+
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
handleError(err);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
|
|
1564
|
+
// ============================================================
|
|
1565
|
+
// BUDGETS commands — Sprint 5
|
|
1566
|
+
// ============================================================
|
|
1567
|
+
const budgetsCmd = program.command('budgets').description('Manage sales budgets');
|
|
1568
|
+
|
|
1569
|
+
budgetsCmd
|
|
1570
|
+
.command('create')
|
|
1571
|
+
.description('Create a new budget allocation')
|
|
1572
|
+
.requiredOption('--name <name>', 'Budget name')
|
|
1573
|
+
.requiredOption('--amount <amount>', 'Total budget amount', parseFloat)
|
|
1574
|
+
.requiredOption('--period-start <date>', 'Period start date (YYYY-MM-DD)')
|
|
1575
|
+
.requiredOption('--period-end <date>', 'Period end date (YYYY-MM-DD)')
|
|
1576
|
+
.option('--period-type <type>', 'Period type: monthly, quarterly, annual, custom (default: custom)')
|
|
1577
|
+
.option('--currency <code>', 'Currency code (default: USD)')
|
|
1578
|
+
.option('--owner-key <key>', 'API key of budget owner')
|
|
1579
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
1580
|
+
.action(async (opts) => {
|
|
1581
|
+
const globalOpts = program.opts();
|
|
1582
|
+
const client = getClient(globalOpts);
|
|
1583
|
+
try {
|
|
1584
|
+
const res = await client.post('/budgets', {
|
|
1585
|
+
name: opts.name,
|
|
1586
|
+
total_amount: opts.amount,
|
|
1587
|
+
period_start: opts.periodStart,
|
|
1588
|
+
period_end: opts.periodEnd,
|
|
1589
|
+
period_type: opts.periodType || 'custom',
|
|
1590
|
+
currency: opts.currency,
|
|
1591
|
+
owner_key: opts.ownerKey,
|
|
1592
|
+
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : undefined
|
|
1593
|
+
});
|
|
1594
|
+
printJSON(res.data);
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
handleError(err);
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
|
|
1600
|
+
budgetsCmd
|
|
1601
|
+
.command('list')
|
|
1602
|
+
.description('List budgets with spend summary')
|
|
1603
|
+
.option('--status <status>', 'Filter by status (active/paused/exhausted/archived)')
|
|
1604
|
+
.option('--period-type <type>', 'Filter by period type')
|
|
1605
|
+
.action(async (opts) => {
|
|
1606
|
+
const globalOpts = program.opts();
|
|
1607
|
+
const client = getClient(globalOpts);
|
|
1608
|
+
try {
|
|
1609
|
+
const params = {};
|
|
1610
|
+
if (opts.status) params.status = opts.status;
|
|
1611
|
+
if (opts.periodType) params.period_type = opts.periodType;
|
|
1612
|
+
const res = await client.get('/budgets', { params });
|
|
1613
|
+
printJSON(res.data);
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
handleError(err);
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
budgetsCmd
|
|
1620
|
+
.command('get <id>')
|
|
1621
|
+
.description('Get a budget with line items and spend summary')
|
|
1622
|
+
.action(async (id) => {
|
|
1623
|
+
const globalOpts = program.opts();
|
|
1624
|
+
const client = getClient(globalOpts);
|
|
1625
|
+
try {
|
|
1626
|
+
const res = await client.get(`/budgets/${id}`);
|
|
1627
|
+
printJSON(res.data);
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
handleError(err);
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
budgetsCmd
|
|
1634
|
+
.command('update <id>')
|
|
1635
|
+
.description('Update budget metadata')
|
|
1636
|
+
.option('--name <name>', 'New name')
|
|
1637
|
+
.option('--amount <amount>', 'New total amount', parseFloat)
|
|
1638
|
+
.option('--status <status>', 'New status (active/paused/exhausted/archived)')
|
|
1639
|
+
.option('--period-start <date>', 'New period start')
|
|
1640
|
+
.option('--period-end <date>', 'New period end')
|
|
1641
|
+
.option('--currency <code>', 'Currency code')
|
|
1642
|
+
.action(async (id, opts) => {
|
|
1643
|
+
const globalOpts = program.opts();
|
|
1644
|
+
const client = getClient(globalOpts);
|
|
1645
|
+
try {
|
|
1646
|
+
const body = {};
|
|
1647
|
+
if (opts.name) body.name = opts.name;
|
|
1648
|
+
if (opts.amount !== undefined) body.total_amount = opts.amount;
|
|
1649
|
+
if (opts.status) body.status = opts.status;
|
|
1650
|
+
if (opts.periodStart) body.period_start = opts.periodStart;
|
|
1651
|
+
if (opts.periodEnd) body.period_end = opts.periodEnd;
|
|
1652
|
+
if (opts.currency) body.currency = opts.currency;
|
|
1653
|
+
const res = await client.patch(`/budgets/${id}`, body);
|
|
1654
|
+
printJSON(res.data);
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
handleError(err);
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
budgetsCmd
|
|
1661
|
+
.command('spend <id>')
|
|
1662
|
+
.description('Log a spend event against a budget')
|
|
1663
|
+
.requiredOption('--description <desc>', 'What was purchased')
|
|
1664
|
+
.requiredOption('--amount <amount>', 'Spend amount', parseFloat)
|
|
1665
|
+
.requiredOption('--date <date>', 'Transaction date (YYYY-MM-DD)')
|
|
1666
|
+
.option('--category <cat>', 'Category: data_enrichment, advertising, events, tools, other (default: other)')
|
|
1667
|
+
.option('--lead-id <id>', 'Link to a lead')
|
|
1668
|
+
.option('--reference <ref>', 'Invoice or order reference')
|
|
1669
|
+
.action(async (id, opts) => {
|
|
1670
|
+
const globalOpts = program.opts();
|
|
1671
|
+
const client = getClient(globalOpts);
|
|
1672
|
+
try {
|
|
1673
|
+
const res = await client.post(`/budgets/${id}/spend`, {
|
|
1674
|
+
description: opts.description,
|
|
1675
|
+
amount: opts.amount,
|
|
1676
|
+
transaction_date: opts.date,
|
|
1677
|
+
category: opts.category || 'other',
|
|
1678
|
+
lead_id: opts.leadId,
|
|
1679
|
+
reference: opts.reference
|
|
1680
|
+
});
|
|
1681
|
+
printJSON(res.data);
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
handleError(err);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
budgetsCmd
|
|
1688
|
+
.command('line-items <id>')
|
|
1689
|
+
.description('List spend line items for a budget')
|
|
1690
|
+
.option('--category <cat>', 'Filter by category')
|
|
1691
|
+
.action(async (id, opts) => {
|
|
1692
|
+
const globalOpts = program.opts();
|
|
1693
|
+
const client = getClient(globalOpts);
|
|
1694
|
+
try {
|
|
1695
|
+
const params = {};
|
|
1696
|
+
if (opts.category) params.category = opts.category;
|
|
1697
|
+
const res = await client.get(`/budgets/${id}/line-items`, { params });
|
|
1698
|
+
printJSON(res.data);
|
|
1699
|
+
} catch (err) {
|
|
1700
|
+
handleError(err);
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
budgetsCmd
|
|
1705
|
+
.command('check <id>')
|
|
1706
|
+
.description('Check if a spend amount is approved (APPROVED/INSUFFICIENT_FUNDS)')
|
|
1707
|
+
.requiredOption('--amount <amount>', 'Amount to check', parseFloat)
|
|
1708
|
+
.action(async (id, opts) => {
|
|
1709
|
+
const globalOpts = program.opts();
|
|
1710
|
+
const client = getClient(globalOpts);
|
|
1711
|
+
try {
|
|
1712
|
+
const res = await client.get(`/budgets/${id}/check`, { params: { amount: opts.amount } });
|
|
1713
|
+
printJSON(res.data);
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
handleError(err);
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
// ============================================================
|
|
1720
|
+
// TARGETS commands — Sprint 5
|
|
1721
|
+
// ============================================================
|
|
1722
|
+
const targetsCmd = program.command('targets').description('Manage sales targets');
|
|
1723
|
+
|
|
1724
|
+
targetsCmd
|
|
1725
|
+
.command('create')
|
|
1726
|
+
.description('Create a new sales target')
|
|
1727
|
+
.requiredOption('--name <name>', 'Target name')
|
|
1728
|
+
.requiredOption('--metric <metric>', 'Metric: calls_made, emails_sent, meetings_booked, leads_qualified, revenue_closed, deals_closed, contracts_signed, or any custom')
|
|
1729
|
+
.requiredOption('--goal <value>', 'Goal value', parseFloat)
|
|
1730
|
+
.requiredOption('--period-start <date>', 'Period start date (YYYY-MM-DD)')
|
|
1731
|
+
.requiredOption('--period-end <date>', 'Period end date (YYYY-MM-DD)')
|
|
1732
|
+
.option('--type <type>', 'Target type: activity, revenue, pipeline, conversion (default: activity)')
|
|
1733
|
+
.option('--period-type <type>', 'Period type: daily, weekly, monthly, quarterly, annual (default: monthly)')
|
|
1734
|
+
.option('--unit <unit>', 'Unit label: calls, USD, leads, % (default: count)')
|
|
1735
|
+
.option('--owner-key <key>', 'API key owner of this target')
|
|
1736
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
1737
|
+
.action(async (opts) => {
|
|
1738
|
+
const globalOpts = program.opts();
|
|
1739
|
+
const client = getClient(globalOpts);
|
|
1740
|
+
try {
|
|
1741
|
+
const res = await client.post('/targets', {
|
|
1742
|
+
name: opts.name,
|
|
1743
|
+
metric: opts.metric,
|
|
1744
|
+
goal_value: opts.goal,
|
|
1745
|
+
period_start: opts.periodStart,
|
|
1746
|
+
period_end: opts.periodEnd,
|
|
1747
|
+
target_type: opts.type || 'activity',
|
|
1748
|
+
period_type: opts.periodType || 'monthly',
|
|
1749
|
+
unit: opts.unit || 'count',
|
|
1750
|
+
owner_key: opts.ownerKey,
|
|
1751
|
+
tags: opts.tags ? opts.tags.split(',').map(t => t.trim()) : undefined
|
|
1752
|
+
});
|
|
1753
|
+
printJSON(res.data);
|
|
1754
|
+
} catch (err) {
|
|
1755
|
+
handleError(err);
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
targetsCmd
|
|
1760
|
+
.command('list')
|
|
1761
|
+
.description('List targets with live-computed actuals and status')
|
|
1762
|
+
.option('--status <status>', 'Filter by status: on_track, at_risk, behind, achieved, failed')
|
|
1763
|
+
.option('--type <type>', 'Filter by target_type')
|
|
1764
|
+
.option('--metric <metric>', 'Filter by metric')
|
|
1765
|
+
.option('--owner-key <key>', 'Filter by owner API key')
|
|
1766
|
+
.action(async (opts) => {
|
|
1767
|
+
const globalOpts = program.opts();
|
|
1768
|
+
const client = getClient(globalOpts);
|
|
1769
|
+
try {
|
|
1770
|
+
const params = {};
|
|
1771
|
+
if (opts.status) params.status = opts.status;
|
|
1772
|
+
if (opts.type) params.target_type = opts.type;
|
|
1773
|
+
if (opts.metric) params.metric = opts.metric;
|
|
1774
|
+
if (opts.ownerKey) params.owner_key = opts.ownerKey;
|
|
1775
|
+
const res = await client.get('/targets', { params });
|
|
1776
|
+
printJSON(res.data);
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
handleError(err);
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
targetsCmd
|
|
1783
|
+
.command('get <id>')
|
|
1784
|
+
.description('Get a target with full breakdown: current, remaining, attainment %, days remaining')
|
|
1785
|
+
.action(async (id) => {
|
|
1786
|
+
const globalOpts = program.opts();
|
|
1787
|
+
const client = getClient(globalOpts);
|
|
1788
|
+
try {
|
|
1789
|
+
const res = await client.get(`/targets/${id}`);
|
|
1790
|
+
printJSON(res.data);
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
handleError(err);
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
targetsCmd
|
|
1797
|
+
.command('update <id>')
|
|
1798
|
+
.description('Update a target')
|
|
1799
|
+
.option('--name <name>', 'New name')
|
|
1800
|
+
.option('--goal <value>', 'New goal value', parseFloat)
|
|
1801
|
+
.option('--period-start <date>', 'New period start')
|
|
1802
|
+
.option('--period-end <date>', 'New period end')
|
|
1803
|
+
.option('--unit <unit>', 'New unit label')
|
|
1804
|
+
.option('--owner-key <key>', 'New owner API key')
|
|
1805
|
+
.action(async (id, opts) => {
|
|
1806
|
+
const globalOpts = program.opts();
|
|
1807
|
+
const client = getClient(globalOpts);
|
|
1808
|
+
try {
|
|
1809
|
+
const body = {};
|
|
1810
|
+
if (opts.name) body.name = opts.name;
|
|
1811
|
+
if (opts.goal !== undefined) body.goal_value = opts.goal;
|
|
1812
|
+
if (opts.periodStart) body.period_start = opts.periodStart;
|
|
1813
|
+
if (opts.periodEnd) body.period_end = opts.periodEnd;
|
|
1814
|
+
if (opts.unit) body.unit = opts.unit;
|
|
1815
|
+
if (opts.ownerKey) body.owner_key = opts.ownerKey;
|
|
1816
|
+
const res = await client.patch(`/targets/${id}`, body);
|
|
1817
|
+
printJSON(res.data);
|
|
1818
|
+
} catch (err) {
|
|
1819
|
+
handleError(err);
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
targetsCmd
|
|
1824
|
+
.command('progress <id>')
|
|
1825
|
+
.description('Log manual progress toward a target')
|
|
1826
|
+
.requiredOption('--value <n>', 'Progress value to add', parseFloat)
|
|
1827
|
+
.option('--source <src>', 'Source: manual, auto_calls, auto_emails, auto_meetings, auto_leads (default: manual)')
|
|
1828
|
+
.option('--note <note>', 'Context note')
|
|
1829
|
+
.option('--logged-at <iso>', 'When this progress happened (ISO8601, defaults to now)')
|
|
1830
|
+
.action(async (id, opts) => {
|
|
1831
|
+
const globalOpts = program.opts();
|
|
1832
|
+
const client = getClient(globalOpts);
|
|
1833
|
+
try {
|
|
1834
|
+
const res = await client.post(`/targets/${id}/progress`, {
|
|
1835
|
+
value: opts.value,
|
|
1836
|
+
source: opts.source || 'manual',
|
|
1837
|
+
note: opts.note,
|
|
1838
|
+
logged_at: opts.loggedAt
|
|
1839
|
+
});
|
|
1840
|
+
printJSON(res.data);
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
handleError(err);
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
targetsCmd
|
|
1847
|
+
.command('dashboard')
|
|
1848
|
+
.description('Show all active targets with attainment % and status summary')
|
|
1849
|
+
.option('--owner-key <key>', 'Filter by owner API key')
|
|
1850
|
+
.action(async (opts) => {
|
|
1851
|
+
const globalOpts = program.opts();
|
|
1852
|
+
const client = getClient(globalOpts);
|
|
1853
|
+
try {
|
|
1854
|
+
const params = {};
|
|
1855
|
+
if (opts.ownerKey) params.owner_key = opts.ownerKey;
|
|
1856
|
+
const res = await client.get('/targets/dashboard', { params });
|
|
1857
|
+
printJSON(res.data);
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
handleError(err);
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
// ============================================================
|
|
1864
|
+
// PERFORMANCE REPORT command — Sprint 5
|
|
1865
|
+
// ============================================================
|
|
1866
|
+
program
|
|
1867
|
+
.command('performance')
|
|
1868
|
+
.description('Combined performance report: all targets + budgets in one JSON response')
|
|
1869
|
+
.option('--period-start <date>', 'Filter from date (YYYY-MM-DD)')
|
|
1870
|
+
.option('--period-end <date>', 'Filter to date (YYYY-MM-DD)')
|
|
1871
|
+
.option('--owner-key <key>', 'Filter by owner API key')
|
|
1872
|
+
.action(async (opts) => {
|
|
1873
|
+
const globalOpts = program.opts();
|
|
1874
|
+
const client = getClient(globalOpts);
|
|
1875
|
+
try {
|
|
1876
|
+
const params = {};
|
|
1877
|
+
if (opts.periodStart) params.period_start = opts.periodStart;
|
|
1878
|
+
if (opts.periodEnd) params.period_end = opts.periodEnd;
|
|
1879
|
+
if (opts.ownerKey) params.owner_key = opts.ownerKey;
|
|
1880
|
+
const res = await client.get('/performance', { params });
|
|
1881
|
+
printJSON(res.data);
|
|
1882
|
+
} catch (err) {
|
|
1883
|
+
handleError(err);
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
|
|
920
1887
|
program.parse(process.argv);
|