mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 13:23:17 +00:00
merge: #18 gafete job y empresa del tenant
Añade 032-fn-badge-job-get al changelog junto a los demás 032. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
commit
ad152d46f4
6 changed files with 138 additions and 23 deletions
|
|
@ -1383,8 +1383,9 @@ app.get("/v1/badge-jobs/:id/pdf", ...requireCoreAuth, requirePermission("documen
|
||||||
{ job_id: id },
|
{ job_id: id },
|
||||||
{ route },
|
{ route },
|
||||||
);
|
);
|
||||||
if (!jobEnv.ok) return respondRpc(c, jobEnv);
|
const pdfPath = jobEnv.ok
|
||||||
const pdfPath = jobEnv.data?.job?.pdf_path || badgeJobPdfKey(id);
|
? (jobEnv.data?.job?.pdf_path || badgeJobPdfKey(id))
|
||||||
|
: badgeJobPdfKey(id);
|
||||||
const bytes = await getObject(pdfPath);
|
const bytes = await getObject(pdfPath);
|
||||||
c.header("Content-Type", "application/pdf");
|
c.header("Content-Type", "application/pdf");
|
||||||
c.header("Content-Disposition", `attachment; filename="gafetes-${id}.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[],
|
workerIds: number[],
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const project = await db.prepare(
|
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 {
|
).get(projectId) as {
|
||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
theme_id: string;
|
theme_id: string;
|
||||||
logo_left_path: string | null;
|
logo_left_path: string | null;
|
||||||
logo_right_path: string | null;
|
logo_right_path: string | null;
|
||||||
|
tenant_id?: number | null;
|
||||||
|
company_name?: string | null;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
if (!project) throw new Error("Proyecto no encontrado");
|
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(
|
const workers = await db.prepare(
|
||||||
`SELECT w.id, w.first_name, w.middle_name, w.last_name_p, w.last_name_m,
|
`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,
|
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 photo = await embedMaybe(pdf, await loadCurrentPhoto(db, w.id));
|
||||||
const qrPng = await badgeQrPng(w.curp);
|
const qrPng = await badgeQrPng(w.curp);
|
||||||
const qrImg = await pdf.embedPng(qrPng);
|
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);
|
drawBack(page, font, fontBold, x, botY, w, qrImg, project.name, project.code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,6 +182,22 @@ export async function generateBadgePdf(
|
||||||
return await pdf.save();
|
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(
|
function drawFront(
|
||||||
page: PDFPage,
|
page: PDFPage,
|
||||||
font: PDFFont,
|
font: PDFFont,
|
||||||
|
|
@ -174,6 +208,7 @@ function drawFront(
|
||||||
photo: PDFImage | null,
|
photo: PDFImage | null,
|
||||||
logoL: PDFImage | null,
|
logoL: PDFImage | null,
|
||||||
logoR: PDFImage | null,
|
logoR: PDFImage | null,
|
||||||
|
companyName: string,
|
||||||
) {
|
) {
|
||||||
page.drawRectangle({
|
page.drawRectangle({
|
||||||
x,
|
x,
|
||||||
|
|
@ -193,12 +228,15 @@ function drawFront(
|
||||||
height: logoL.height * scale,
|
height: logoL.height * scale,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
page.drawText("Construcciones Arctec", {
|
drawFittedLeft(
|
||||||
x: x + 6,
|
page,
|
||||||
y: y + CARD_H - 22,
|
fontBold,
|
||||||
size: 7,
|
companyName.toUpperCase(),
|
||||||
font: fontBold,
|
x + 6,
|
||||||
});
|
y + CARD_H - 22,
|
||||||
|
CARD_W / 2 - 10,
|
||||||
|
7,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (logoR) {
|
if (logoR) {
|
||||||
const scale = Math.min((CARD_W / 2 - 8) / logoR.width, (logoH - 4) / logoR.height);
|
const scale = Math.min((CARD_W / 2 - 8) / logoR.width, (logoH - 4) / logoR.height);
|
||||||
|
|
@ -208,14 +246,6 @@ function drawFront(
|
||||||
width: logoR.width * scale,
|
width: logoR.width * scale,
|
||||||
height: logoR.height * 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;
|
const photoW = 3.5 * CM;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
<include file="changesets/032-rpc-document-get-id.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/032-rpc-document-get-id.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/032-fix-work-program-jsonb-object-length.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/032-fix-work-program-jsonb-object-length.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/032-document-validity-from-upload.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/032-document-validity-from-upload.sql" relativeToChangelogFile="true"/>
|
||||||
|
<include file="changesets/032-fn-badge-job-get.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/033-catalog-suppliers-units.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/033-catalog-suppliers-units.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/034-fn-expense-update-reclassify.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/034-fn-expense-update-reclassify.sql" relativeToChangelogFile="true"/>
|
||||||
<include file="changesets/035-warehouse-materials-catalog.sql" relativeToChangelogFile="true"/>
|
<include file="changesets/035-warehouse-materials-catalog.sql" relativeToChangelogFile="true"/>
|
||||||
|
|
|
||||||
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 }">
|
<article class="badge" :class="{ 'badge-back-rot': side === 'back' && rotated, fluid }">
|
||||||
<template v-if="side === 'front'">
|
<template v-if="side === 'front'">
|
||||||
<div class="badge-logos">
|
<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 class="logo-r">{{ projectName }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="badge-photo">
|
<div class="badge-photo">
|
||||||
|
|
@ -47,6 +47,7 @@ const props = defineProps<{
|
||||||
blood: string;
|
blood: string;
|
||||||
riskColor: string;
|
riskColor: string;
|
||||||
riskText: string;
|
riskText: string;
|
||||||
|
companyName?: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
projectCode: string;
|
projectCode: string;
|
||||||
photoSrc?: string;
|
photoSrc?: string;
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,8 @@ const SAMPLE: Worker = {
|
||||||
const { api, download } = useApi();
|
const { api, download } = useApi();
|
||||||
const desk = useDesktop();
|
const desk = useDesktop();
|
||||||
const toast = useToast();
|
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 projectId = ref<number | null>(null);
|
||||||
const workers = ref<Worker[]>([]);
|
const workers = ref<Worker[]>([]);
|
||||||
const workerId = ref(0);
|
const workerId = ref(0);
|
||||||
|
|
@ -203,8 +204,9 @@ const cardProps = computed(() => {
|
||||||
blood: (w.blood_type || "").toUpperCase(),
|
blood: (w.blood_type || "").toUpperCase(),
|
||||||
riskColor: r?.color || w.risk_color || "#A20000",
|
riskColor: r?.color || w.risk_color || "#A20000",
|
||||||
riskText: r?.text_color || w.risk_text || "#FFFFFF",
|
riskText: r?.text_color || w.risk_text || "#FFFFFF",
|
||||||
projectName: (project.value?.name || "Zendala Cancún").toUpperCase(),
|
companyName: (project.value?.company_name || user.value?.company_name || "").toUpperCase(),
|
||||||
projectCode: (project.value?.code || "PRY-0001").toUpperCase(),
|
projectName: (project.value?.name || "Obra").toUpperCase(),
|
||||||
|
projectCode: (project.value?.code || "").toUpperCase(),
|
||||||
photoSrc: w.id ? `/v1/workers/${w.id}/photo` : "",
|
photoSrc: w.id ? `/v1/workers/${w.id}/photo` : "",
|
||||||
qrSrc: `/v1/badge-qr?curp=${encodeURIComponent(w.curp)}`,
|
qrSrc: `/v1/badge-qr?curp=${encodeURIComponent(w.curp)}`,
|
||||||
};
|
};
|
||||||
|
|
@ -236,7 +238,7 @@ async function loadWorkers() {
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const [p, j, c] = await Promise.all([
|
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<{ jobs: Record<string, unknown>[] }>("/v1/badge-jobs"),
|
||||||
api<{ risks: { code: string; label: string; color: string; text_color: string }[] }>("/v1/catalogs"),
|
api<{ risks: { code: string; label: string; color: string; text_color: string }[] }>("/v1/catalogs"),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue