Fix report pagination and rebuild saved plan navigation
Test and deploy / test-and-deploy (push) Successful in 1m43s
Test and deploy / test-and-deploy (push) Successful in 1m43s
This commit is contained in:
1 parent
824a964f9d
commit
5f3a015ea2
20 files changed
+1480
-809
No files matched your search
+38
-4
@@ -1,3 +1,4 @@
|
||||
import { sanitizeObservations } from "../shared/roast-comparison.js";
|
||||
import express from "express";
|
||||
import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
@@ -46,6 +47,7 @@ const PUBLIC_SHELL_FILES = new Set([
|
||||
"/inventory.html",
|
||||
"/cupping.html",
|
||||
"/roasts.html",
|
||||
"/plans.html",
|
||||
"/beans.html",
|
||||
"/brews.html",
|
||||
"/gear.html",
|
||||
@@ -143,6 +145,7 @@ export function createApp({
|
||||
req.path === "/inventory" ||
|
||||
req.path === "/cupping" ||
|
||||
req.path === "/roasts" ||
|
||||
req.path === "/plans" ||
|
||||
req.path === "/beans" ||
|
||||
req.path === "/brews" ||
|
||||
req.path === "/gear" ||
|
||||
@@ -427,6 +430,7 @@ export function createApp({
|
||||
}
|
||||
});
|
||||
for (const [route, file] of [
|
||||
["/plans", "plans.html"],
|
||||
["/beans", "beans.html"],
|
||||
["/brews", "brews.html"],
|
||||
["/gear", "gear.html"],
|
||||
@@ -749,6 +753,23 @@ export function createApp({
|
||||
// ─── Plans ─────────────────────────────────────────────────────────────
|
||||
app.get("/api/plans", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
if (req.query.view === "summary") {
|
||||
const page = Number(req.query.page ?? 1), limit = Number(req.query.limit ?? 25);
|
||||
if (!Number.isSafeInteger(page) || page < 1 || !Number.isSafeInteger((page - 1) * limit) || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
||||
return res.status(400).json({ok:false, code:"bad_pagination"});
|
||||
const params = [req.user.id], where = ["user_id=$1"];
|
||||
const add = (value, clause) => { params.push(value); where.push(clause.replaceAll("?", "$"+params.length)); };
|
||||
const query = typeof req.query.q === "string" ? req.query.q.trim().slice(0,200) : "";
|
||||
if (query) add("%"+query.replace(/[\\%_]/g, "\\$&")+"%", "(name ILIKE ? OR plan->'fields'->>'0.1' ILIKE ? OR plan->'fields'->>'1.1' ILIKE ? OR plan->'fields'->>'1.3' ILIKE ?)");
|
||||
for (const [key,field] of [["process","3.1"],["level","4.1"]]) {
|
||||
if (typeof req.query[key] === "string" && req.query[key]) add(req.query[key].slice(0,80), `plan->'fields'->>'${field}' = ?`);
|
||||
}
|
||||
const predicate = where.join(" AND ");
|
||||
const total = Number((await db.query(`SELECT COUNT(*) AS total FROM roast_plans WHERE ${predicate}`, params)).rows[0].total);
|
||||
const sort = new Map([["updated","updated_at DESC, id DESC"],["oldest","updated_at ASC, id ASC"],["name","name ASC, id ASC"]]).get(req.query.sort) ?? "updated_at DESC, id DESC";
|
||||
const rows = (await db.query(`SELECT id,name,created_at,updated_at,plan->'fields' AS fields FROM roast_plans WHERE ${predicate} ORDER BY ${sort} LIMIT $${params.length+1} OFFSET $${params.length+2}`, [...params,limit,(page-1)*limit])).rows;
|
||||
return res.json({ok:true, plans:rows.map(row=>({id:row.id,name:row.name,title:row.name||row.fields?.["0.1"]||"Untitled plan",coffee:row.fields?.["0.1"]||"",origin:row.fields?.["1.3"]||"",cultivar:row.fields?.["1.1"]||"",process:row.fields?.["3.1"]||"",level:row.fields?.["4.1"]||"",batchG:row.fields?.["0.4"]||"",updated_at:row.updated_at,created_at:row.created_at})),total,page,limit});
|
||||
}
|
||||
res.json({
|
||||
ok: true,
|
||||
plans: (
|
||||
@@ -762,6 +783,13 @@ export function createApp({
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/plans/:id", requireAuth, requireUuidParam("id"), async (req,res,next)=>{
|
||||
try {
|
||||
const row=(await db.query("SELECT id,plan,name,created_at,updated_at FROM roast_plans WHERE id=$1 AND user_id=$2",[req.params.id,req.user.id])).rows[0];
|
||||
if(!row) return res.status(404).json({ok:false,code:"not_found"});
|
||||
res.json({ok:true,plan:row});
|
||||
} catch(error) {next(error);}
|
||||
});
|
||||
app.post("/api/plans", requireAuth, csrf, async (req, res, next) => {
|
||||
try {
|
||||
if (!req.body.plan || typeof req.body.plan !== "object")
|
||||
@@ -1497,7 +1525,7 @@ export function createApp({
|
||||
createdAt: row.created_at,
|
||||
};
|
||||
return full
|
||||
? { ...base, parsed, evaluation, after: row.after ?? {}, plan: row.plan ?? null }
|
||||
? { ...base, parsed, evaluation, observations: row.observations ?? {}, after: row.after ?? {}, plan: row.plan ?? null }
|
||||
: base;
|
||||
};
|
||||
// "After the roast" observations recorded against an actual roast (moved here from the
|
||||
@@ -1782,6 +1810,11 @@ export function createApp({
|
||||
params.push(roastPlanId);
|
||||
sets.push(`roast_plan_id=$${params.length}`);
|
||||
}
|
||||
if (req.body.observations !== undefined) {
|
||||
const observations = sanitizeObservations(req.body.observations);
|
||||
if (!observations) return res.status(400).json({ok:false,code:"bad_observations"});
|
||||
params.push(observations);sets.push(`observations=$${params.length}`);
|
||||
}
|
||||
if (req.body.after !== undefined) {
|
||||
const after = sanitizeAfter(req.body.after);
|
||||
if (!after)
|
||||
@@ -1793,7 +1826,7 @@ export function createApp({
|
||||
return res.status(400).json({ ok: false, code: "bad_request" });
|
||||
params.push(req.params.id, req.user.id);
|
||||
const r = await db.query(
|
||||
`UPDATE actual_roasts SET ${sets.join(", ")}, updated_at=now() WHERE id=$${params.length - 1} AND user_id=$${params.length} RETURNING roaster_id, roast_plan_id, after`,
|
||||
`UPDATE actual_roasts SET ${sets.join(", ")}, updated_at=now() WHERE id=$${params.length - 1} AND user_id=$${params.length} RETURNING roaster_id, roast_plan_id, after, observations`,
|
||||
params,
|
||||
);
|
||||
if (!r.rowCount)
|
||||
@@ -1803,6 +1836,7 @@ export function createApp({
|
||||
roasterId: r.rows[0].roaster_id,
|
||||
roastPlanId: r.rows[0].roast_plan_id,
|
||||
after: r.rows[0].after,
|
||||
observations: r.rows[0].observations,
|
||||
});
|
||||
} catch (e) {
|
||||
next(e);
|
||||
@@ -2855,7 +2889,7 @@ export function createApp({
|
||||
"actual_roasts",
|
||||
[
|
||||
"id", "user_id", "roast_plan_id", "roaster_id", "filename", "original_content", "parsed",
|
||||
"after", "evaluation", "evaluation_status", "evaluation_error", "created_at", "updated_at",
|
||||
"observations", "after", "evaluation", "evaluation_status", "evaluation_error", "created_at", "updated_at",
|
||||
],
|
||||
],
|
||||
[
|
||||
@@ -2936,7 +2970,7 @@ export function createApp({
|
||||
for (const row of rows) {
|
||||
const params = columns.map((column) => {
|
||||
const value = row[column];
|
||||
if (value === undefined) return null;
|
||||
if (value === undefined) return table === "actual_roasts" && column === "observations" ? "{}" : null;
|
||||
// jsonb columns arrive as objects; everything else is scalar.
|
||||
return value !== null && typeof value === "object"
|
||||
? JSON.stringify(value)
|
||||
|
||||
Reference in new issue
Block a user