checkbox-cli 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/checkbox.js +610 -74
  2. package/package.json +1 -1
package/dist/checkbox.js CHANGED
@@ -16,7 +16,7 @@ import { join } from 'path';
16
16
  // ── Config ──────────────────────────────────────────────────────────
17
17
  const CONFIG_DIR = join(homedir(), '.checkbox');
18
18
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
19
- const VERSION = '3.0.1';
19
+ const VERSION = '3.1.0';
20
20
  function loadConfig() {
21
21
  try {
22
22
  if (!existsSync(CONFIG_FILE))
@@ -480,40 +480,149 @@ async function mTasks(cfg, post) {
480
480
  if (!frequency)
481
481
  return mTasks(cfg, post);
482
482
  const payload = { name, frequency };
483
+ // Frequency configuration (day of week, day of month, etc.)
484
+ if (frequency === 'weekly') {
485
+ const dow = await sel('Which day of the week?', [
486
+ { value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
487
+ { value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
488
+ { value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
489
+ { value: 'sunday', label: 'Sunday' },
490
+ ]);
491
+ if (dow)
492
+ payload.frequency_config = { day_of_week: dow };
493
+ }
494
+ else if (frequency === 'monthly') {
495
+ const dom = await txt('Which day of the month? (1-31)', false, '1');
496
+ if (dom)
497
+ payload.frequency_config = { day_of_month: parseInt(dom, 10) };
498
+ }
499
+ else if (frequency === 'yearly') {
500
+ const month = await txt('Month (1-12)', false, '1');
501
+ const day = await txt('Day (1-31)', false, '1');
502
+ if (month && day)
503
+ payload.frequency_config = { month: parseInt(month, 10), day: parseInt(day, 10) };
504
+ }
483
505
  const addPlan = await yn('Add to a plan?');
484
506
  if (addPlan) {
485
507
  const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
486
- if (plan)
508
+ if (plan) {
487
509
  payload.plan_id = plan.id;
510
+ // Optionally link to a specific requirement
511
+ const linkReq = await yn('Link to a specific requirement?');
512
+ if (linkReq) {
513
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
514
+ if (req)
515
+ payload.requirement_id = req.id;
516
+ }
517
+ }
488
518
  }
489
- const more = await yn('Add more details? (description, time, reminders, etc.)');
490
- if (more) {
491
- const desc = await txt('Description');
492
- if (desc)
493
- payload.description = desc;
494
- const priv = await yn('Private task?');
495
- if (priv)
496
- payload.is_private = true;
497
- const time = await txt('Scheduled time (HH:MM)', false, '09:00');
498
- if (time)
499
- payload.time = time;
500
- const reminder = await sel('Reminder', [
501
- { value: 'none', label: 'No reminder' },
502
- { value: 'notification', label: 'Push notification' },
503
- { value: 'email', label: 'Email' },
504
- ]);
505
- if (reminder && reminder !== 'none') {
506
- payload.reminder_mode = reminder;
507
- const mins = await txt('Minutes before', false, '30');
508
- if (mins)
509
- payload.reminder_minutes = parseInt(mins, 10);
519
+ // Description
520
+ const desc = await txt('Description (blank to skip)');
521
+ if (desc)
522
+ payload.description = desc;
523
+ // Time
524
+ const time = await txt('Scheduled time (HH:MM, blank to skip)', false, '09:00');
525
+ if (time)
526
+ payload.time = time;
527
+ // Assignment
528
+ const assign = await yn('Assign to a specific person?');
529
+ if (assign) {
530
+ const assignId = await txt('User ID to assign to', true);
531
+ if (assignId) {
532
+ payload.assigned_to = assignId;
533
+ const assignName = await txt('Their display name (optional)');
534
+ if (assignName)
535
+ payload.assigned_to_name = assignName;
536
+ }
537
+ }
538
+ // Privacy
539
+ const priv = await yn('Private task?');
540
+ if (priv)
541
+ payload.is_private = true;
542
+ // Reminder
543
+ const reminder = await sel('Reminder', [
544
+ { value: 'none', label: 'No reminder' },
545
+ { value: 'notification', label: 'Push notification' },
546
+ { value: 'email', label: 'Email' },
547
+ ]);
548
+ if (reminder && reminder !== 'none') {
549
+ payload.reminder_mode = reminder;
550
+ const mins = await txt('Minutes before', false, '30');
551
+ if (mins)
552
+ payload.reminder_minutes = parseInt(mins, 10);
553
+ }
554
+ // Conditional question
555
+ const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
556
+ if (cond)
557
+ payload.conditional_question = cond;
558
+ // Approval requirement
559
+ const approval = await yn('Requires approval before completion?');
560
+ if (approval)
561
+ payload.approval_required = true;
562
+ // Proof / Validation requirements (THE MOST IMPORTANT FEATURE)
563
+ const addProof = await yn('Require proof to complete this task? (photos, documents, location, etc.)');
564
+ if (addProof) {
565
+ const validationTypes = [];
566
+ let addMoreProof = true;
567
+ while (addMoreProof) {
568
+ const proofType = await sel('What type of proof?', [
569
+ { value: 'photo', label: 'Photo', hint: 'require a photo to be taken' },
570
+ { value: 'document', label: 'Document', hint: 'require a document upload' },
571
+ { value: 'geolocation', label: 'Location check-in', hint: 'require being at a specific location' },
572
+ { value: 'checklist', label: 'Checklist', hint: 'require completing a checklist' },
573
+ { value: 'table', label: 'Table record', hint: 'require filling out a data table' },
574
+ ]);
575
+ if (!proofType)
576
+ break;
577
+ const vt = { type: proofType };
578
+ // Pick the specific asset for this proof type
579
+ if (proofType === 'photo') {
580
+ const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
581
+ if (ph)
582
+ vt.config_id = ph.id;
583
+ }
584
+ else if (proofType === 'document') {
585
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
586
+ if (doc) {
587
+ vt.config_id = doc.id;
588
+ const targetMode = await sel('Where should uploads go?', [
589
+ { value: 'locked', label: 'Always to this folder' },
590
+ { value: 'user_choice', label: 'Let user choose folder' },
591
+ ]);
592
+ if (targetMode) {
593
+ vt.target_mode = targetMode;
594
+ if (targetMode === 'locked')
595
+ vt.target_folder_id = doc.id;
596
+ }
597
+ }
598
+ }
599
+ else if (proofType === 'geolocation') {
600
+ const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
601
+ if (area)
602
+ vt.config_id = area.id;
603
+ }
604
+ else if (proofType === 'checklist') {
605
+ const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
606
+ if (cl)
607
+ vt.config_id = cl.id;
608
+ }
609
+ else if (proofType === 'table') {
610
+ const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
611
+ if (tbl)
612
+ vt.config_id = tbl.id;
613
+ }
614
+ validationTypes.push(vt);
615
+ addMoreProof = !!(await yn('Add another proof requirement?'));
510
616
  }
511
- const approval = await yn('Requires approval?');
512
- if (approval)
513
- payload.approval_required = true;
514
- const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
515
- if (cond)
516
- payload.conditional_question = cond;
617
+ if (validationTypes.length > 0)
618
+ payload.validation_types = validationTypes;
619
+ }
620
+ // Share with specific users
621
+ const share = await yn('Share with specific users?');
622
+ if (share) {
623
+ const ids = await txt('User IDs (comma-separated)', true);
624
+ if (ids)
625
+ payload.shared_with_ids = ids.split(',').map(s => s.trim()).filter(Boolean);
517
626
  }
518
627
  await cmd(post, cfg.orgId, 'create_task', payload, 'Creating task...');
519
628
  return mTasks(cfg, post);
@@ -522,24 +631,126 @@ async function mTasks(cfg, post) {
522
631
  const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
523
632
  if (!t)
524
633
  return mTasks(cfg, post);
525
- p.log.info(`Editing: ${pc.cyan(t.name)} ${pc.dim('(leave blank to skip)')}`);
526
- const updates = {};
527
- const n = await txt('New name');
528
- if (n === null)
529
- return mTasks(cfg, post);
530
- if (n)
531
- updates.name = n;
532
- const f = await sel('New frequency', [
533
- { value: '__skip', label: pc.dim('Keep current') },
534
- { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
535
- { value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
536
- { value: 'one-time', label: 'One time' },
634
+ p.log.info(`Editing: ${pc.cyan(t.name)}`);
635
+ const field = await sel('What would you like to change?', [
636
+ { value: 'name', label: 'Name' },
637
+ { value: 'frequency', label: 'Frequency' },
638
+ { value: 'description', label: 'Description' },
639
+ { value: 'time', label: 'Scheduled time' },
640
+ { value: 'assigned_to', label: 'Assignment' },
641
+ { value: 'is_private', label: 'Privacy' },
642
+ { value: 'reminder', label: 'Reminder settings' },
643
+ { value: 'conditional_question', label: 'Conditional question' },
644
+ { value: 'plan', label: 'Plan / requirement link' },
645
+ { value: '__back', label: pc.dim('\u2190 Back') },
537
646
  ]);
538
- if (f && f !== '__skip')
539
- updates.frequency = f;
540
- const d = await txt('New description');
541
- if (d)
542
- updates.description = d;
647
+ if (!field || field === '__back')
648
+ return mTasks(cfg, post);
649
+ const updates = {};
650
+ switch (field) {
651
+ case 'name': {
652
+ const n = await txt('New name', true);
653
+ if (n)
654
+ updates.name = n;
655
+ break;
656
+ }
657
+ case 'frequency': {
658
+ const f = await sel('New frequency', [
659
+ { value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
660
+ { value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
661
+ { value: 'one-time', label: 'One time' },
662
+ ]);
663
+ if (f) {
664
+ updates.frequency = f;
665
+ if (f === 'weekly') {
666
+ const dow = await sel('Which day?', [
667
+ { value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
668
+ { value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
669
+ { value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
670
+ { value: 'sunday', label: 'Sunday' },
671
+ ]);
672
+ if (dow)
673
+ updates.frequency_config = { day_of_week: dow };
674
+ }
675
+ else if (f === 'monthly') {
676
+ const dom = await txt('Which day of the month? (1-31)', false, '1');
677
+ if (dom)
678
+ updates.frequency_config = { day_of_month: parseInt(dom, 10) };
679
+ }
680
+ }
681
+ break;
682
+ }
683
+ case 'description': {
684
+ const d = await txt('New description', true);
685
+ if (d)
686
+ updates.description = d;
687
+ break;
688
+ }
689
+ case 'time': {
690
+ const time = await txt('New scheduled time (HH:MM)', false, '09:00');
691
+ if (time)
692
+ updates.time = time;
693
+ break;
694
+ }
695
+ case 'assigned_to': {
696
+ const aid = await txt('User ID to assign to (blank to unassign)');
697
+ updates.assigned_to = aid || null;
698
+ if (aid) {
699
+ const aname = await txt('Their display name (optional)');
700
+ if (aname)
701
+ updates.assigned_to_name = aname;
702
+ }
703
+ break;
704
+ }
705
+ case 'is_private': {
706
+ const priv = await yn('Make this task private?');
707
+ updates.is_private = !!priv;
708
+ break;
709
+ }
710
+ case 'reminder': {
711
+ const mode = await sel('Reminder type', [
712
+ { value: 'none', label: 'No reminder' },
713
+ { value: 'notification', label: 'Push notification' },
714
+ { value: 'email', label: 'Email' },
715
+ ]);
716
+ if (mode === 'none') {
717
+ updates.reminder_mode = null;
718
+ updates.reminder_minutes = null;
719
+ }
720
+ else if (mode) {
721
+ updates.reminder_mode = mode;
722
+ const mins = await txt('Minutes before', false, '30');
723
+ if (mins)
724
+ updates.reminder_minutes = parseInt(mins, 10);
725
+ }
726
+ break;
727
+ }
728
+ case 'conditional_question': {
729
+ const q = await txt('Conditional question (blank to remove)');
730
+ updates.conditional_question = q || null;
731
+ break;
732
+ }
733
+ case 'plan': {
734
+ const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
735
+ if (plan) {
736
+ updates.plan_id = plan.id;
737
+ const linkReq = await yn('Link to a requirement?');
738
+ if (linkReq) {
739
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
740
+ if (req)
741
+ updates.requirement_id = req.id;
742
+ }
743
+ }
744
+ else {
745
+ const unlink = await yn('Remove from current plan?');
746
+ if (unlink) {
747
+ updates.plan_id = null;
748
+ updates.requirement_id = null;
749
+ }
750
+ }
751
+ break;
752
+ }
753
+ }
543
754
  if (Object.keys(updates).length > 0) {
544
755
  await cmd(post, cfg.orgId, 'update_task', { task_id: t.id, updates }, 'Updating...');
545
756
  }
@@ -1009,14 +1220,67 @@ async function mTables(cfg, post) {
1009
1220
  if (!fn)
1010
1221
  break;
1011
1222
  const ft = await sel('Column type', [
1012
- { value: 'text', label: 'Text' }, { value: 'number', label: 'Number' },
1013
- { value: 'date', label: 'Date' }, { value: 'select', label: 'Dropdown' },
1014
- { value: 'checkbox', label: 'Checkbox' }, { value: 'file', label: 'File' },
1223
+ { value: 'text', label: 'Text' },
1224
+ { value: 'number', label: 'Number' },
1225
+ { value: 'date', label: 'Date' },
1226
+ { value: 'select', label: 'Dropdown', hint: 'pick from a list of values' },
1227
+ { value: 'multi_select', label: 'Multi-select dropdown', hint: 'pick multiple values' },
1228
+ { value: 'checkbox', label: 'Checkbox', hint: 'yes/no toggle' },
1229
+ { value: 'file', label: 'File upload' },
1230
+ { value: 'url', label: 'URL / link' },
1231
+ { value: 'email', label: 'Email address' },
1232
+ { value: 'phone', label: 'Phone number' },
1233
+ { value: 'currency', label: 'Currency / money' },
1234
+ { value: 'percent', label: 'Percentage' },
1235
+ { value: 'rating', label: 'Rating (1-5)' },
1236
+ { value: 'relation', label: 'Link to another table' },
1015
1237
  ]);
1016
1238
  if (!ft)
1017
1239
  break;
1240
+ const col = { name: fn, type: ft };
1018
1241
  const req = await yn('Required?');
1019
- fields.push({ name: fn, type: ft, ...(req ? { required: true } : {}) });
1242
+ if (req)
1243
+ col.required = true;
1244
+ // Dropdown values
1245
+ if (ft === 'select' || ft === 'multi_select') {
1246
+ const optSource = await sel('Where do the options come from?', [
1247
+ { value: 'manual', label: 'I\'ll type them now' },
1248
+ { value: 'list', label: 'Link to an existing table/list' },
1249
+ ]);
1250
+ if (optSource === 'manual') {
1251
+ const optStr = await txt('Options (comma-separated)', true, 'e.g. Pass, Fail, N/A');
1252
+ if (optStr)
1253
+ col.options = optStr.split(',').map(s => s.trim()).filter(Boolean);
1254
+ }
1255
+ else if (optSource === 'list') {
1256
+ const srcTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
1257
+ if (srcTable) {
1258
+ col.source_table_id = srcTable.id;
1259
+ const srcField = await txt('Which column to use as the option label?', false, 'name');
1260
+ if (srcField)
1261
+ col.source_field = srcField;
1262
+ }
1263
+ }
1264
+ const defaultVal = await txt('Default value (blank for none)');
1265
+ if (defaultVal)
1266
+ col.default = defaultVal;
1267
+ }
1268
+ // Number/currency constraints
1269
+ if (ft === 'number' || ft === 'currency' || ft === 'percent') {
1270
+ const min = await txt('Minimum value (blank for no limit)');
1271
+ if (min)
1272
+ col.min = parseFloat(min);
1273
+ const max = await txt('Maximum value (blank for no limit)');
1274
+ if (max)
1275
+ col.max = parseFloat(max);
1276
+ }
1277
+ // Relation target
1278
+ if (ft === 'relation') {
1279
+ const relTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
1280
+ if (relTable)
1281
+ col.relation_table_id = relTable.id;
1282
+ }
1283
+ fields.push(col);
1020
1284
  addMore = !!(await yn('Add another column?'));
1021
1285
  }
1022
1286
  if (fields.length > 0)
@@ -1510,20 +1774,37 @@ async function mAutomation(cfg, post) {
1510
1774
  return mAutomation(cfg, post);
1511
1775
  }
1512
1776
  case 'val-create': {
1513
- const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
1514
- if (!task)
1777
+ // Choose target: task or requirement
1778
+ const target = await sel('Apply this rule to', [
1779
+ { value: 'task', label: 'A specific task' },
1780
+ { value: 'requirement', label: 'A requirement', hint: 'applies to all tasks under it' },
1781
+ ]);
1782
+ if (!target)
1515
1783
  return mAutomation(cfg, post);
1784
+ const rulePayload = {};
1785
+ if (target === 'task') {
1786
+ const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
1787
+ if (!task)
1788
+ return mAutomation(cfg, post);
1789
+ rulePayload.task_id = task.id;
1790
+ }
1791
+ else {
1792
+ const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
1793
+ if (!req)
1794
+ return mAutomation(cfg, post);
1795
+ rulePayload.requirement_id = req.id;
1796
+ }
1516
1797
  const name = await txt('Rule name', true, 'e.g. Require photo proof');
1517
1798
  if (!name)
1518
1799
  return mAutomation(cfg, post);
1519
1800
  const ruleType = await sel('Rule type', [
1520
- { value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, etc.' },
1521
- { value: 'field_constraint', label: 'Field constraint', hint: 'validate a data field' },
1801
+ { value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, checklist, or table' },
1802
+ { value: 'field_constraint', label: 'Field constraint', hint: 'validate a specific field value' },
1803
+ { value: 'cross_field', label: 'Cross-field validation', hint: 'compare two fields' },
1804
+ { value: 'aggregate', label: 'Aggregate check', hint: 'count/sum/avg of historical data' },
1522
1805
  { value: 'approval_required', label: 'Approval required', hint: 'needs manager sign-off' },
1523
1806
  { value: 'time_window', label: 'Time window', hint: 'must be done within a time range' },
1524
- { value: 'record_exists', label: 'Record exists', hint: 'verify data was entered' },
1525
- { value: 'cross_field', label: 'Cross-field validation' },
1526
- { value: 'aggregate', label: 'Aggregate check' },
1807
+ { value: 'record_exists', label: 'Record exists', hint: 'verify a table record was created' },
1527
1808
  ]);
1528
1809
  if (!ruleType)
1529
1810
  return mAutomation(cfg, post);
@@ -1531,21 +1812,201 @@ async function mAutomation(cfg, post) {
1531
1812
  if (ruleType === 'proof_required') {
1532
1813
  const proofType = await sel('What kind of proof?', [
1533
1814
  { value: 'photo', label: 'Photo' }, { value: 'document', label: 'Document' },
1534
- { value: 'geolocation', label: 'Location' }, { value: 'checklist', label: 'Checklist' },
1815
+ { value: 'geolocation', label: 'Location check-in' }, { value: 'checklist', label: 'Checklist' },
1816
+ { value: 'table', label: 'Table record' },
1817
+ ]);
1818
+ if (proofType) {
1819
+ condition.validation_type = proofType;
1820
+ // Let them pick the specific asset
1821
+ if (proofType === 'photo') {
1822
+ const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
1823
+ if (ph)
1824
+ condition.config_id = ph.id;
1825
+ }
1826
+ else if (proofType === 'document') {
1827
+ const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
1828
+ if (doc) {
1829
+ condition.config_id = doc.id;
1830
+ const tMode = await sel('Upload destination', [
1831
+ { value: 'locked', label: 'Always to this folder' },
1832
+ { value: 'user_choice', label: 'Let user choose' },
1833
+ ]);
1834
+ if (tMode) {
1835
+ condition.target_mode = tMode;
1836
+ if (tMode === 'locked')
1837
+ condition.target_folder_id = doc.id;
1838
+ }
1839
+ }
1840
+ }
1841
+ else if (proofType === 'geolocation') {
1842
+ const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
1843
+ if (area)
1844
+ condition.config_id = area.id;
1845
+ }
1846
+ else if (proofType === 'checklist') {
1847
+ const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
1848
+ if (cl)
1849
+ condition.config_id = cl.id;
1850
+ }
1851
+ else if (proofType === 'table') {
1852
+ const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
1853
+ if (tbl)
1854
+ condition.config_id = tbl.id;
1855
+ }
1856
+ }
1857
+ }
1858
+ else if (ruleType === 'field_constraint') {
1859
+ const field = await txt('Field name', true, 'e.g. status, temperature, score');
1860
+ if (field)
1861
+ condition.field = field;
1862
+ const op = await sel('Condition', [
1863
+ { value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
1864
+ { value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater than or equal' },
1865
+ { value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less than or equal' },
1866
+ { value: 'in', label: 'Is one of (list)' }, { value: 'not_in', label: 'Is not one of (list)' },
1867
+ { value: 'is_null', label: 'Is empty' }, { value: 'is_not_null', label: 'Is not empty' },
1868
+ { value: 'matches', label: 'Matches pattern' },
1869
+ ]);
1870
+ if (op)
1871
+ condition.operator = op;
1872
+ if (op && !['is_null', 'is_not_null'].includes(op)) {
1873
+ if (op === 'in' || op === 'not_in') {
1874
+ const vals = await txt('Values (comma-separated)', true);
1875
+ if (vals)
1876
+ condition.value = vals.split(',').map(s => s.trim());
1877
+ }
1878
+ else {
1879
+ const val = await txt('Value', true);
1880
+ if (val)
1881
+ condition.value = isNaN(Number(val)) ? val : Number(val);
1882
+ }
1883
+ }
1884
+ }
1885
+ else if (ruleType === 'cross_field') {
1886
+ const f1 = await txt('First field', true);
1887
+ if (f1)
1888
+ condition.field = f1;
1889
+ const cop = await sel('Must be', [
1890
+ { value: 'eq', label: 'Equal to' }, { value: 'neq', label: 'Not equal to' },
1891
+ { value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater or equal to' },
1892
+ { value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less or equal to' },
1893
+ ]);
1894
+ if (cop)
1895
+ condition.compare_operator = cop;
1896
+ const f2 = await txt('Second field', true);
1897
+ if (f2)
1898
+ condition.compare_field = f2;
1899
+ }
1900
+ else if (ruleType === 'aggregate') {
1901
+ const aggFn = await sel('Aggregate function', [
1902
+ { value: 'count', label: 'Count' }, { value: 'sum', label: 'Sum' },
1903
+ { value: 'avg', label: 'Average' }, { value: 'min', label: 'Minimum' },
1904
+ { value: 'max', label: 'Maximum' },
1905
+ ]);
1906
+ if (aggFn)
1907
+ condition.aggregate_function = aggFn;
1908
+ condition.aggregate_table = 'task_history';
1909
+ if (aggFn !== 'count') {
1910
+ const aggField = await txt('Field to aggregate', true);
1911
+ if (aggField)
1912
+ condition.aggregate_field = aggField;
1913
+ }
1914
+ const aggOp = await sel('Result must be', [
1915
+ { value: 'eq', label: 'Equal to' }, { value: 'gt', label: 'Greater than' },
1916
+ { value: 'gte', label: 'At least' }, { value: 'lt', label: 'Less than' },
1917
+ { value: 'lte', label: 'At most' },
1918
+ ]);
1919
+ if (aggOp)
1920
+ condition.aggregate_operator = aggOp;
1921
+ const aggVal = await txt('Target value', true, 'e.g. 5');
1922
+ if (aggVal)
1923
+ condition.aggregate_value = parseFloat(aggVal);
1924
+ }
1925
+ else if (ruleType === 'approval_required') {
1926
+ const approverType = await sel('Who must approve?', [
1927
+ { value: 'role', label: 'Anyone with a specific role' },
1928
+ { value: 'user', label: 'A specific person' },
1535
1929
  ]);
1536
- if (proofType)
1537
- condition.proof_type = proofType;
1930
+ if (approverType === 'role') {
1931
+ const role = await sel('Required role', [
1932
+ { value: 'admin', label: 'Admin' }, { value: 'super_admin', label: 'Super Admin' },
1933
+ ]);
1934
+ if (role)
1935
+ condition.approval_approver_role = role;
1936
+ }
1937
+ else if (approverType === 'user') {
1938
+ const uid = await txt('Approver user ID', true);
1939
+ if (uid)
1940
+ condition.approval_approver_id = uid;
1941
+ }
1538
1942
  }
1539
1943
  else if (ruleType === 'time_window') {
1540
- const start = await txt('Start time (HH:MM)', false, '08:00');
1541
- const end = await txt('End time (HH:MM)', false, '17:00');
1944
+ const wType = await sel('Window type', [
1945
+ { value: 'daily', label: 'Daily time window' },
1946
+ { value: 'custom', label: 'Custom date/time range' },
1947
+ ]);
1948
+ if (wType)
1949
+ condition.window_type = wType;
1950
+ const start = await txt('Start time (HH:MM)', true, '08:00');
1542
1951
  if (start)
1543
1952
  condition.start_time = start;
1953
+ const end = await txt('End time (HH:MM)', true, '17:00');
1544
1954
  if (end)
1545
1955
  condition.end_time = end;
1956
+ const tz = await txt('Timezone (blank for UTC)', false, 'America/New_York');
1957
+ if (tz)
1958
+ condition.timezone = tz;
1959
+ }
1960
+ else if (ruleType === 'record_exists') {
1961
+ const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
1962
+ if (tbl) {
1963
+ condition.table = tbl.id;
1964
+ const addFilters = await yn('Add filters? (only match records with specific values)');
1965
+ if (addFilters) {
1966
+ const filters = [];
1967
+ let addF = true;
1968
+ while (addF) {
1969
+ const ff = await txt('Field name', true);
1970
+ if (!ff)
1971
+ break;
1972
+ const fop = await sel('Condition', [
1973
+ { value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
1974
+ { value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
1975
+ { value: 'is_not_null', label: 'Has a value' },
1976
+ ]);
1977
+ if (!fop)
1978
+ break;
1979
+ if (fop === 'is_not_null') {
1980
+ filters.push({ field: ff, operator: fop, value: null });
1981
+ }
1982
+ else {
1983
+ const fv = await txt('Value', true);
1984
+ if (fv)
1985
+ filters.push({ field: ff, operator: fop, value: isNaN(Number(fv)) ? fv : Number(fv) });
1986
+ }
1987
+ addF = !!(await yn('Add another filter?'));
1988
+ }
1989
+ if (filters.length > 0)
1990
+ condition.filters = filters;
1991
+ }
1992
+ }
1993
+ }
1994
+ // Grouping options
1995
+ const addGroup = await yn('Add to a rule group? (for AND/OR logic with other rules)');
1996
+ if (addGroup) {
1997
+ const gid = await txt('Group ID (create a new UUID or use an existing group ID)', true);
1998
+ if (gid) {
1999
+ rulePayload.group_id = gid;
2000
+ const gop = await sel('Group logic', [
2001
+ { value: 'AND', label: 'AND — all rules in the group must pass' },
2002
+ { value: 'OR', label: 'OR — any rule in the group can pass' },
2003
+ ]);
2004
+ if (gop)
2005
+ rulePayload.group_operator = gop;
2006
+ }
1546
2007
  }
1547
2008
  await cmd(post, cfg.orgId, 'create_validation_rule', {
1548
- task_id: task.id, name, rule_type: ruleType, condition,
2009
+ ...rulePayload, name, rule_type: ruleType, condition,
1549
2010
  }, 'Creating validation rule...');
1550
2011
  return mAutomation(cfg, post);
1551
2012
  }
@@ -1621,25 +2082,100 @@ async function mAutomation(cfg, post) {
1621
2082
  if (!name)
1622
2083
  return mAutomation(cfg, post);
1623
2084
  const eventType = await sel('What triggers this?', [
1624
- { value: 'record_created', label: 'New record added to a table' },
1625
- { value: 'record_updated', label: 'Record updated in a table' },
1626
2085
  { value: 'task_completed', label: 'Another task completed' },
1627
- { value: 'task_missed', label: 'A task was missed' },
1628
- { value: 'approval_resolved', label: 'An approval was resolved' },
2086
+ { value: 'task_uncompleted', label: 'A task was reopened' },
2087
+ { value: 'task_created', label: 'A new task was created' },
2088
+ { value: 'task_updated', label: 'A task was updated' },
2089
+ { value: 'validation_submitted', label: 'Proof was submitted' },
2090
+ { value: 'approval_requested', label: 'An approval was requested' },
2091
+ { value: 'approval_approved', label: 'An approval was granted' },
2092
+ { value: 'approval_rejected', label: 'An approval was rejected' },
2093
+ { value: 'table_record_created', label: 'New record added to a table' },
2094
+ { value: 'conditional_answer', label: 'A conditional question was answered' },
2095
+ { value: 'document_created', label: 'A document was uploaded' },
2096
+ { value: 'checklist_created', label: 'A checklist was created' },
1629
2097
  ]);
1630
2098
  if (!eventType)
1631
2099
  return mAutomation(cfg, post);
1632
- const sourceType = await sel('Source type', [
1633
- { value: 'table', label: 'Table' }, { value: 'task', label: 'Task' },
2100
+ const sourceType = await sel('Source type (what entity fires the event)', [
2101
+ { value: 'task', label: 'Task' },
2102
+ { value: 'table', label: 'Table' },
2103
+ { value: 'document', label: 'Document' },
2104
+ { value: 'checklist', label: 'Checklist' },
2105
+ { value: 'geolocation_area', label: 'Location area' },
1634
2106
  { value: 'approval', label: 'Approval' },
2107
+ { value: 'plan', label: 'Plan' },
1635
2108
  ]);
1636
2109
  if (!sourceType)
1637
2110
  return mAutomation(cfg, post);
1638
- const payload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
1639
- const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
1640
- if (source)
1641
- payload.source_entity_id = source.id;
1642
- await cmd(post, cfg.orgId, 'create_trigger_rule', payload, 'Creating trigger rule...');
2111
+ const trigPayload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
2112
+ const pickSource = await yn(`Pick a specific ${sourceType}? (or trigger on any)`);
2113
+ if (pickSource) {
2114
+ const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
2115
+ if (source)
2116
+ trigPayload.source_entity_id = source.id;
2117
+ }
2118
+ // Conditions
2119
+ const addConds = await yn('Add conditions? (only trigger when specific criteria are met)');
2120
+ if (addConds) {
2121
+ const conditions = [];
2122
+ let addC = true;
2123
+ while (addC) {
2124
+ const cf = await txt('Field to check', true, 'e.g. status, frequency, name');
2125
+ if (!cf)
2126
+ break;
2127
+ const cop = await sel('Condition', [
2128
+ { value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
2129
+ { value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
2130
+ { value: 'in', label: 'Is one of' }, { value: 'is_not_null', label: 'Has a value' },
2131
+ ]);
2132
+ if (!cop)
2133
+ break;
2134
+ const csrc = await sel('Check against', [
2135
+ { value: 'payload', label: 'Event data (payload)' },
2136
+ { value: 'entity', label: 'Entity state' },
2137
+ ]);
2138
+ let cv = null;
2139
+ if (cop !== 'is_not_null') {
2140
+ if (cop === 'in') {
2141
+ const cvStr = await txt('Values (comma-separated)', true);
2142
+ if (cvStr)
2143
+ cv = cvStr.split(',').map(s => s.trim());
2144
+ }
2145
+ else {
2146
+ const cvTxt = await txt('Value', true);
2147
+ if (cvTxt)
2148
+ cv = isNaN(Number(cvTxt)) ? cvTxt : Number(cvTxt);
2149
+ }
2150
+ }
2151
+ conditions.push({ field: cf, operator: cop, value: cv, ...(csrc ? { source: csrc } : {}) });
2152
+ addC = !!(await yn('Add another condition?'));
2153
+ }
2154
+ if (conditions.length > 0) {
2155
+ trigPayload.conditions = conditions;
2156
+ if (conditions.length > 1) {
2157
+ const logic = await sel('Condition logic', [
2158
+ { value: 'AND', label: 'ALL conditions must be true' },
2159
+ { value: 'OR', label: 'ANY condition can be true' },
2160
+ ]);
2161
+ if (logic)
2162
+ trigPayload.condition_operator = logic;
2163
+ }
2164
+ }
2165
+ }
2166
+ // Advanced options
2167
+ const advanced = await yn('Configure advanced options? (frequency, message, debounce)');
2168
+ if (advanced) {
2169
+ const respect = await yn('Respect task frequency? (only activate on scheduled days)');
2170
+ trigPayload.respect_frequency = !!respect;
2171
+ const msg = await txt('Activation message (shown when triggered)');
2172
+ if (msg)
2173
+ trigPayload.activation_message = msg;
2174
+ const debounce = await txt('Debounce seconds (prevent re-triggering too fast)', false, '60');
2175
+ if (debounce)
2176
+ trigPayload.debounce_seconds = parseInt(debounce, 10);
2177
+ }
2178
+ await cmd(post, cfg.orgId, 'create_trigger_rule', trigPayload, 'Creating trigger rule...');
1643
2179
  return mAutomation(cfg, post);
1644
2180
  }
1645
2181
  case 'trig-edit': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "checkbox-cli",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Beautiful, interactive CLI for Checkbox compliance management. Setup wizard, guided workflows, and full workspace control from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/checkbox.js",