@series-a/cli 1.1.0 → 1.3.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/CHANGELOG.md +24 -0
- package/README.md +49 -1
- package/bin/setu.mjs +232 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@series-a/cli` (and the paired `@series-a/mcp` server).
|
|
4
4
|
|
|
5
|
+
## Unreleased — claims review dashboard
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `setu claims` — review dashboard showing pending claim requests, counts by status and unclaimed profiles scored for readiness (with what's still missing)
|
|
9
|
+
- `setu claims show|approve|decline <claim-id>` (`--reason`, `--yes`), plus `setu claims --status approved|rejected|all`
|
|
10
|
+
- MCP tools `setu_claims_dashboard`, `setu_claims_list`, `setu_claim_detail` and `setu_claim_decide`
|
|
11
|
+
|
|
12
|
+
## 1.3.0 — lists, full profiles and image uploads
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `practice list` and `dentist list` — browse seeded profiles, with `--limit`, `--search` and (for clinicians) `--practice`
|
|
16
|
+
- `setu status <id> --full` and `setu status --dentist` — the complete enriched profile including photos, prices, services and attached clinicians
|
|
17
|
+
- `setu <type> images --upload <file|url>` and `--upload-logo <file|url>` — upload photos straight into Setu storage with your CLI key, no third-party hosting needed
|
|
18
|
+
- `dentist create` now honours `--photo`, `--gallery`, `--phone`, `--website`, `--booking` and `--bio`
|
|
19
|
+
- MCP tool `setu_profile_upload_image`; `setu_profile_status` gained `full`, `list`, `limit`, `search` and `practice_id`; `setu_dentist_create` gained photo/gallery/contact fields
|
|
20
|
+
|
|
21
|
+
## 1.2.0 — booking links and full profile-settings editing
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `practice update` / `dentist update` `--booking <url>` and `--website <url>` shortcuts for setting the booking and website links without `--set`
|
|
25
|
+
- `--set-enrichment path=value` for nested practice profile values (positioning summary/archetype/marketing descriptions, amenities, default currency), so every field on the practice profile settings screen is editable from the CLI
|
|
26
|
+
- `--fields` now also lists the available `--set-enrichment` paths
|
|
27
|
+
- MCP `setu_profile_update` accepts `enrichment_set`
|
|
28
|
+
|
|
5
29
|
## 1.1.0 — editing, pricing and media
|
|
6
30
|
|
|
7
31
|
### Added
|
package/README.md
CHANGED
|
@@ -48,6 +48,8 @@ are listed at the end so you can retry them.
|
|
|
48
48
|
setu practice claim-link <practice-id>
|
|
49
49
|
setu status # every seeded profile and its claim state
|
|
50
50
|
setu status <practice-id> # one profile plus its claim attempts
|
|
51
|
+
setu status <profile-id> --full # every field, photos, prices, services, clinicians
|
|
52
|
+
setu status --dentist # list clinicians instead of practices
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
Send the claim link in outreach. The practice signs in, and is verified either
|
|
@@ -56,10 +58,31 @@ contact address on their Google listing. On success the profile transfers to
|
|
|
56
58
|
them, pre-filled, and they continue through the normal onboarding and
|
|
57
59
|
subscription flow.
|
|
58
60
|
|
|
61
|
+
## Reviewing claims
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
setu claims # pending claims + unclaimed profiles by readiness
|
|
65
|
+
setu claims --ready # only profiles ready to promote
|
|
66
|
+
setu claims --status all # every claim, any status
|
|
67
|
+
setu claims show <claim-id> # claimant, method, attempts, target profile
|
|
68
|
+
setu claims approve <claim-id> # transfer the profile to the claimant
|
|
69
|
+
setu claims decline <claim-id> --reason "Not the practice owner"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Approving a practice claim makes the claimant the owner, adds an owner
|
|
73
|
+
membership and marks the practice's clinicians as claimed. A claim can only be
|
|
74
|
+
approved once the claimant has signed in, so there's an account to transfer to.
|
|
75
|
+
|
|
59
76
|
## Clinicians
|
|
60
77
|
|
|
61
78
|
```bash
|
|
62
|
-
setu dentist create --name "Dr Jane Roe" --practice <id> --profile-url https://…/team/jane
|
|
79
|
+
setu dentist create --name "Dr Jane Roe" --practice <id> --profile-url https://…/team/jane \
|
|
80
|
+
--photo https://…/jane.jpg --gallery "https://…/a.jpg,https://…/b.jpg" \
|
|
81
|
+
--phone 02392000000 --website https://… --booking https://…/book
|
|
82
|
+
|
|
83
|
+
setu practice list --limit 100 --search "smile"
|
|
84
|
+
setu dentist list --practice <practice-id>
|
|
85
|
+
setu dentist status <dentist-id> --full
|
|
63
86
|
```
|
|
64
87
|
|
|
65
88
|
## Flags
|
|
@@ -94,10 +117,26 @@ else is reported and skipped before any API call is made.
|
|
|
94
117
|
setu practice update <practice-id> --fields # what you can set
|
|
95
118
|
setu practice update <practice-id> --set tagline="Gentle care" --set emergency_available=true
|
|
96
119
|
setu dentist update <dentist-id> --set bio="20 years in implants"
|
|
120
|
+
|
|
121
|
+
# Booking / website link shortcuts
|
|
122
|
+
setu practice update <practice-id> --booking https://clinic.co.uk/book
|
|
123
|
+
setu dentist update <dentist-id> --booking https://clinic.co.uk/book --website https://clinic.co.uk
|
|
97
124
|
```
|
|
98
125
|
|
|
99
126
|
Lists take comma-separated values, e.g. `--set services="Implants,Whitening"`.
|
|
100
127
|
|
|
128
|
+
Everything on the practice profile settings screen is settable. The few values that
|
|
129
|
+
live in the nested profile record — positioning, amenities, default currency — use
|
|
130
|
+
`--set-enrichment`:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
setu practice update <practice-id> --set-enrichment positioning.summary="Calm, modern care in Leeds"
|
|
134
|
+
setu practice update <practice-id> --set-enrichment amenities="Wi-Fi,Free parking,Step-free access"
|
|
135
|
+
setu practice update <practice-id> --set-enrichment positioning.archetype=boutique
|
|
136
|
+
setu practice update <practice-id> --set parking_info="Two-hour free bay on site" \
|
|
137
|
+
--set accessibility="Step-free,Hearing loop" --set languages="English,Polish"
|
|
138
|
+
```
|
|
139
|
+
|
|
101
140
|
## Photos and logos
|
|
102
141
|
|
|
103
142
|
```bash
|
|
@@ -110,6 +149,15 @@ setu practice images <practice-id> --logo https://.../logo.png
|
|
|
110
149
|
setu dentist images <dentist-id> --logo https://.../headshot.jpg # main photo
|
|
111
150
|
```
|
|
112
151
|
|
|
152
|
+
Upload from your machine (or copy a remote image into Setu storage) instead of
|
|
153
|
+
linking to someone else's server:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
setu practice images <practice-id> --upload ./reception.jpg
|
|
157
|
+
setu practice images <practice-id> --upload-logo ./logo.png
|
|
158
|
+
setu dentist images <dentist-id> --upload-logo https://.../headshot.jpg
|
|
159
|
+
```
|
|
160
|
+
|
|
113
161
|
## Prices and services
|
|
114
162
|
|
|
115
163
|
```bash
|
package/bin/setu.mjs
CHANGED
|
@@ -71,20 +71,35 @@ ${c.bold("Commands")}
|
|
|
71
71
|
[--unpublish] [--yes]
|
|
72
72
|
practice update <practice-id> Edit fields on a seeded profile
|
|
73
73
|
--set field=value repeatable, e.g. --set tagline="Gentle care"
|
|
74
|
+
--set-enrichment path=value e.g. --set-enrichment amenities="Wi-Fi|Parking"
|
|
75
|
+
[--booking <url>] [--website <url>] shortcuts for the booking / website links
|
|
74
76
|
[--fields] list the fields you can set
|
|
75
77
|
practice images <practice-id> Manage photos on a seeded profile
|
|
76
78
|
[--add <url>] [--remove <url|index>] [--order "url1,url2"]
|
|
77
79
|
[--logo <url>] [--replace "url1,url2"] [--clear] [--list]
|
|
80
|
+
[--upload <file|url>] [--upload-logo <file|url>]
|
|
78
81
|
practice pricing <practice-id> Manage treatment prices
|
|
79
82
|
[--set "Implant=1200"] [--remove "Implant"] [--clear] [--list]
|
|
83
|
+
practice list List seeded practices
|
|
84
|
+
[--limit 50] [--search <name>]
|
|
80
85
|
dentist create --name "Dr Jane Roe" Seed one clinician
|
|
81
86
|
[--practice <id>] [--profile-url <url>] [--town <town>]
|
|
87
|
+
[--photo <url>] [--gallery "url1,url2"] [--phone <n>]
|
|
88
|
+
[--website <url>] [--booking <url>] [--bio "..."]
|
|
89
|
+
dentist list List seeded clinicians
|
|
90
|
+
[--limit 50] [--search <name>] [--practice <id>]
|
|
82
91
|
dentist update <dentist-id> Edit fields on a seeded clinician
|
|
83
92
|
dentist images <dentist-id> Manage a clinician's photos
|
|
84
93
|
dentist pricing <dentist-id> Manage a clinician's treatment prices
|
|
85
94
|
dentist delete <dentist-id> Remove an unclaimed seeded clinician
|
|
86
95
|
[--unpublish] [--yes]
|
|
87
|
-
status [<
|
|
96
|
+
status [<profile-id>] List or inspect seeded profiles
|
|
97
|
+
[--dentist] [--full] [--limit 50] [--search <name>]
|
|
98
|
+
claims Review dashboard: pending claims + ready profiles
|
|
99
|
+
[--status pending|approved|rejected|all] [--dentist] [--ready] [--limit 50]
|
|
100
|
+
claims show <claim-id> Full detail for one claim
|
|
101
|
+
claims approve <claim-id> Hand the profile to the claimant
|
|
102
|
+
claims decline <claim-id> Decline a claim [--reason "..."]
|
|
88
103
|
|
|
89
104
|
${c.bold("Global")} --json --yes --verbose --operator <name> --version
|
|
90
105
|
`);
|
|
@@ -134,9 +149,13 @@ async function cmdProfileUpdate(type, id) {
|
|
|
134
149
|
// The server answers with the whitelist when nothing valid is sent.
|
|
135
150
|
try {
|
|
136
151
|
const r = await call("cli-profile-update", { id: id || "00000000-0000-0000-0000-000000000000", profile_type: type, read: true });
|
|
137
|
-
if (JSON_OUT) return out({ fields: r.allowed_fields });
|
|
152
|
+
if (JSON_OUT) return out({ fields: r.allowed_fields, enrichment_paths: r.allowed_enrichment_paths ?? [] });
|
|
138
153
|
heading(`Fields you can set on a ${type}`);
|
|
139
154
|
log(" " + r.allowed_fields.join(", "));
|
|
155
|
+
if (r.allowed_enrichment_paths?.length) {
|
|
156
|
+
heading("Extra fields (use --set-enrichment path=value)");
|
|
157
|
+
log(" " + r.allowed_enrichment_paths.join(", "));
|
|
158
|
+
}
|
|
140
159
|
return;
|
|
141
160
|
} catch (e) {
|
|
142
161
|
const fields = e.data?.allowed_fields;
|
|
@@ -150,26 +169,77 @@ async function cmdProfileUpdate(type, id) {
|
|
|
150
169
|
}
|
|
151
170
|
return;
|
|
152
171
|
}
|
|
153
|
-
if (!id) { fail(`Usage: setu ${type} update <id> --set field=value`); process.exit(1); }
|
|
172
|
+
if (!id) { fail(`Usage: setu ${type} update <id> [--set field=value] [--booking <url>] [--website <url>]`); process.exit(1); }
|
|
154
173
|
|
|
155
|
-
const pairs = asList(flags.set);
|
|
156
|
-
if (!pairs.length) { fail("Pass at least one --set field=value (or --fields to see what you can set)."); process.exit(1); }
|
|
157
174
|
const set = {};
|
|
175
|
+
const pairs = asList(flags.set);
|
|
158
176
|
for (const pair of pairs) {
|
|
159
177
|
const raw = String(pair);
|
|
160
178
|
const eq = raw.indexOf("=");
|
|
161
179
|
if (eq < 1) { fail(`Bad --set value: "${raw}". Use field=value.`); process.exit(1); }
|
|
162
180
|
set[raw.slice(0, eq).trim()] = raw.slice(eq + 1);
|
|
163
181
|
}
|
|
182
|
+
const enrichmentSet = {};
|
|
183
|
+
for (const pair of asList(flags["set-enrichment"])) {
|
|
184
|
+
const raw = String(pair);
|
|
185
|
+
const eq = raw.indexOf("=");
|
|
186
|
+
if (eq < 1) { fail(`Bad --set-enrichment value: "${raw}". Use path=value.`); process.exit(1); }
|
|
187
|
+
enrichmentSet[raw.slice(0, eq).trim()] = raw.slice(eq + 1);
|
|
188
|
+
}
|
|
189
|
+
if (flags.booking !== undefined) set.booking_url = flags.booking === true ? "" : String(flags.booking);
|
|
190
|
+
if (flags.website !== undefined) set[type === "dentist" ? "website_url" : "website"] = flags.website === true ? "" : String(flags.website);
|
|
191
|
+
if (!Object.keys(set).length && !Object.keys(enrichmentSet).length) { fail("Pass at least one --set field=value, --set-enrichment path=value, --booking <url> or --website <url> (or --fields to see what you can set)."); process.exit(1); }
|
|
164
192
|
|
|
165
|
-
const res = await call("cli-profile-update", {
|
|
193
|
+
const res = await call("cli-profile-update", {
|
|
194
|
+
id,
|
|
195
|
+
profile_type: type,
|
|
196
|
+
set,
|
|
197
|
+
...(Object.keys(enrichmentSet).length ? { enrichment_set: enrichmentSet } : {}),
|
|
198
|
+
});
|
|
166
199
|
if (JSON_OUT) return out(res);
|
|
167
200
|
ok(`${res.name} — updated ${res.updated.join(", ")}`);
|
|
168
201
|
if (res.rejected?.length) warn(`Ignored unknown field(s): ${res.rejected.join(", ")}`);
|
|
169
202
|
}
|
|
170
203
|
|
|
204
|
+
const MIME_BY_EXT = {
|
|
205
|
+
jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png",
|
|
206
|
+
webp: "image/webp", svg: "image/svg+xml", gif: "image/gif",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** A local path becomes base64; a URL is fetched server-side. */
|
|
210
|
+
function uploadPayload(src) {
|
|
211
|
+
const value = String(src);
|
|
212
|
+
if (/^https?:\/\//i.test(value)) return { source_url: value };
|
|
213
|
+
const ext = value.split(".").pop().toLowerCase();
|
|
214
|
+
return {
|
|
215
|
+
data: readFileSync(value).toString("base64"),
|
|
216
|
+
content_type: MIME_BY_EXT[ext] || "image/jpeg",
|
|
217
|
+
filename: value.split(/[\\/]/).pop(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function uploadImages(type, id, sources, attach) {
|
|
222
|
+
const urls = [];
|
|
223
|
+
for (const src of sources) {
|
|
224
|
+
const s = spinner(`Uploading ${src}`);
|
|
225
|
+
try {
|
|
226
|
+
const res = await call("cli-profile-upload", { id, profile_type: type, attach, ...uploadPayload(src) });
|
|
227
|
+
s.stop();
|
|
228
|
+
urls.push(res.url);
|
|
229
|
+
if (!JSON_OUT) ok(`${attach === "logo" ? "Logo" : "Photo"} uploaded — ${c.dim(res.url)}`);
|
|
230
|
+
} catch (e) { s.stop(); throw e; }
|
|
231
|
+
}
|
|
232
|
+
return urls;
|
|
233
|
+
}
|
|
234
|
+
|
|
171
235
|
async function cmdProfileImages(type, id) {
|
|
172
|
-
if (!id) { fail(`Usage: setu ${type} images <id> [--add <url>] [--remove <url|index>] [--order "a,b"] [--logo <url>]`); process.exit(1); }
|
|
236
|
+
if (!id) { fail(`Usage: setu ${type} images <id> [--add <url>] [--upload <file>] [--remove <url|index>] [--order "a,b"] [--logo <url>] [--upload-logo <file>]`); process.exit(1); }
|
|
237
|
+
|
|
238
|
+
const uploads = splitList(flags.upload);
|
|
239
|
+
const logoUploads = splitList(flags["upload-logo"]);
|
|
240
|
+
const uploaded = [];
|
|
241
|
+
if (uploads.length) uploaded.push(...await uploadImages(type, id, uploads, "gallery"));
|
|
242
|
+
if (logoUploads.length) await uploadImages(type, id, logoUploads.slice(0, 1), "logo");
|
|
173
243
|
|
|
174
244
|
const images = {};
|
|
175
245
|
if (splitList(flags.add).length) images.add = splitList(flags.add);
|
|
@@ -501,34 +571,170 @@ async function cmdDentistCreate() {
|
|
|
501
571
|
town: typeof flags.town === "string" ? flags.town : undefined,
|
|
502
572
|
postcode: typeof flags.postcode === "string" ? flags.postcode : undefined,
|
|
503
573
|
email: typeof flags.email === "string" ? flags.email : undefined,
|
|
574
|
+
phone: typeof flags.phone === "string" ? flags.phone : undefined,
|
|
575
|
+
website_url: typeof flags.website === "string" ? flags.website : undefined,
|
|
576
|
+
booking_url: typeof flags.booking === "string" ? flags.booking : undefined,
|
|
577
|
+
bio: typeof flags.bio === "string" ? flags.bio : undefined,
|
|
578
|
+
photo_url: typeof flags.photo === "string" ? flags.photo : undefined,
|
|
579
|
+
gallery: splitList(flags.gallery),
|
|
504
580
|
});
|
|
505
581
|
s.stop();
|
|
506
582
|
if (JSON_OUT) return out(res);
|
|
507
583
|
ok("Clinician seeded as unclaimed");
|
|
508
|
-
table([
|
|
584
|
+
table([
|
|
585
|
+
["Dentist", res.dentist_id],
|
|
586
|
+
["AI enriched", res.enriched ? "yes" : "no"],
|
|
587
|
+
["Photo", res.photo_url ?? "—"],
|
|
588
|
+
["Claim link", res.claim_url],
|
|
589
|
+
]);
|
|
509
590
|
} catch (e) { s.stop(); throw e; }
|
|
510
591
|
}
|
|
511
592
|
|
|
512
|
-
|
|
513
|
-
|
|
593
|
+
function printProfileList(res) {
|
|
594
|
+
heading(`Seeded ${res.profile_type === "dentist" ? "clinicians" : "practices"} (${res.items.length})`);
|
|
595
|
+
for (const p of res.items) {
|
|
596
|
+
const state = p.claim_status === "claimed" ? c.green("claimed") : p.is_live ? c.green("live") : c.yellow(p.claim_status);
|
|
597
|
+
log(` ${state.padEnd(20)} ${String(p.name).slice(0, 38).padEnd(40)} ${c.dim(p.id)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function cmdProfileList(type) {
|
|
602
|
+
const res = await call("cli-profile-status", {
|
|
603
|
+
list: true,
|
|
604
|
+
profile_type: type,
|
|
605
|
+
limit: flags.limit,
|
|
606
|
+
search: typeof flags.search === "string" ? flags.search : undefined,
|
|
607
|
+
practice_id: type === "dentist" && typeof flags.practice === "string" ? flags.practice : undefined,
|
|
608
|
+
});
|
|
609
|
+
if (JSON_OUT) return out(res);
|
|
610
|
+
printProfileList(res);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function cmdStatus(id, type = flags.dentist ? "dentist" : "practice") {
|
|
614
|
+
const res = await call(
|
|
615
|
+
"cli-profile-status",
|
|
616
|
+
id
|
|
617
|
+
? { id, profile_type: type, full: !!flags.full }
|
|
618
|
+
: { list: true, profile_type: type, limit: flags.limit, search: typeof flags.search === "string" ? flags.search : undefined },
|
|
619
|
+
);
|
|
514
620
|
if (JSON_OUT) return out(res);
|
|
515
|
-
if (res.items)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
621
|
+
if (res.items) return printProfileList(res);
|
|
622
|
+
|
|
623
|
+
const p = res.profile ?? res.practice;
|
|
624
|
+
heading(p.name);
|
|
625
|
+
table([
|
|
626
|
+
["Type", res.profile_type],
|
|
627
|
+
["Status", p.claim_status],
|
|
628
|
+
["Live", p.is_live ? "yes" : "no"],
|
|
629
|
+
["Source", `${p.source} (${p.created_by_operator ?? "—"})`],
|
|
630
|
+
["Claim link", p.claim_url],
|
|
631
|
+
]);
|
|
632
|
+
|
|
633
|
+
if (flags.full) {
|
|
634
|
+
const gallery = res.gallery ?? [];
|
|
635
|
+
heading(`Photos (${gallery.length})`);
|
|
636
|
+
gallery.forEach((url, i) => log(` ${String(i).padEnd(3)} ${c.dim(url)}`));
|
|
637
|
+
log(` ${c.dim(res.profile_type === "dentist" ? "main photo" : "logo")}: ${res.logo ?? c.dim("none")}`);
|
|
638
|
+
|
|
639
|
+
const pricing = res.pricing ?? [];
|
|
640
|
+
heading(`Prices (${pricing.length})`);
|
|
641
|
+
for (const row of pricing) {
|
|
642
|
+
const amount = row.amount_to ? `${row.amount}–${row.amount_to}` : row.amount ?? "—";
|
|
643
|
+
log(` ${String(row.label).slice(0, 38).padEnd(40)} ${amount}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (res.dentists) {
|
|
647
|
+
heading(`Clinicians (${res.dentists.length})`);
|
|
648
|
+
for (const d of res.dentists) log(` ${String(d.name).slice(0, 38).padEnd(40)} ${c.dim(d.id)}`);
|
|
520
649
|
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
heading(`Claims (${res.claims.length})`);
|
|
653
|
+
for (const cl of res.claims) log(` ${cl.status.padEnd(10)} ${cl.claimant_email} ${c.dim(cl.method)}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function printClaimRow(cl) {
|
|
657
|
+
const who = String(cl.claimant_email ?? "—").slice(0, 30).padEnd(32);
|
|
658
|
+
const name = String(cl.profile?.name ?? cl.profile_type ?? "—").slice(0, 26).padEnd(28);
|
|
659
|
+
const flag = cl.awaiting_signin ? c.yellow(" (not signed in)") : "";
|
|
660
|
+
log(` ${String(cl.status).padEnd(9)} ${who} ${name} ${c.dim(cl.method ?? "")}${flag}`);
|
|
661
|
+
log(` ${c.dim(cl.id)}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function cmdClaims(sub, arg) {
|
|
665
|
+
if (sub === "show") {
|
|
666
|
+
if (!arg) throw new Error("Usage: setu claims show <claim-id>");
|
|
667
|
+
const res = await call("cli-claims", { action: "detail", claim_id: arg });
|
|
668
|
+
if (JSON_OUT) return out(res);
|
|
669
|
+
heading(`Claim ${res.claim.id}`);
|
|
670
|
+
table([
|
|
671
|
+
["Status", res.claim.status],
|
|
672
|
+
["Claimant", res.claim.claimant_email],
|
|
673
|
+
["Method", res.claim.method],
|
|
674
|
+
["Attempts", res.claim.attempts],
|
|
675
|
+
["Signed in", res.claim.claimant_user_id ? "yes" : "no"],
|
|
676
|
+
["Profile", res.profile ? `${res.profile.name} (${res.claim.profile_type})` : "—"],
|
|
677
|
+
["Readiness", res.profile ? `${res.profile.score}%` : "—"],
|
|
678
|
+
["Missing", res.profile?.missing?.join(", ")],
|
|
679
|
+
["Claim link", res.profile?.claim_url],
|
|
680
|
+
]);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (sub === "approve" || sub === "decline" || sub === "reject") {
|
|
685
|
+
if (!arg) throw new Error(`Usage: setu claims ${sub} <claim-id>`);
|
|
686
|
+
if (!flags.yes && !JSON_OUT) {
|
|
687
|
+
const go = await confirm(`${sub === "approve" ? "Approve" : "Decline"} claim ${arg}?`);
|
|
688
|
+
if (!go) return warn("Cancelled.");
|
|
689
|
+
}
|
|
690
|
+
const res = await call("cli-claims", {
|
|
691
|
+
action: sub === "approve" ? "approve" : "decline",
|
|
692
|
+
claim_id: arg,
|
|
693
|
+
reason: typeof flags.reason === "string" ? flags.reason : undefined,
|
|
694
|
+
});
|
|
695
|
+
if (JSON_OUT) return out(res);
|
|
696
|
+
return ok(`Claim ${res.status} — ${res.claim_id}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (sub && sub !== "list") throw new Error(`Unknown command: claims ${sub}`);
|
|
700
|
+
|
|
701
|
+
const status = typeof flags.status === "string" ? flags.status : "pending";
|
|
702
|
+
const profileType = flags.dentist ? "dentist" : undefined;
|
|
703
|
+
if (sub === "list" || (typeof flags.status === "string" && flags.status !== "pending")) {
|
|
704
|
+
const res = await call("cli-claims", { action: "list", status, profile_type: profileType, limit: flags.limit });
|
|
705
|
+
if (JSON_OUT) return out(res);
|
|
706
|
+
heading(`Claims — ${status} (${res.claims.length})`);
|
|
707
|
+
res.claims.forEach(printClaimRow);
|
|
708
|
+
if (!res.claims.length) log(c.dim(" none"));
|
|
521
709
|
return;
|
|
522
710
|
}
|
|
523
|
-
|
|
711
|
+
|
|
712
|
+
const res = await call("cli-claims", {
|
|
713
|
+
action: "dashboard",
|
|
714
|
+
profile_type: profileType,
|
|
715
|
+
ready_only: !!flags.ready,
|
|
716
|
+
limit: flags.limit,
|
|
717
|
+
});
|
|
718
|
+
if (JSON_OUT) return out(res);
|
|
719
|
+
|
|
720
|
+
heading("Claims");
|
|
524
721
|
table([
|
|
525
|
-
["
|
|
526
|
-
["
|
|
527
|
-
["
|
|
528
|
-
["Claim link", res.practice.claim_url],
|
|
722
|
+
["Pending", res.counts.pending],
|
|
723
|
+
["Approved", res.counts.approved],
|
|
724
|
+
["Rejected", res.counts.rejected],
|
|
529
725
|
]);
|
|
530
|
-
|
|
531
|
-
|
|
726
|
+
|
|
727
|
+
heading(`Pending review (${res.pending_claims.length})`);
|
|
728
|
+
res.pending_claims.forEach(printClaimRow);
|
|
729
|
+
if (!res.pending_claims.length) log(c.dim(" nothing waiting"));
|
|
730
|
+
|
|
731
|
+
heading(`Unclaimed profiles — ${res.unclaimed.ready}/${res.unclaimed.total} ready, ${res.unclaimed.not_live} ready but offline`);
|
|
732
|
+
for (const u of res.unclaimed.items) {
|
|
733
|
+
const badge = u.ready ? c.green("ready") : c.yellow("gaps ");
|
|
734
|
+
log(` ${badge} ${String(u.readiness_score + "%").padStart(4)} ${String(u.name).slice(0, 30).padEnd(32)} ${c.dim(u.profile_type)}${u.is_live ? "" : c.dim(" offline")}`);
|
|
735
|
+
if (u.missing.length) log(` ${c.dim("missing: " + u.missing.join(", "))}`);
|
|
736
|
+
}
|
|
737
|
+
if (!res.unclaimed.items.length) log(c.dim(" none"));
|
|
532
738
|
}
|
|
533
739
|
|
|
534
740
|
async function main() {
|
|
@@ -544,6 +750,7 @@ async function main() {
|
|
|
544
750
|
case "logout": return cmdLogout();
|
|
545
751
|
case "whoami": return cmdWhoami();
|
|
546
752
|
case "status": return cmdStatus(sub);
|
|
753
|
+
case "claims": return cmdClaims(sub, arg);
|
|
547
754
|
case "practice":
|
|
548
755
|
switch (sub) {
|
|
549
756
|
case "create": return cmdPracticeCreate();
|
|
@@ -555,6 +762,7 @@ async function main() {
|
|
|
555
762
|
case "update": return cmdProfileUpdate("practice", arg);
|
|
556
763
|
case "images": return cmdProfileImages("practice", arg);
|
|
557
764
|
case "pricing": return cmdProfilePricing("practice", arg);
|
|
765
|
+
case "list": return cmdProfileList("practice");
|
|
558
766
|
default:
|
|
559
767
|
fail(`Unknown command: practice ${sub ?? ""}`.trim());
|
|
560
768
|
usage();
|
|
@@ -563,6 +771,8 @@ async function main() {
|
|
|
563
771
|
}
|
|
564
772
|
case "dentist":
|
|
565
773
|
if (sub === "create") return cmdDentistCreate();
|
|
774
|
+
if (sub === "list") return cmdProfileList("dentist");
|
|
775
|
+
if (sub === "status") return cmdStatus(arg, "dentist");
|
|
566
776
|
if (sub === "update") return cmdProfileUpdate("dentist", arg);
|
|
567
777
|
if (sub === "images") return cmdProfileImages("dentist", arg);
|
|
568
778
|
if (sub === "pricing") return cmdProfilePricing("dentist", arg);
|