Add full sprint pricing workflow and supporting infrastructure: - New sidebar navigation entry for the sprint dashboard - PocketBase category collection schema and migration script - Server API routes for sprint item claims, attachments, counts, and file proxying - Dev identity utilities and UI switcher for multi-user testing - Reusable FilterTabs component replacing ItemStatusTabs - Updated TypeScript types for sprint-related entities Also include: - Fix PocketBase file URL method casing (getUrl → getURL) across codebase - Add development utility, smoke test, and helper scripts - Update package.json with new sprint migration command
124 lines
No EOL
4.2 KiB
TypeScript
124 lines
No EOL
4.2 KiB
TypeScript
/* eslint-disable */
|
|
/**
|
|
* Resolve a list of `price_list` PB record ids to their actual data,
|
|
* with the supplier name resolved from the `suppliers` collection.
|
|
*
|
|
* Usage:
|
|
* env PB_ADMIN_EMAIL=… PB_ADMIN_PASSWORD=… NEXT_PUBLIC_POCKETBASE_URL=…
|
|
* npx tsx scripts/utils/resolve-price-list-ids.ts id1 id2 id3 …
|
|
*
|
|
* Or pipe ids via stdin (one per line). Or pass them as a comma-
|
|
* separated list. Output is paste-ready text for an LLM.
|
|
*
|
|
* The output preserves the input order (so if your LLM is doing
|
|
* a re-rank, the input order is the canonical "before" view).
|
|
*/
|
|
import PocketBase from "pocketbase";
|
|
|
|
async function main() {
|
|
const URL = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
|
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
|
|
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
|
|
if (!URL || !ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
|
throw new Error(
|
|
"Set PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, NEXT_PUBLIC_POCKETBASE_URL."
|
|
);
|
|
}
|
|
|
|
// Pull ids from argv (space-separated), stdin, or a JSON list
|
|
// passed as the first arg. Keeps the call ergonomic.
|
|
const rawArg = process.argv.slice(2).join(" ").trim();
|
|
let ids: string[] = [];
|
|
if (rawArg) {
|
|
// Try JSON first (handles {"ranked_ids": [...]} shapes too)
|
|
try {
|
|
const parsed = JSON.parse(rawArg);
|
|
ids = Array.isArray(parsed)
|
|
? parsed
|
|
: Array.isArray(parsed.ranked_ids)
|
|
? parsed.ranked_ids
|
|
: [];
|
|
} catch {
|
|
ids = rawArg.split(/[\s,]+/).filter(Boolean);
|
|
}
|
|
}
|
|
if (ids.length === 0) {
|
|
console.error("No ids supplied. Pass them as argv, e.g.:");
|
|
console.error(' npx tsx scripts/utils/resolve-price-list-ids.ts id1 id2 id3');
|
|
process.exit(1);
|
|
}
|
|
|
|
const pb = new PocketBase(URL);
|
|
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
|
|
|
// Batch fetch the price_list rows in one filter call. PB 0.26 has
|
|
// no `IN` operator, so OR-chain the equalities. 30 ids is well
|
|
// under the filter-length budget.
|
|
const filter =
|
|
"(" + ids.map((id) => `id = "${id.replace(/"/g, '\\"')}"`).join(" || ") + ")";
|
|
const rows = await pb.collection("price_list").getFullList({
|
|
filter,
|
|
fields: "id,sku,description,cost_price,selling_price,unit,supplier,brand,category,subcategory,sku_normalised",
|
|
});
|
|
|
|
// Index by id so we can re-emit in input order even if PB returns
|
|
// them out of order.
|
|
const byId = new Map(rows.map((r) => [r.id, r]));
|
|
|
|
// Resolve supplier ids → supplier names in one shot. Collect all
|
|
// unique supplier ids first so we do at most a handful of lookups.
|
|
const supplierIds = Array.from(
|
|
new Set(
|
|
rows
|
|
.map((r) => (r.supplier || "").trim())
|
|
.filter((s) => s && s.length === 15) // PB record ids are 15 chars
|
|
)
|
|
);
|
|
const supplierNames = new Map<string, string>();
|
|
if (supplierIds.length > 0) {
|
|
const sFilter =
|
|
"(" +
|
|
supplierIds
|
|
.map((id) => `id = "${id.replace(/"/g, '\\"')}"`)
|
|
.join(" || ") +
|
|
")";
|
|
const suppliers = await pb.collection("suppliers").getFullList({
|
|
filter: sFilter,
|
|
fields: "id,suppliername",
|
|
});
|
|
for (const s of suppliers) supplierNames.set(s.id, s.suppliername);
|
|
}
|
|
|
|
// Emit paste-ready list. Each row: `[n] SKU | description | Rcost | supplier`
|
|
console.log(`# ${ids.length} price_list rows (input order preserved)\n`);
|
|
let emitted = 0;
|
|
for (const id of ids) {
|
|
const r = byId.get(id);
|
|
if (!r) {
|
|
console.log(`[${++emitted}] id=${id} -- NOT FOUND --`);
|
|
continue;
|
|
}
|
|
const supplierDisplay =
|
|
supplierNames.get(r.supplier || "") ||
|
|
(r.supplier ? r.supplier : "—");
|
|
const sku = r.sku || "(no sku)";
|
|
const desc = (r.description || "(no description)")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
const cost = Number(r.cost_price) || 0;
|
|
const unit = r.unit || "";
|
|
const brand = r.brand || "";
|
|
const category = r.category || "";
|
|
const meta = [brand, category].filter(Boolean).join(" · ");
|
|
const costStr = `R${cost.toFixed(2)}${unit ? "/" + unit : ""}`;
|
|
console.log(
|
|
`[${++emitted}] ${sku} | ${desc} | ${costStr} | ${supplierDisplay}` +
|
|
(meta ? ` | ${meta}` : "")
|
|
);
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e);
|
|
process.exit(1);
|
|
}); |