@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.
- package/README.md +40 -0
- package/dist/index.js +156 -1
- 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
|