@opsrev/acculynx-cli 1.2.0 → 1.4.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 (3) hide show
  1. package/README.md +40 -0
  2. package/dist/index.js +156 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -84,6 +84,12 @@ acculynx unofficial messages list <jobId> --type Email # Filter by type: Comment
84
84
  acculynx unofficial messages list <jobId> --count-only # Get message count only
85
85
  acculynx unofficial messages post <jobId> <message> # Post a comment to a job
86
86
  acculynx unofficial messages post <jobId> <message> --notify <userId,...> # Post and notify users
87
+ acculynx unofficial milestones list <jobId> # List workflow milestones/statuses for a job
88
+ acculynx unofficial milestones set <jobId> <status> # Set a job's milestone/status by name
89
+ acculynx unofficial milestones set <jobId> <status> --message "note" # Set with comment
90
+ acculynx unofficial jobs list # List jobs (unofficial)
91
+ acculynx unofficial jobs list --status <name> # Filter by workflow status name
92
+ acculynx unofficial jobs list --milestone <name> # Filter by milestone name
87
93
  ```
88
94
 
89
95
  #### Messages
@@ -106,6 +112,40 @@ acculynx unofficial messages post <jobId> "Check this out" --notify "userId1,use
106
112
 
107
113
  User IDs for `--notify` come from the official users API (`/api/v2/users`).
108
114
 
115
+ #### Milestones
116
+
117
+ AccuLynx workflows have milestones (Lead, Prospect, Approved, etc.) that contain custom statuses (Estimate Sent, Contract Signed, etc.). The official API only filters by top-level milestones. The unofficial milestones commands let you work with the granular workflow statuses.
118
+
119
+ List all milestones and their statuses for a job:
120
+
121
+ ```bash
122
+ acculynx unofficial milestones list <jobId>
123
+ ```
124
+
125
+ Set a job to a specific workflow status by name (case-insensitive):
126
+
127
+ ```bash
128
+ acculynx unofficial milestones set <jobId> "Estimate Sent"
129
+ acculynx unofficial milestones set <jobId> "OpsRev: Estimate Ready for Review"
130
+ acculynx unofficial milestones set <jobId> "Contract Signed" --message "Moving to contract phase"
131
+ ```
132
+
133
+ If the status name doesn't match, the command returns the list of available statuses.
134
+
135
+ #### Jobs (unofficial)
136
+
137
+ The official `jobs list --milestones` filter only accepts top-level milestone names (Lead, Prospect, Approved, etc.). The unofficial `jobs list` command supports filtering by custom workflow status names.
138
+
139
+ ```bash
140
+ acculynx unofficial jobs list --status "Estimate Sent" # jobs in a specific status
141
+ acculynx unofficial jobs list --status "OpsRev: Estimate Ready for Review" # custom status names work
142
+ acculynx unofficial jobs list --milestone Prospect # filter by milestone
143
+ acculynx unofficial jobs list --status "Estimate Sent" --query "Smith" # combine with search
144
+ acculynx unofficial jobs list --sort "lastTouched|desc" --page 2 # sort and paginate
145
+ ```
146
+
147
+ The `--status` flag resolves the status name (case-insensitive) to a GUID via the company workflow configuration, then queries the internal job list API. If the name doesn't match, the command returns the list of available statuses.
148
+
109
149
  **Getting started:** If you don't know your company ID, run `companies` first -- it logs in and returns all companies available to your account:
110
150
 
111
151
  ```bash
package/dist/index.js CHANGED
@@ -127,7 +127,7 @@ async function readStdin(stream = process.stdin) {
127
127
  // src/commands/jobs.ts
128
128
  function registerJobsCommands(parentCmd, getClient2) {
129
129
  const jobs = parentCmd.command("jobs").description("Job operations");
130
- jobs.command("list").description("List jobs (paginated)").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--date-filter-type <type>", "Date field to filter on").option("--milestones <milestones>", "Filter by milestones").option("--sort-by <field>", "Sort by: CreatedDate, MilestoneDate, ModifiedDate").option("--sort-order <order>", "Ascending or Descending").option("--includes <fields>", "Include: contact, initialAppointment").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
130
+ jobs.command("list").description("List jobs (paginated)").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--date-filter-type <type>", "Date field to filter on").option("--milestones <milestones>", "Filter by milestones").option("--sort-by <field>", "Sort by: CreatedDate, MilestoneDate, ModifiedDate").option("--sort-order <order>", "Ascending or Descending").option("--includes <fields>", "Include: contact, initialAppointment").option("--assignment <type>", "Filter by assignment: assigned, unassigned").option("--limit <n>", "Max total results (default: 25)").option("--all", "Fetch all results (no limit)").action(async (opts) => {
131
131
  const params = {};
132
132
  if (opts.startDate) params.startDate = opts.startDate;
133
133
  if (opts.endDate) params.endDate = opts.endDate;
@@ -136,6 +136,7 @@ function registerJobsCommands(parentCmd, getClient2) {
136
136
  if (opts.sortBy) params.sortBy = opts.sortBy;
137
137
  if (opts.sortOrder) params.sortOrder = opts.sortOrder;
138
138
  if (opts.includes) params.includes = opts.includes;
139
+ if (opts.assignment) params.assignment = opts.assignment;
139
140
  const limit = opts.all ? Infinity : opts.limit ? parseInt(opts.limit, 10) : void 0;
140
141
  const result = await paginate(getClient2(), "/jobs", params, limit);
141
142
  console.log(JSON.stringify(result));
@@ -764,6 +765,158 @@ function registerMessagesCommands(parentCmd, getClient2) {
764
765
  });
765
766
  }
766
767
 
768
+ // src/unofficial/commands/milestones.ts
769
+ function registerMilestonesCommands(parentCmd, getClient2) {
770
+ const milestones = parentCmd.command("milestones").description("Job milestone/status operations");
771
+ milestones.command("list").argument("<jobId>", "Job ID (GUID)").description("List all milestones and statuses for a job's workflow").action(async (jobId) => {
772
+ const data = await getClient2().get(
773
+ `/api/v3/job-workflows/${jobId}`
774
+ );
775
+ const result = data.Milestones.map((m) => ({
776
+ milestoneId: m.MilestoneId,
777
+ name: m.Name,
778
+ isCurrent: m.IsCurrent,
779
+ done: m.Done,
780
+ statuses: m.Statuses.map((s) => ({
781
+ statusId: s.StatusId,
782
+ name: s.Name.trim(),
783
+ isCurrent: s.IsCurrent,
784
+ done: s.Done
785
+ }))
786
+ }));
787
+ console.log(JSON.stringify(result));
788
+ });
789
+ milestones.command("set").argument("<jobId>", "Job ID (GUID)").argument("<statusName>", "Destination milestone/status name (case-insensitive)").option("--message <message>", "Optional comment when advancing", "").description("Set a job's milestone/status by name").action(async (jobId, statusName, opts) => {
790
+ const data = await getClient2().get(
791
+ `/api/v3/job-workflows/${jobId}`
792
+ );
793
+ const needle = statusName.toLowerCase().trim();
794
+ let matchedMilestoneId = null;
795
+ let matchedStatus = null;
796
+ for (const m of data.Milestones) {
797
+ if (m.Name.toLowerCase().trim() === needle) {
798
+ matchedMilestoneId = m.MilestoneId;
799
+ matchedStatus = m.Statuses[0] ?? null;
800
+ break;
801
+ }
802
+ for (const s of m.Statuses) {
803
+ if (s.Name.toLowerCase().trim() === needle) {
804
+ matchedMilestoneId = m.MilestoneId;
805
+ matchedStatus = s;
806
+ break;
807
+ }
808
+ }
809
+ if (matchedStatus) break;
810
+ }
811
+ if (!matchedMilestoneId || !matchedStatus) {
812
+ const available = data.Milestones.flatMap(
813
+ (m) => m.Statuses.map((s) => s.Name.trim())
814
+ );
815
+ console.error(
816
+ JSON.stringify({
817
+ error: `Status "${statusName}" not found`,
818
+ available
819
+ })
820
+ );
821
+ process.exitCode = 1;
822
+ return;
823
+ }
824
+ await getClient2().post(
825
+ `/api/v3/job-workflows/${jobId}/set-state`,
826
+ {
827
+ MilestoneId: matchedMilestoneId,
828
+ StatusId: matchedStatus.StatusId,
829
+ Message: opts.message
830
+ }
831
+ );
832
+ console.log(
833
+ JSON.stringify({
834
+ jobId,
835
+ milestoneId: matchedMilestoneId,
836
+ statusId: matchedStatus.StatusId,
837
+ statusName: matchedStatus.Name.trim(),
838
+ message: opts.message || null
839
+ })
840
+ );
841
+ });
842
+ }
843
+
844
+ // src/unofficial/commands/jobs.ts
845
+ async function resolveStatusId(client, statusName) {
846
+ const workflow = await client.get(
847
+ "/api/v3/workflows"
848
+ );
849
+ const needle = statusName.toLowerCase().trim();
850
+ for (const m of workflow.Milestones) {
851
+ for (const s of m.Statuses) {
852
+ if (s.Name.toLowerCase().trim() === needle) {
853
+ return { statusId: s.StatusId, milestoneName: m.Name };
854
+ }
855
+ }
856
+ }
857
+ return null;
858
+ }
859
+ async function getAvailableStatuses(client) {
860
+ const workflow = await client.get(
861
+ "/api/v3/workflows"
862
+ );
863
+ return workflow.Milestones.flatMap(
864
+ (m) => m.Statuses.map((s) => s.Name.trim())
865
+ );
866
+ }
867
+ function registerUnofficialJobsCommands(parentCmd, getClient2) {
868
+ const jobs = parentCmd.command("jobs").description("Unofficial job search with workflow status filtering");
869
+ jobs.command("list").description("List jobs, optionally filtered by workflow status or milestone").option("--status <name>", "Filter by workflow status name (case-insensitive)").option("--milestone <name>", "Filter by milestone name (e.g. Prospect, Approved)").option("--query <text>", "Search term").option("--sort <field>", "Sort: milestoneStart|desc, lastTouched|desc, createdDate|desc", "milestoneStart|desc").option("--page <n>", "Page number", "1").action(async (opts) => {
870
+ const client = getClient2();
871
+ const params = new URLSearchParams();
872
+ if (opts.status) {
873
+ const resolved = await resolveStatusId(client, opts.status);
874
+ if (!resolved) {
875
+ const available = await getAvailableStatuses(client);
876
+ console.error(
877
+ JSON.stringify({
878
+ error: `Status "${opts.status}" not found`,
879
+ available
880
+ })
881
+ );
882
+ process.exitCode = 1;
883
+ return;
884
+ }
885
+ params.append("filters", `currentStatusId=${resolved.statusId}`);
886
+ }
887
+ if (opts.milestone) {
888
+ params.append("filters", `currentMilestoneList=${opts.milestone}`);
889
+ }
890
+ params.set("sort", opts.sort);
891
+ params.set("page", opts.page);
892
+ params.set("type", "2");
893
+ params.set("loadAll", "false");
894
+ if (opts.query) params.set("query", opts.query);
895
+ const data = await client.get(
896
+ `/api/joblist?${params.toString()}`
897
+ );
898
+ const result = {
899
+ totalRecords: data.TotalRecords,
900
+ currentPage: data.CurrentPage,
901
+ totalPages: data.TotalPages,
902
+ jobs: data.results.map((j) => ({
903
+ id: j.Id,
904
+ name: j.Name,
905
+ address: j.FullAddress,
906
+ cityState: j.CityState,
907
+ milestone: j.CurrentMilestone,
908
+ status: j.CurrentStatus,
909
+ salesPerson: j.SalesPerson,
910
+ createdDate: j.CreatedDate,
911
+ lastTouched: j.LastTouched,
912
+ workTypes: j.WorkTypes,
913
+ trades: j.Trades
914
+ }))
915
+ };
916
+ console.log(JSON.stringify(result));
917
+ });
918
+ }
919
+
767
920
  // src/unofficial/commands/index.ts
768
921
  function getSessionFromEnv() {
769
922
  const email = process.env.ACCULYNX_EMAIL;
@@ -795,6 +948,8 @@ function registerUnofficialCommands(parentCmd) {
795
948
  }
796
949
  registerDocumentsCommands(unofficial, getClient2);
797
950
  registerMessagesCommands(unofficial, getClient2);
951
+ registerMilestonesCommands(unofficial, getClient2);
952
+ registerUnofficialJobsCommands(unofficial, getClient2);
798
953
  }
799
954
 
800
955
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsrev/acculynx-cli",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for the AccuLynx roofing CRM API",
5
5
  "type": "module",
6
6
  "bin": {