const express = require('express'); const pool = require('../db'); const { requireCurator } = require('../middleware/auth'); const { logCuratorAction } = require('../audit-log'); const { TRANSLATABLE_FIELDS, getEntityCanonical, listTranslations, upsertTranslation, getTranslationCoverage, } = require('../translation-service'); const router = express.Router(); const VALID_ENTITY_TYPES = new Set(Object.keys(TRANSLATABLE_FIELDS)); router.get('/worklist/:entityType', requireCurator, async (req, res) => { try { const entityType = req.params.entityType; const locale = typeof req.query.locale === 'string' ? req.query.locale : 'ru'; if (!VALID_ENTITY_TYPES.has(entityType)) { return res.status(400).json({ error: 'Invalid entity type' }); } const tables = { artist: { table: 'artists', label: 'name', idCol: 'id' }, painting: { table: 'paintings', label: 'title', idCol: 'id' }, movement: { table: 'art_movements', label: 'name', idCol: 'id' }, }; const spec = tables[entityType]; if (!spec) { return res.json({ items: [] }); } const { rows } = await pool.query( `SELECT id AS entity_id, ${spec.label} AS label FROM ${spec.table} ORDER BY ${spec.label} LIMIT 1000`, ); const ids = rows.map((r) => r.entity_id); const { rows: transRows } = ids.length ? await pool.query( `SELECT entity_id, field_name, status FROM entity_translations WHERE entity_type = $1 AND entity_id = ANY($2::int[]) AND locale = $3`, [entityType, ids, locale], ) : { rows: [] }; const byEntity = new Map(); for (const tr of transRows) { if (!byEntity.has(tr.entity_id)) byEntity.set(tr.entity_id, []); byEntity.get(tr.entity_id).push(tr); } const fields = TRANSLATABLE_FIELDS[entityType] || []; const items = rows.map((row) => { const existing = byEntity.get(row.entity_id) || []; const publishedCount = existing.filter((t) => t.status === 'published').length; const draftCount = existing.filter((t) => t.status !== 'published').length; const haveFields = new Set(existing.map((t) => t.field_name)); const missingFields = fields.filter((f) => !haveFields.has(f)); return { entityId: row.entity_id, label: row.label, publishedCount, draftCount, missingFields, }; }); res.json({ items }); } catch (err) { console.error('Translation worklist error:', err.message); res.status(500).json({ error: 'Failed to load worklist' }); } }); router.get('/coverage', requireCurator, async (req, res) => { try { const locale = typeof req.query.locale === 'string' ? req.query.locale : 'ru'; const coverage = await getTranslationCoverage(locale); res.json({ locale, coverage }); } catch (err) { console.error('Translation coverage error:', err.message); res.status(500).json({ error: 'Failed to load coverage' }); } }); router.get('/', requireCurator, async (req, res) => { try { const entityType = typeof req.query.entityType === 'string' ? req.query.entityType : undefined; const locale = typeof req.query.locale === 'string' ? req.query.locale : 'ru'; const status = typeof req.query.status === 'string' ? req.query.status : undefined; const rows = await listTranslations({ entityType, locale, status }); res.json({ translations: rows }); } catch (err) { console.error('List translations error:', err.message); res.status(500).json({ error: 'Failed to list translations' }); } }); router.get('/:entityType/:id', requireCurator, async (req, res) => { try { const entityType = req.params.entityType; const entityId = parseInt(req.params.id, 10); if (!VALID_ENTITY_TYPES.has(entityType) || !Number.isFinite(entityId)) { return res.status(400).json({ error: 'Invalid entity type or id' }); } const canonical = await getEntityCanonical(entityType, entityId); if (!canonical) { return res.status(404).json({ error: 'Entity not found' }); } const { rows } = await pool.query( `SELECT locale, field_name, value, status, source, updated_at FROM entity_translations WHERE entity_type = $1 AND entity_id = $2 ORDER BY locale, field_name`, [entityType, entityId], ); res.json({ entityType, entityId, canonical, translatableFields: TRANSLATABLE_FIELDS[entityType], translations: rows, }); } catch (err) { console.error('Get translation error:', err.message); res.status(500).json({ error: 'Failed to load translation' }); } }); router.put('/:entityType/:id', requireCurator, async (req, res) => { try { const entityType = req.params.entityType; const entityId = parseInt(req.params.id, 10); const locale = typeof req.body?.locale === 'string' ? req.body.locale : 'ru'; const fields = req.body?.fields; const status = typeof req.body?.status === 'string' ? req.body.status : 'draft'; if (!VALID_ENTITY_TYPES.has(entityType) || !Number.isFinite(entityId)) { return res.status(400).json({ error: 'Invalid entity type or id' }); } if (!fields || typeof fields !== 'object') { return res.status(400).json({ error: 'fields object required' }); } const canonical = await getEntityCanonical(entityType, entityId); if (!canonical) { return res.status(404).json({ error: 'Entity not found' }); } const allowed = new Set(TRANSLATABLE_FIELDS[entityType]); const saved = []; for (const [fieldName, value] of Object.entries(fields)) { if (!allowed.has(fieldName) || typeof value !== 'string') continue; const row = await upsertTranslation({ entityType, entityId, locale, fieldName, value, status, source: 'manual', }); saved.push(row); } await logCuratorAction({ userId: req.curatorUser.id, action: 'translation.upsert', resourceType: entityType, resourceId: entityId, details: { locale, fieldCount: saved.length, status }, req, }); res.json({ saved }); } catch (err) { console.error('Upsert translation error:', err.message); res.status(500).json({ error: 'Failed to save translation' }); } }); router.post('/:entityType/:id/publish', requireCurator, async (req, res) => { try { const entityType = req.params.entityType; const entityId = parseInt(req.params.id, 10); const locale = typeof req.body?.locale === 'string' ? req.body.locale : 'ru'; if (!VALID_ENTITY_TYPES.has(entityType) || !Number.isFinite(entityId)) { return res.status(400).json({ error: 'Invalid entity type or id' }); } const { rowCount } = await pool.query( `UPDATE entity_translations SET status = 'published', updated_at = now() WHERE entity_type = $1 AND entity_id = $2 AND locale = $3 AND status IN ('draft', 'reviewed')`, [entityType, entityId, locale], ); await logCuratorAction({ userId: req.curatorUser.id, action: 'translation.publish', resourceType: entityType, resourceId: entityId, details: { locale, updated: rowCount }, req, }); res.json({ published: rowCount }); } catch (err) { console.error('Publish translation error:', err.message); res.status(500).json({ error: 'Failed to publish translations' }); } }); module.exports = router;