style: run prettier on entire codebase
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { requirePermission } from '../../middleware/auth';
|
||||
import { logAudit } from '../../services/audit';
|
||||
import { success, error, parseId } from '../../utils/response';
|
||||
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
|
||||
import { parseBody } from '../../schemas/common';
|
||||
import { CreateQuotationSchema, UpdateQuotationSchema } from '../../schemas/offers.schema';
|
||||
import prisma from '../../config/database';
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { requirePermission } from "../../middleware/auth";
|
||||
import { logAudit } from "../../services/audit";
|
||||
import { success, error, parseId } from "../../utils/response";
|
||||
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
|
||||
import { parseBody } from "../../schemas/common";
|
||||
import {
|
||||
CreateQuotationSchema,
|
||||
UpdateQuotationSchema,
|
||||
} from "../../schemas/offers.schema";
|
||||
import prisma from "../../config/database";
|
||||
import {
|
||||
listOffers,
|
||||
getOffer,
|
||||
@@ -15,177 +18,301 @@ import {
|
||||
duplicateOffer,
|
||||
invalidateOffer,
|
||||
getNextOfferNumber,
|
||||
} from '../../services/offers.service';
|
||||
} from "../../services/offers.service";
|
||||
|
||||
const LOCK_TIMEOUT_MS = 10 * 1000; // 10 seconds — lock expires if no heartbeat
|
||||
|
||||
export default async function quotationsRoutes(
|
||||
fastify: FastifyInstance,
|
||||
): Promise<void> {
|
||||
fastify.get(
|
||||
"/",
|
||||
{ preHandler: requirePermission("offers.view") },
|
||||
async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
const { page, limit, skip, sort, order, search } = parsePagination(query);
|
||||
|
||||
export default async function quotationsRoutes(fastify: FastifyInstance): Promise<void> {
|
||||
fastify.get('/', { preHandler: requirePermission('offers.view') }, async (request, reply) => {
|
||||
const query = request.query as Record<string, unknown>;
|
||||
const { page, limit, skip, sort, order, search } = parsePagination(query);
|
||||
const result = await listOffers({
|
||||
page,
|
||||
limit,
|
||||
skip,
|
||||
sort,
|
||||
order,
|
||||
search,
|
||||
status: query.status ? String(query.status) : undefined,
|
||||
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
|
||||
});
|
||||
|
||||
const result = await listOffers({
|
||||
page, limit, skip, sort, order, search,
|
||||
status: query.status ? String(query.status) : undefined,
|
||||
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
|
||||
});
|
||||
|
||||
return reply.send({ success: true, data: result.data, pagination: buildPaginationMeta(result.total, page, limit) });
|
||||
});
|
||||
return reply.send({
|
||||
success: true,
|
||||
data: result.data,
|
||||
pagination: buildPaginationMeta(result.total, page, limit),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// GET /api/admin/offers/next-number
|
||||
fastify.get('/next-number', { preHandler: requirePermission('offers.create') }, async (_request, reply) => {
|
||||
const number = await getNextOfferNumber();
|
||||
return success(reply, { number, next_number: number });
|
||||
});
|
||||
fastify.get(
|
||||
"/next-number",
|
||||
{ preHandler: requirePermission("offers.create") },
|
||||
async (_request, reply) => {
|
||||
const number = await getNextOfferNumber();
|
||||
return success(reply, { number, next_number: number });
|
||||
},
|
||||
);
|
||||
|
||||
// POST /api/admin/offers/:id/duplicate
|
||||
fastify.post<{ Params: { id: string } }>('/:id/duplicate', { preHandler: requirePermission('offers.create') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
fastify.post<{ Params: { id: string } }>(
|
||||
"/:id/duplicate",
|
||||
{ preHandler: requirePermission("offers.create") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
const result = await duplicateOffer(id);
|
||||
if (!result) return error(reply, 'Nabídka nenalezena', 404);
|
||||
const result = await duplicateOffer(id);
|
||||
if (!result) return error(reply, "Nabídka nenalezena", 404);
|
||||
|
||||
await logAudit({ request, authData: request.authData, action: 'create', entityType: 'quotation', entityId: result.copy.id, description: `Duplikována nabídka ${result.original.quotation_number} → ${result.copy.quotation_number}` });
|
||||
return success(reply, { id: result.copy.id, quotation_number: result.copy.quotation_number }, 201, 'Nabídka byla duplikována');
|
||||
});
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "create",
|
||||
entityType: "quotation",
|
||||
entityId: result.copy.id,
|
||||
description: `Duplikována nabídka ${result.original.quotation_number} → ${result.copy.quotation_number}`,
|
||||
});
|
||||
return success(
|
||||
reply,
|
||||
{ id: result.copy.id, quotation_number: result.copy.quotation_number },
|
||||
201,
|
||||
"Nabídka byla duplikována",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// POST /api/admin/offers/:id/invalidate
|
||||
fastify.post<{ Params: { id: string } }>('/:id/invalidate', { preHandler: requirePermission('offers.edit') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
fastify.post<{ Params: { id: string } }>(
|
||||
"/:id/invalidate",
|
||||
{ preHandler: requirePermission("offers.edit") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
const existing = await invalidateOffer(id);
|
||||
if (!existing) return error(reply, 'Nabídka nenalezena', 404);
|
||||
const existing = await invalidateOffer(id);
|
||||
if (!existing) return error(reply, "Nabídka nenalezena", 404);
|
||||
|
||||
await logAudit({ request, authData: request.authData, action: 'update', entityType: 'quotation', entityId: id, description: `Zneplatněna nabídka ${existing.quotation_number}` });
|
||||
return success(reply, null, 200, 'Nabídka zneplatněna');
|
||||
});
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "update",
|
||||
entityType: "quotation",
|
||||
entityId: id,
|
||||
description: `Zneplatněna nabídka ${existing.quotation_number}`,
|
||||
});
|
||||
return success(reply, null, 200, "Nabídka zneplatněna");
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('offers.view') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
fastify.get<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("offers.view") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
const data = await getOffer(id);
|
||||
if (!data) return error(reply, 'Nabídka nenalezena', 404);
|
||||
const data = await getOffer(id);
|
||||
if (!data) return error(reply, "Nabídka nenalezena", 404);
|
||||
|
||||
// Include lock info
|
||||
const quotation = await prisma.quotations.findUnique({
|
||||
where: { id },
|
||||
select: { locked_by: true, locked_at: true },
|
||||
});
|
||||
let lockedBy: { user_id: number; username: string; full_name: string } | null = null;
|
||||
if (quotation?.locked_by && quotation?.locked_at) {
|
||||
const lockAge = Date.now() - new Date(quotation.locked_at).getTime();
|
||||
if (lockAge < LOCK_TIMEOUT_MS && quotation.locked_by !== request.authData!.userId) {
|
||||
const lockUser = await prisma.users.findUnique({
|
||||
where: { id: quotation.locked_by },
|
||||
select: { id: true, username: true, first_name: true, last_name: true },
|
||||
});
|
||||
if (lockUser) {
|
||||
lockedBy = { user_id: lockUser.id, username: lockUser.username, full_name: `${lockUser.first_name} ${lockUser.last_name}`.trim() };
|
||||
// Include lock info
|
||||
const quotation = await prisma.quotations.findUnique({
|
||||
where: { id },
|
||||
select: { locked_by: true, locked_at: true },
|
||||
});
|
||||
let lockedBy: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
full_name: string;
|
||||
} | null = null;
|
||||
if (quotation?.locked_by && quotation?.locked_at) {
|
||||
const lockAge = Date.now() - new Date(quotation.locked_at).getTime();
|
||||
if (
|
||||
lockAge < LOCK_TIMEOUT_MS &&
|
||||
quotation.locked_by !== request.authData!.userId
|
||||
) {
|
||||
const lockUser = await prisma.users.findUnique({
|
||||
where: { id: quotation.locked_by },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
first_name: true,
|
||||
last_name: true,
|
||||
},
|
||||
});
|
||||
if (lockUser) {
|
||||
lockedBy = {
|
||||
user_id: lockUser.id,
|
||||
username: lockUser.username,
|
||||
full_name: `${lockUser.first_name} ${lockUser.last_name}`.trim(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success(reply, { ...data, locked_by: lockedBy });
|
||||
});
|
||||
return success(reply, { ...data, locked_by: lockedBy });
|
||||
},
|
||||
);
|
||||
|
||||
// POST /api/admin/offers/:id/lock — acquire lock
|
||||
fastify.post<{ Params: { id: string } }>('/:id/lock', { preHandler: requirePermission('offers.edit') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
fastify.post<{ Params: { id: string } }>(
|
||||
"/:id/lock",
|
||||
{ preHandler: requirePermission("offers.edit") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
const quotation = await prisma.quotations.findUnique({
|
||||
where: { id },
|
||||
select: { locked_by: true, locked_at: true },
|
||||
});
|
||||
if (!quotation) return error(reply, 'Nabídka nenalezena', 404);
|
||||
const quotation = await prisma.quotations.findUnique({
|
||||
where: { id },
|
||||
select: { locked_by: true, locked_at: true },
|
||||
});
|
||||
if (!quotation) return error(reply, "Nabídka nenalezena", 404);
|
||||
|
||||
// Check if locked by someone else and lock is fresh
|
||||
if (quotation.locked_by && quotation.locked_at) {
|
||||
const lockAge = Date.now() - new Date(quotation.locked_at).getTime();
|
||||
if (lockAge < LOCK_TIMEOUT_MS && quotation.locked_by !== request.authData!.userId) {
|
||||
const lockUser = await prisma.users.findUnique({
|
||||
where: { id: quotation.locked_by },
|
||||
select: { first_name: true, last_name: true },
|
||||
});
|
||||
return error(reply, `Nabídku právě upravuje ${lockUser ? `${lockUser.first_name} ${lockUser.last_name}`.trim() : 'jiný uživatel'}`, 423);
|
||||
// Check if locked by someone else and lock is fresh
|
||||
if (quotation.locked_by && quotation.locked_at) {
|
||||
const lockAge = Date.now() - new Date(quotation.locked_at).getTime();
|
||||
if (
|
||||
lockAge < LOCK_TIMEOUT_MS &&
|
||||
quotation.locked_by !== request.authData!.userId
|
||||
) {
|
||||
const lockUser = await prisma.users.findUnique({
|
||||
where: { id: quotation.locked_by },
|
||||
select: { first_name: true, last_name: true },
|
||||
});
|
||||
return error(
|
||||
reply,
|
||||
`Nabídku právě upravuje ${lockUser ? `${lockUser.first_name} ${lockUser.last_name}`.trim() : "jiný uživatel"}`,
|
||||
423,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.quotations.update({
|
||||
where: { id },
|
||||
data: { locked_by: request.authData!.userId, locked_at: new Date() },
|
||||
});
|
||||
await prisma.quotations.update({
|
||||
where: { id },
|
||||
data: { locked_by: request.authData!.userId, locked_at: new Date() },
|
||||
});
|
||||
|
||||
return success(reply, null, 200, 'Zámek nastaven');
|
||||
});
|
||||
return success(reply, null, 200, "Zámek nastaven");
|
||||
},
|
||||
);
|
||||
|
||||
// POST /api/admin/offers/:id/heartbeat — keep lock alive
|
||||
fastify.post<{ Params: { id: string } }>('/:id/heartbeat', { preHandler: requirePermission('offers.edit') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
fastify.post<{ Params: { id: string } }>(
|
||||
"/:id/heartbeat",
|
||||
{ preHandler: requirePermission("offers.edit") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
await prisma.quotations.updateMany({
|
||||
where: { id, locked_by: request.authData!.userId },
|
||||
data: { locked_at: new Date() },
|
||||
});
|
||||
await prisma.quotations.updateMany({
|
||||
where: { id, locked_by: request.authData!.userId },
|
||||
data: { locked_at: new Date() },
|
||||
});
|
||||
|
||||
return success(reply, null);
|
||||
});
|
||||
return success(reply, null);
|
||||
},
|
||||
);
|
||||
|
||||
// POST /api/admin/offers/:id/unlock — release lock
|
||||
fastify.post<{ Params: { id: string } }>('/:id/unlock', { preHandler: requirePermission('offers.edit') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
fastify.post<{ Params: { id: string } }>(
|
||||
"/:id/unlock",
|
||||
{ preHandler: requirePermission("offers.edit") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
await prisma.quotations.updateMany({
|
||||
where: { id, locked_by: request.authData!.userId },
|
||||
data: { locked_by: null, locked_at: null },
|
||||
});
|
||||
await prisma.quotations.updateMany({
|
||||
where: { id, locked_by: request.authData!.userId },
|
||||
data: { locked_by: null, locked_at: null },
|
||||
});
|
||||
|
||||
return success(reply, null);
|
||||
});
|
||||
return success(reply, null);
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post('/', { preHandler: requirePermission('offers.create') }, async (request, reply) => {
|
||||
const parsed = parseBody(CreateQuotationSchema, request.body);
|
||||
if ('error' in parsed) return error(reply, parsed.error, 400);
|
||||
fastify.post(
|
||||
"/",
|
||||
{ preHandler: requirePermission("offers.create") },
|
||||
async (request, reply) => {
|
||||
const parsed = parseBody(CreateQuotationSchema, request.body);
|
||||
if ("error" in parsed) return error(reply, parsed.error, 400);
|
||||
|
||||
const quotation = await createOffer(parsed.data);
|
||||
const quotation = await createOffer(parsed.data);
|
||||
|
||||
await logAudit({ request, authData: request.authData, action: 'create', entityType: 'quotation', entityId: quotation.id, description: `Vytvořena nabídka ${quotation.quotation_number}` });
|
||||
return success(reply, { id: quotation.id }, 201, 'Nabídka byla vytvořena');
|
||||
});
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "create",
|
||||
entityType: "quotation",
|
||||
entityId: quotation.id,
|
||||
description: `Vytvořena nabídka ${quotation.quotation_number}`,
|
||||
});
|
||||
return success(
|
||||
reply,
|
||||
{ id: quotation.id },
|
||||
201,
|
||||
"Nabídka byla vytvořena",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('offers.edit') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
const parsed = parseBody(UpdateQuotationSchema, request.body);
|
||||
if ('error' in parsed) return error(reply, parsed.error, 400);
|
||||
fastify.put<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("offers.edit") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
const parsed = parseBody(UpdateQuotationSchema, request.body);
|
||||
if ("error" in parsed) return error(reply, parsed.error, 400);
|
||||
|
||||
const result = await updateOffer(id, parsed.data);
|
||||
if ('error' in result) {
|
||||
if (result.error === 'not_found') return error(reply, 'Nabídka nenalezena', 404);
|
||||
if (result.error === 'invalidated') return error(reply, 'Nelze upravit zneplatněnou nabídku', 400);
|
||||
}
|
||||
const result = await updateOffer(id, parsed.data);
|
||||
if ("error" in result) {
|
||||
if (result.error === "not_found")
|
||||
return error(reply, "Nabídka nenalezena", 404);
|
||||
if (result.error === "invalidated")
|
||||
return error(reply, "Nelze upravit zneplatněnou nabídku", 400);
|
||||
}
|
||||
|
||||
// Keep lock — user stays on the page after save
|
||||
// Keep lock — user stays on the page after save
|
||||
|
||||
await logAudit({ request, authData: request.authData, action: 'update', entityType: 'quotation', entityId: id, description: `Upravena nabídka ${(result as any).quotation_number}` });
|
||||
return success(reply, { id }, 200, 'Nabídka byla uložena');
|
||||
});
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "update",
|
||||
entityType: "quotation",
|
||||
entityId: id,
|
||||
description: `Upravena nabídka ${(result as any).quotation_number}`,
|
||||
});
|
||||
return success(reply, { id }, 200, "Nabídka byla uložena");
|
||||
},
|
||||
);
|
||||
|
||||
fastify.delete<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('offers.delete') }, async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
fastify.delete<{ Params: { id: string } }>(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("offers.delete") },
|
||||
async (request, reply) => {
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
|
||||
const existing = await deleteOffer(id);
|
||||
if (!existing) return error(reply, 'Nabídka nenalezena', 404);
|
||||
const existing = await deleteOffer(id);
|
||||
if (!existing) return error(reply, "Nabídka nenalezena", 404);
|
||||
|
||||
await logAudit({ request, authData: request.authData, action: 'delete', entityType: 'quotation', entityId: id, description: `Smazána nabídka ${existing.quotation_number}` });
|
||||
return success(reply, null, 200, 'Nabídka smazána');
|
||||
});
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "delete",
|
||||
entityType: "quotation",
|
||||
entityId: id,
|
||||
description: `Smazána nabídka ${existing.quotation_number}`,
|
||||
});
|
||||
return success(reply, null, 200, "Nabídka smazána");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user