initial commit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
179
src/routes/admin/company-settings.ts
Normal file
179
src/routes/admin/company-settings.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import prisma from '../../config/database';
|
||||
import { requireAuth, requirePermission } from '../../middleware/auth';
|
||||
import { logAudit } from '../../services/audit';
|
||||
import { success, error } from '../../utils/response';
|
||||
import multipart from '@fastify/multipart';
|
||||
|
||||
/** Encode custom_fields + supplier_field_order into a single JSON blob (matching PHP format) */
|
||||
function encodeCustomFields(fields: unknown, fieldOrder: unknown): string | null {
|
||||
const f = Array.isArray(fields) ? fields : [];
|
||||
const o = Array.isArray(fieldOrder) ? fieldOrder : [];
|
||||
if (f.length === 0 && o.length === 0) return null;
|
||||
return JSON.stringify({ fields: f, field_order: o });
|
||||
}
|
||||
|
||||
/** Decode custom_fields JSON blob into separate fields + field_order for frontend */
|
||||
function decodeCustomFields(raw: string | null): { custom_fields: unknown[]; supplier_field_order: string[] } {
|
||||
if (!raw) return { custom_fields: [], supplier_field_order: [] };
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
// PHP format: { fields: [...], field_order: [...] }
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'fields' in parsed) {
|
||||
return { custom_fields: parsed.fields || [], supplier_field_order: parsed.field_order || [] };
|
||||
}
|
||||
// Legacy TS format: raw array
|
||||
if (Array.isArray(parsed)) {
|
||||
return { custom_fields: parsed, supplier_field_order: [] };
|
||||
}
|
||||
return { custom_fields: [], supplier_field_order: [] };
|
||||
} catch {
|
||||
return { custom_fields: [], supplier_field_order: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export default async function companySettingsRoutes(fastify: FastifyInstance): Promise<void> {
|
||||
await fastify.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } });
|
||||
|
||||
// GET /api/admin/company-settings/logo
|
||||
fastify.get('/logo', { preHandler: requireAuth }, async (_request, reply) => {
|
||||
const settings = await prisma.company_settings.findFirst({ select: { logo_data: true } });
|
||||
if (!settings?.logo_data) return error(reply, 'Logo nenalezeno', 404);
|
||||
|
||||
// Detect image type from magic bytes
|
||||
const buf = settings.logo_data;
|
||||
let mime = 'image/png';
|
||||
if (buf[0] === 0xFF && buf[1] === 0xD8) mime = 'image/jpeg';
|
||||
else if (buf[0] === 0x47 && buf[1] === 0x49) mime = 'image/gif';
|
||||
|
||||
return reply.type(mime).send(buf);
|
||||
});
|
||||
|
||||
// POST /api/admin/company-settings/logo
|
||||
fastify.post('/logo', { preHandler: requirePermission('offers.settings') }, async (request, reply) => {
|
||||
const file = await request.file();
|
||||
if (!file) return error(reply, 'Nebyl nahrán žádný soubor', 400);
|
||||
|
||||
const allowed = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
||||
if (!allowed.includes(file.mimetype)) {
|
||||
return error(reply, 'Nepodporovaný formát. Povoleno: PNG, JPG, GIF, WebP', 400);
|
||||
}
|
||||
|
||||
const buffer = await file.toBuffer();
|
||||
const existing = await prisma.company_settings.findFirst();
|
||||
if (!existing) return error(reply, 'Nastavení nenalezeno', 404);
|
||||
|
||||
await prisma.company_settings.update({
|
||||
where: { id: existing.id },
|
||||
data: { logo_data: new Uint8Array(buffer), modified_at: new Date() },
|
||||
});
|
||||
|
||||
await logAudit({ request, authData: request.authData, action: 'update', entityType: 'company_settings', entityId: existing.id, description: 'Nahráno logo' });
|
||||
return success(reply, null, 200, 'Logo nahráno');
|
||||
});
|
||||
|
||||
fastify.get('/', { preHandler: requireAuth }, async (_request, reply) => {
|
||||
let settings = await prisma.company_settings.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
company_name: true,
|
||||
street: true,
|
||||
city: true,
|
||||
postal_code: true,
|
||||
country: true,
|
||||
company_id: true,
|
||||
vat_id: true,
|
||||
custom_fields: true,
|
||||
quotation_prefix: true,
|
||||
default_currency: true,
|
||||
default_vat_rate: true,
|
||||
uuid: true,
|
||||
modified_at: true,
|
||||
is_deleted: true,
|
||||
sync_version: true,
|
||||
order_type_code: true,
|
||||
invoice_type_code: true,
|
||||
require_2fa: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
settings = await prisma.company_settings.create({
|
||||
data: {
|
||||
company_name: '',
|
||||
quotation_prefix: 'N',
|
||||
default_currency: 'EUR',
|
||||
default_vat_rate: 21.0,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
company_name: true,
|
||||
street: true,
|
||||
city: true,
|
||||
postal_code: true,
|
||||
country: true,
|
||||
company_id: true,
|
||||
vat_id: true,
|
||||
custom_fields: true,
|
||||
quotation_prefix: true,
|
||||
default_currency: true,
|
||||
default_vat_rate: true,
|
||||
uuid: true,
|
||||
modified_at: true,
|
||||
is_deleted: true,
|
||||
sync_version: true,
|
||||
order_type_code: true,
|
||||
invoice_type_code: true,
|
||||
require_2fa: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if logo exists
|
||||
const logoCheck = await prisma.company_settings.findFirst({
|
||||
where: { id: settings.id },
|
||||
select: { logo_data: true },
|
||||
});
|
||||
const has_logo = !!(logoCheck?.logo_data);
|
||||
|
||||
const { custom_fields, supplier_field_order } = decodeCustomFields(settings.custom_fields as string | null);
|
||||
|
||||
return success(reply, { ...settings, custom_fields, supplier_field_order, has_logo });
|
||||
});
|
||||
|
||||
fastify.put('/', { preHandler: requirePermission('offers.settings') }, async (request, reply) => {
|
||||
const body = request.body as Record<string, unknown>;
|
||||
|
||||
const existing = await prisma.company_settings.findFirst();
|
||||
if (!existing) return error(reply, 'Nastavení nenalezeno', 404);
|
||||
|
||||
const data: Record<string, unknown> = { modified_at: new Date() };
|
||||
const strFields = ['company_name', 'street', 'city', 'postal_code', 'country', 'company_id', 'vat_id', 'quotation_prefix', 'default_currency', 'order_type_code', 'invoice_type_code'];
|
||||
for (const f of strFields) {
|
||||
if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;
|
||||
}
|
||||
if (body.default_vat_rate !== undefined) data.default_vat_rate = Number(body.default_vat_rate);
|
||||
if (body.require_2fa !== undefined) data.require_2fa = body.require_2fa === true || body.require_2fa === 1 || body.require_2fa === '1';
|
||||
if (body.custom_fields !== undefined || body.supplier_field_order !== undefined) {
|
||||
let existingFields: unknown[] = [];
|
||||
let existingOrder: unknown[] = [];
|
||||
if (existing.custom_fields) {
|
||||
try {
|
||||
const parsed = JSON.parse(existing.custom_fields);
|
||||
existingFields = parsed?.fields || [];
|
||||
existingOrder = parsed?.field_order || [];
|
||||
} catch { /* invalid JSON, use defaults */ }
|
||||
}
|
||||
data.custom_fields = encodeCustomFields(
|
||||
body.custom_fields !== undefined ? body.custom_fields : existingFields,
|
||||
body.supplier_field_order !== undefined ? body.supplier_field_order : existingOrder,
|
||||
);
|
||||
}
|
||||
data.sync_version = (existing.sync_version ?? 0) + 1;
|
||||
|
||||
await prisma.company_settings.update({ where: { id: existing.id }, data });
|
||||
|
||||
await logAudit({ request, authData: request.authData, action: 'update', entityType: 'company_settings', entityId: existing.id, description: 'Upraveno firemní nastavení' });
|
||||
return success(reply, { id: existing.id }, 200, 'Nastavení bylo uloženo');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user