mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 12:43:18 +00:00
Pre-computed merge for change #18
This commit is contained in:
commit
c6d67438cd
6 changed files with 138 additions and 23 deletions
|
|
@ -1355,8 +1355,9 @@ app.get("/v1/badge-jobs/:id/pdf", ...requireCoreAuth, requirePermission("documen
|
|||
{ job_id: id },
|
||||
{ route },
|
||||
);
|
||||
if (!jobEnv.ok) return respondRpc(c, jobEnv);
|
||||
const pdfPath = jobEnv.data?.job?.pdf_path || badgeJobPdfKey(id);
|
||||
const pdfPath = jobEnv.ok
|
||||
? (jobEnv.data?.job?.pdf_path || badgeJobPdfKey(id))
|
||||
: badgeJobPdfKey(id);
|
||||
const bytes = await getObject(pdfPath);
|
||||
c.header("Content-Type", "application/pdf");
|
||||
c.header("Content-Disposition", `attachment; filename="gafetes-${id}.pdf"`);
|
||||
|
|
|
|||
62
api/pdf.ts
62
api/pdf.ts
|
|
@ -101,16 +101,34 @@ export async function generateBadgePdf(
|
|||
workerIds: number[],
|
||||
): Promise<Uint8Array> {
|
||||
const project = await db.prepare(
|
||||
"SELECT name, code, theme_id, logo_left_path, logo_right_path FROM projects WHERE id = ?",
|
||||
`SELECT p.name, p.code, p.theme_id, p.logo_left_path, p.logo_right_path, p.tenant_id,
|
||||
COALESCE(NULLIF(c.nombre_comercial, ''), NULLIF(c.name, '')) AS company_name
|
||||
FROM projects p
|
||||
LEFT JOIN companies c ON c.id = p.company_id
|
||||
WHERE p.id = ?`,
|
||||
).get(projectId) as {
|
||||
name: string;
|
||||
code: string;
|
||||
theme_id: string;
|
||||
logo_left_path: string | null;
|
||||
logo_right_path: string | null;
|
||||
tenant_id?: number | null;
|
||||
company_name?: string | null;
|
||||
} | undefined;
|
||||
if (!project) throw new Error("Proyecto no encontrado");
|
||||
|
||||
let companyName = String(project.company_name || "").trim();
|
||||
if (!companyName && project.tenant_id) {
|
||||
const principal = await db.prepare(
|
||||
`SELECT COALESCE(NULLIF(nombre_comercial, ''), name) AS company_name
|
||||
FROM companies
|
||||
WHERE tenant_id = ? AND kind = 'principal'
|
||||
ORDER BY id
|
||||
LIMIT 1`,
|
||||
).get(project.tenant_id) as { company_name?: string } | undefined;
|
||||
companyName = String(principal?.company_name || "").trim();
|
||||
}
|
||||
|
||||
const workers = await db.prepare(
|
||||
`SELECT w.id, w.first_name, w.middle_name, w.last_name_p, w.last_name_m,
|
||||
w.curp, w.nss, w.blood_type, w.position,
|
||||
|
|
@ -156,7 +174,7 @@ export async function generateBadgePdf(
|
|||
const photo = await embedMaybe(pdf, await loadCurrentPhoto(db, w.id));
|
||||
const qrPng = await badgeQrPng(w.curp);
|
||||
const qrImg = await pdf.embedPng(qrPng);
|
||||
drawFront(page, font, fontBold, x, topY, w, photo, logoL, logoR);
|
||||
drawFront(page, font, fontBold, x, topY, w, photo, logoL, logoR, companyName);
|
||||
drawBack(page, font, fontBold, x, botY, w, qrImg, project.name, project.code);
|
||||
}
|
||||
}
|
||||
|
|
@ -164,6 +182,22 @@ export async function generateBadgePdf(
|
|||
return await pdf.save();
|
||||
}
|
||||
|
||||
function drawFittedLeft(
|
||||
page: PDFPage,
|
||||
font: PDFFont,
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
maxWidth: number,
|
||||
size: number,
|
||||
color = rgb(0, 0, 0),
|
||||
) {
|
||||
if (!text) return;
|
||||
let s = size;
|
||||
while (s > 5 && font.widthOfTextAtSize(text, s) > maxWidth) s -= 0.4;
|
||||
page.drawText(text, { x, y, size: s, font, color });
|
||||
}
|
||||
|
||||
function drawFront(
|
||||
page: PDFPage,
|
||||
font: PDFFont,
|
||||
|
|
@ -174,6 +208,7 @@ function drawFront(
|
|||
photo: PDFImage | null,
|
||||
logoL: PDFImage | null,
|
||||
logoR: PDFImage | null,
|
||||
companyName: string,
|
||||
) {
|
||||
page.drawRectangle({
|
||||
x,
|
||||
|
|
@ -193,12 +228,15 @@ function drawFront(
|
|||
height: logoL.height * scale,
|
||||
});
|
||||
} else {
|
||||
page.drawText("Construcciones Arctec", {
|
||||
x: x + 6,
|
||||
y: y + CARD_H - 22,
|
||||
size: 7,
|
||||
font: fontBold,
|
||||
});
|
||||
drawFittedLeft(
|
||||
page,
|
||||
fontBold,
|
||||
companyName.toUpperCase(),
|
||||
x + 6,
|
||||
y + CARD_H - 22,
|
||||
CARD_W / 2 - 10,
|
||||
7,
|
||||
);
|
||||
}
|
||||
if (logoR) {
|
||||
const scale = Math.min((CARD_W / 2 - 8) / logoR.width, (logoH - 4) / logoR.height);
|
||||
|
|
@ -208,14 +246,6 @@ function drawFront(
|
|||
width: logoR.width * scale,
|
||||
height: logoR.height * scale,
|
||||
});
|
||||
} else {
|
||||
page.drawText("Zendala", {
|
||||
x: x + CARD_W / 2 + 8,
|
||||
y: y + CARD_H - 22,
|
||||
size: 9,
|
||||
font: fontBold,
|
||||
color: rgb(0.1, 0.25, 0.55),
|
||||
});
|
||||
}
|
||||
|
||||
const photoW = 3.5 * CM;
|
||||
|
|
|
|||
|
|
@ -38,5 +38,6 @@
|
|||
<include file="changesets/029-schema-work-program.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/030-rpc-work-program.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/031-project-doc-types-technical.sql" relativeToChangelogFile="true"/>
|
||||
<include file="changesets/032-fn-badge-job-get.sql" relativeToChangelogFile="true"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
|
|
|||
80
db/core/changesets/032-fn-badge-job-get.sql
Normal file
80
db/core/changesets/032-fn-badge-job-get.sql
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
--liquibase formatted sql
|
||||
-- PANELS · core · fn_badge_job_get: ORDER BY del subquery de personas
|
||||
|
||||
--changeset panel:core-032-fn-badge-job-get-people-order splitStatements:false
|
||||
CREATE OR REPLACE FUNCTION core.fn_badge_job_get(payload jsonb)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY INVOKER
|
||||
SET search_path = core
|
||||
AS $$
|
||||
DECLARE
|
||||
v_job_id bigint := COALESCE(
|
||||
NULLIF(payload->>'job_id', '')::bigint,
|
||||
NULLIF(payload->>'id', '')::bigint
|
||||
);
|
||||
v_job jsonb;
|
||||
v_people jsonb;
|
||||
BEGIN
|
||||
IF v_job_id IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'VALIDATION',
|
||||
'fn_badge_job_get: job_id es obligatorio',
|
||||
jsonb_build_object('fn', 'fn_badge_job_get', 'field', 'job_id')
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT to_jsonb(x)
|
||||
INTO v_job
|
||||
FROM (
|
||||
SELECT
|
||||
j.*,
|
||||
p.code AS project_code,
|
||||
p.name AS project_name
|
||||
FROM badge_jobs j
|
||||
JOIN projects p ON p.id = j.project_id
|
||||
WHERE j.id = v_job_id
|
||||
) x;
|
||||
|
||||
IF v_job IS NULL THEN
|
||||
RETURN core.rpc_err(
|
||||
'NOT_FOUND',
|
||||
format('fn_badge_job_get: job de gafetes id=%s no encontrado', v_job_id),
|
||||
jsonb_build_object('fn', 'fn_badge_job_get', 'job_id', v_job_id)
|
||||
);
|
||||
END IF;
|
||||
|
||||
SELECT COALESCE(jsonb_agg(to_jsonb(p) ORDER BY p.last_name_p, p.first_name), '[]'::jsonb)
|
||||
INTO v_people
|
||||
FROM (
|
||||
SELECT
|
||||
bp.job_id,
|
||||
bp.worker_id,
|
||||
bp.delivered,
|
||||
bp.delivered_at,
|
||||
w.first_name,
|
||||
w.last_name_p,
|
||||
w.last_name_m,
|
||||
w.position
|
||||
FROM badge_job_people bp
|
||||
JOIN workers w ON w.id = bp.worker_id
|
||||
WHERE bp.job_id = v_job_id::integer
|
||||
) p;
|
||||
|
||||
RETURN core.rpc_ok(
|
||||
jsonb_build_object('job', v_job, 'people', v_people),
|
||||
format('Job de gafetes #%s cargado: %s persona(s) en el lote',
|
||||
v_job_id, jsonb_array_length(v_people)),
|
||||
jsonb_build_object(
|
||||
'fn', 'fn_badge_job_get',
|
||||
'job_id', v_job_id,
|
||||
'people_count', jsonb_array_length(v_people)
|
||||
)
|
||||
);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN core.rpc_from_exception('fn_badge_job_get', SQLSTATE, SQLERRM);
|
||||
END;
|
||||
$$;
|
||||
|
||||
--changeset panel:core-032-fn-badge-job-get-grant endDelimiter:; splitStatements:true
|
||||
GRANT EXECUTE ON FUNCTION core.fn_badge_job_get(jsonb) TO panels_core_app;
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<article class="badge" :class="{ 'badge-back-rot': side === 'back' && rotated, fluid }">
|
||||
<template v-if="side === 'front'">
|
||||
<div class="badge-logos">
|
||||
<div class="logo-l">Construcciones<br />Arctec</div>
|
||||
<div class="logo-l">{{ companyName || "—" }}</div>
|
||||
<div class="logo-r">{{ projectName }}</div>
|
||||
</div>
|
||||
<div class="badge-photo">
|
||||
|
|
@ -47,6 +47,7 @@ const props = defineProps<{
|
|||
blood: string;
|
||||
riskColor: string;
|
||||
riskText: string;
|
||||
companyName?: string;
|
||||
projectName: string;
|
||||
projectCode: string;
|
||||
photoSrc?: string;
|
||||
|
|
|
|||
|
|
@ -149,7 +149,8 @@ const SAMPLE: Worker = {
|
|||
const { api, download } = useApi();
|
||||
const desk = useDesktop();
|
||||
const toast = useToast();
|
||||
const projects = ref<{ id: number; name: string; code?: string }[]>([]);
|
||||
const { user } = useAuth();
|
||||
const projects = ref<{ id: number; name: string; code?: string; company_name?: string }[]>([]);
|
||||
const projectId = ref<number | null>(null);
|
||||
const workers = ref<Worker[]>([]);
|
||||
const workerId = ref(0);
|
||||
|
|
@ -203,8 +204,9 @@ const cardProps = computed(() => {
|
|||
blood: (w.blood_type || "").toUpperCase(),
|
||||
riskColor: r?.color || w.risk_color || "#A20000",
|
||||
riskText: r?.text_color || w.risk_text || "#FFFFFF",
|
||||
projectName: (project.value?.name || "Zendala Cancún").toUpperCase(),
|
||||
projectCode: (project.value?.code || "PRY-0001").toUpperCase(),
|
||||
companyName: (project.value?.company_name || user.value?.company_name || "").toUpperCase(),
|
||||
projectName: (project.value?.name || "Obra").toUpperCase(),
|
||||
projectCode: (project.value?.code || "").toUpperCase(),
|
||||
photoSrc: w.id ? `/v1/workers/${w.id}/photo` : "",
|
||||
qrSrc: `/v1/badge-qr?curp=${encodeURIComponent(w.curp)}`,
|
||||
};
|
||||
|
|
@ -236,7 +238,7 @@ async function loadWorkers() {
|
|||
|
||||
async function load() {
|
||||
const [p, j, c] = await Promise.all([
|
||||
api<{ projects: { id: number; name: string; code?: string }[] }>("/v1/projects?status=activo"),
|
||||
api<{ projects: { id: number; name: string; code?: string; company_name?: string }[] }>("/v1/projects?status=activo"),
|
||||
api<{ jobs: Record<string, unknown>[] }>("/v1/badge-jobs"),
|
||||
api<{ risks: { code: string; label: string; color: string; text_color: string }[] }>("/v1/catalogs"),
|
||||
]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue