diff --git a/controller/admin/CoffeeController.js b/controller/admin/CoffeeController.js index 0660eea..670e7fc 100644 --- a/controller/admin/CoffeeController.js +++ b/controller/admin/CoffeeController.js @@ -12,16 +12,61 @@ function buildPictureUrlFromKey(key) { return key; // fallback: store key only } +function normalizePictureFiles(req) { + const files = []; + + // upload.any() -> req.files is usually an array + if (Array.isArray(req.files)) { + files.push(...req.files); + } + + // Defensive fallback for potential object-shaped middleware output + if (req.files && !Array.isArray(req.files) && typeof req.files === 'object') { + for (const value of Object.values(req.files)) { + if (Array.isArray(value)) files.push(...value); + else if (value) files.push(value); + } + } + + if (req.file) files.push(req.file); + return files.filter(Boolean); +} + +function toCoffeeResponse(row) { + const pictures = Array.isArray(row?.pictures) && row.pictures.length + ? row.pictures.map((p) => ({ + ...p, + url: p.object_storage_id ? buildPictureUrlFromKey(p.object_storage_id) : '' + })) + : (row?.object_storage_id + ? [{ + id: null, + coffee_id: row.id, + object_storage_id: row.object_storage_id, + original_filename: row.original_filename || null, + sort_order: 0, + created_at: row.created_at || null, + url: buildPictureUrlFromKey(row.object_storage_id) + }] + : []); + + const pictureUrls = pictures.map((p) => p.url).filter(Boolean); + return { + ...row, + pictureUrl: pictureUrls[0] || '', + pictureUrls, + pictures, + }; +} + exports.list = async (req, res) => { const rows = await CoffeeService.list(); - const items = (rows || []).map(r => ({ - ...r, - pictureUrl: r.object_storage_id ? buildPictureUrlFromKey(r.object_storage_id) : '' - })); + const items = (rows || []).map((r) => toCoffeeResponse(r)); res.json(items); }; exports.create = async (req, res) => { + let uploadedKeys = []; try { const { title, description, price } = req.body; const currency = req.body.currency || 'EUR'; @@ -31,19 +76,23 @@ exports.create = async (req, res) => { const interval_count = 1; const state = req.body.state === 'false' || req.body.state === false ? false : true; // default available - // If file uploaded, push to Exoscale and set object_storage_id + // If files uploaded, push to Exoscale and store all uploaded images let object_storage_id = null; let original_filename = null; - let uploadedKey = null; - if (req.file) { + let images = []; + const incomingFiles = normalizePictureFiles(req); + logger.info('[CoffeeController.create] incoming multipart files', { + count: incomingFiles.length, + fields: incomingFiles.map((f) => f?.fieldname).filter(Boolean), + }); + if (incomingFiles.length > 10) { + return res.status(400).json({ error: 'Maximum 10 images allowed' }); + } + + if (incomingFiles.length) { const allowedMime = ['image/jpeg','image/png','image/webp']; - if (!allowedMime.includes(req.file.mimetype)) { - return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' }); - } const maxBytes = 10 * 1024 * 1024; // 10MB - if (req.file.size > maxBytes) { - return res.status(400).json({ error: 'Image exceeds 10MB limit' }); - } + const s3 = new S3Client({ region: process.env.EXOSCALE_REGION, endpoint: process.env.EXOSCALE_ENDPOINT, @@ -52,18 +101,38 @@ exports.create = async (req, res) => { secretAccessKey: process.env.EXOSCALE_SECRET_KEY, }, }); - const key = `coffee/products/${Date.now()}_${req.file.originalname}`; - await s3.send(new PutObjectCommand({ - Bucket: process.env.EXOSCALE_BUCKET, - Key: key, - Body: req.file.buffer, - ContentType: req.file.mimetype, - ACL: 'public-read' - })); - object_storage_id = key; - original_filename = req.file.originalname; - uploadedKey = key; - logger.info('[CoffeeController.create] uploaded picture', { key }); + + for (let i = 0; i < incomingFiles.length; i += 1) { + const file = incomingFiles[i]; + if (!allowedMime.includes(file.mimetype)) { + return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' }); + } + if (file.size > maxBytes) { + return res.status(400).json({ error: 'Image exceeds 10MB limit' }); + } + + const key = `coffee/products/${Date.now()}_${i}_${file.originalname}`; + await s3.send(new PutObjectCommand({ + Bucket: process.env.EXOSCALE_BUCKET, + Key: key, + Body: file.buffer, + ContentType: file.mimetype, + ACL: 'public-read' + })); + + uploadedKeys.push(key); + images.push({ + object_storage_id: key, + original_filename: file.originalname, + sort_order: i, + }); + } + + if (images[0]) { + object_storage_id = images[0].object_storage_id; + original_filename = images[0].original_filename; + } + logger.info('[CoffeeController.create] uploaded pictures', { count: images.length }); } const created = await CoffeeService.create({ @@ -76,17 +145,15 @@ exports.create = async (req, res) => { interval_count, object_storage_id, original_filename, + images, state, }); - res.status(201).json({ - ...created, - pictureUrl: created.object_storage_id ? buildPictureUrlFromKey(created.object_storage_id) : '' - }); + res.status(201).json(toCoffeeResponse(created)); } catch (e) { logger.error('[CoffeeController.create] error', { msg: e.message, stack: e.stack?.split('\n')[0] }); // best-effort cleanup of uploaded object on failure try { - if (object_storage_id) { + if (uploadedKeys.length) { const s3 = new S3Client({ region: process.env.EXOSCALE_REGION, endpoint: process.env.EXOSCALE_ENDPOINT, @@ -95,7 +162,9 @@ exports.create = async (req, res) => { secretAccessKey: process.env.EXOSCALE_SECRET_KEY, }, }); - await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: object_storage_id })); + for (const key of uploadedKeys) { + await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key })); + } } } catch (cleanupErr) { logger.warn('[CoffeeController.create] cleanup failed', { msg: cleanupErr.message }); @@ -251,13 +320,181 @@ exports.remove = async (req, res) => { exports.listActive = async (req, res) => { try { const rows = await CoffeeService.listActive(); - const items = (rows || []).map(r => ({ - ...r, - pictureUrl: r.object_storage_id ? buildPictureUrlFromKey(r.object_storage_id) : '' - })); + const items = (rows || []).map((r) => toCoffeeResponse(r)); res.json(items); } catch (e) { logger.error('[CoffeeController.listActive] error', { msg: e.message }); res.status(500).json({ error: 'Failed to fetch active coffee products' }); } }; + +exports.getPictures = async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid id' }); + } + + const row = await CoffeeService.get(id); + if (!row) { + return res.status(404).json({ error: 'Not found' }); + } + + const normalized = toCoffeeResponse(row); + return res.json({ + coffeeId: id, + pictures: normalized.pictures, + pictureUrls: normalized.pictureUrls, + pictureUrl: normalized.pictureUrl, + }); + } catch (e) { + logger.error('[CoffeeController.getPictures] error', { msg: e.message }); + return res.status(500).json({ error: 'Failed to fetch coffee pictures' }); + } +}; + +exports.editPictures = async (req, res) => { + let uploadedKeys = []; + try { + const id = parseInt(req.params.id, 10); + if (!Number.isFinite(id) || id <= 0) { + return res.status(400).json({ error: 'Invalid id' }); + } + + const parseBoolean = (value, fallback = false) => { + if (value === undefined || value === null || value === '') return fallback; + if (typeof value === 'boolean') return value; + const normalized = String(value).trim().toLowerCase(); + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; + if (['0', 'false', 'no', 'off'].includes(normalized)) return false; + return fallback; + }; + + const parseIds = (value) => { + if (value === undefined || value === null || value === '') return []; + if (Array.isArray(value)) return value.map((x) => Number(x)).filter((x) => Number.isFinite(x) && x > 0); + + const text = String(value).trim(); + if (!text) return []; + + try { + const parsed = JSON.parse(text); + if (Array.isArray(parsed)) { + return parsed.map((x) => Number(x)).filter((x) => Number.isFinite(x) && x > 0); + } + } catch (_) { + // Fall through to CSV parsing. + } + + return text + .split(',') + .map((x) => Number(String(x).trim())) + .filter((x) => Number.isFinite(x) && x > 0); + }; + + const replaceAll = parseBoolean(req.body?.replaceAll, false); + const removePictureIds = parseIds(req.body?.removePictureIds); + + const incomingFiles = normalizePictureFiles(req); + if (incomingFiles.length > 10) { + return res.status(400).json({ error: 'Maximum 10 images allowed per edit request' }); + } + + const images = []; + if (incomingFiles.length) { + const allowedMime = ['image/jpeg','image/png','image/webp']; + const maxBytes = 10 * 1024 * 1024; // 10MB + + const s3 = new S3Client({ + region: process.env.EXOSCALE_REGION, + endpoint: process.env.EXOSCALE_ENDPOINT, + credentials: { + accessKeyId: process.env.EXOSCALE_ACCESS_KEY, + secretAccessKey: process.env.EXOSCALE_SECRET_KEY, + }, + }); + + for (let i = 0; i < incomingFiles.length; i += 1) { + const file = incomingFiles[i]; + if (!allowedMime.includes(file.mimetype)) { + return res.status(400).json({ error: 'Invalid image type. Allowed: JPG, PNG, WebP' }); + } + if (file.size > maxBytes) { + return res.status(400).json({ error: 'Image exceeds 10MB limit' }); + } + + const key = `coffee/products/${Date.now()}_${i}_${file.originalname}`; + await s3.send(new PutObjectCommand({ + Bucket: process.env.EXOSCALE_BUCKET, + Key: key, + Body: file.buffer, + ContentType: file.mimetype, + ACL: 'public-read' + })); + + uploadedKeys.push(key); + images.push({ + object_storage_id: key, + original_filename: file.originalname, + }); + } + } + + const result = await CoffeeService.editPictures(id, { + replaceAll, + removePictureIds, + images, + }); + + if (!result || !result.updated) { + return res.status(404).json({ error: 'Not found' }); + } + + const deletedKeys = (result.deleted || []) + .map((x) => x?.object_storage_id) + .filter(Boolean); + if (deletedKeys.length) { + try { + const s3 = new S3Client({ + region: process.env.EXOSCALE_REGION, + endpoint: process.env.EXOSCALE_ENDPOINT, + credentials: { + accessKeyId: process.env.EXOSCALE_ACCESS_KEY, + secretAccessKey: process.env.EXOSCALE_SECRET_KEY, + }, + }); + + for (const key of deletedKeys) { + await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key })); + } + } catch (cleanupErr) { + logger.warn('[CoffeeController.editPictures] cleanup failed', { msg: cleanupErr.message }); + } + } + + return res.json(toCoffeeResponse(result.updated)); + } catch (e) { + logger.error('[CoffeeController.editPictures] error', { msg: e.message }); + + try { + if (uploadedKeys.length) { + const s3 = new S3Client({ + region: process.env.EXOSCALE_REGION, + endpoint: process.env.EXOSCALE_ENDPOINT, + credentials: { + accessKeyId: process.env.EXOSCALE_ACCESS_KEY, + secretAccessKey: process.env.EXOSCALE_SECRET_KEY, + }, + }); + + for (const key of uploadedKeys) { + await s3.send(new DeleteObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: key })); + } + } + } catch (cleanupErr) { + logger.warn('[CoffeeController.editPictures] rollback cleanup failed', { msg: cleanupErr.message }); + } + + return res.status(500).json({ error: 'Failed to edit coffee pictures' }); + } +}; diff --git a/controller/admin/MailTemplatesController.js b/controller/admin/MailTemplatesController.js index 3e52d84..b40e5dc 100644 --- a/controller/admin/MailTemplatesController.js +++ b/controller/admin/MailTemplatesController.js @@ -71,6 +71,18 @@ class MailTemplatesController { } } + static async unarchive(req, res) { + try { + const data = await MailTemplateService.unarchive(req.params.id, MailTemplatesController._currentUserId(req)); + if (!data) { + return res.status(404).json({ success: false, message: 'Mail template not found' }); + } + return res.json({ success: true, data }); + } catch (error) { + return res.status(error?.status || 500).json({ success: false, message: error.message || 'Failed to unarchive mail template' }); + } + } + static async remove(req, res) { try { const deleted = await MailTemplateService.remove(req.params.id); diff --git a/database/createDb.js b/database/createDb.js index 8f4fe07..18b8976 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -1135,6 +1135,21 @@ const createDatabase = async () => { `); console.log('✅ Coffee table (simplified) created/verified'); + await connection.query(` + CREATE TABLE IF NOT EXISTS coffee_table_images ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + coffee_id BIGINT NOT NULL, + object_storage_id VARCHAR(255) NOT NULL, + original_filename VARCHAR(255) NULL, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_coffee_table_images_coffee FOREIGN KEY (coffee_id) REFERENCES coffee_table(id) ON DELETE CASCADE ON UPDATE CASCADE, + INDEX idx_coffee_table_images_coffee_sort (coffee_id, sort_order), + INDEX idx_coffee_table_images_key (object_storage_id) + ); + `); + console.log('✅ Coffee table images created/verified'); + // --- Coffee shipping fees (fixed package sizes) --- await connection.query(` CREATE TABLE IF NOT EXISTS coffee_shipping_fees ( @@ -1834,6 +1849,8 @@ const createDatabase = async () => { await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_active', 'is_active'); await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_archived', 'is_archived'); await ensureIndex(connection, 'mail_templates', 'idx_mail_templates_type_active', 'template_type, is_active'); + await ensureIndex(connection, 'coffee_table_images', 'idx_coffee_table_images_coffee_sort', 'coffee_id, sort_order'); + await ensureIndex(connection, 'coffee_table_images', 'idx_coffee_table_images_key', 'object_storage_id'); await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id'); await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active'); console.log('🚀 Performance indexes created/verified'); diff --git a/repositories/subscriptions/CoffeeRepository.js b/repositories/subscriptions/CoffeeRepository.js index 4356f4c..c991941 100644 --- a/repositories/subscriptions/CoffeeRepository.js +++ b/repositories/subscriptions/CoffeeRepository.js @@ -2,16 +2,204 @@ const db = require('../../database/database'); const { logger } = require('../../middleware/logger'); class CoffeeRepository { + async _syncSortOrders(coffeeId, conn) { + const cx = conn || db; + const [rows] = await cx.query( + `SELECT id + FROM coffee_table_images + WHERE coffee_id = ? + ORDER BY sort_order ASC, id ASC`, + [coffeeId] + ); + + for (let i = 0; i < (rows || []).length; i += 1) { + await cx.query('UPDATE coffee_table_images SET sort_order = ? WHERE id = ?', [i, rows[i].id]); + } + } + + async ensureLegacyPictureRow(coffeeId, conn) { + const cx = conn || db; + + const [coffeeRows] = await cx.query( + `SELECT object_storage_id, original_filename + FROM coffee_table + WHERE id = ? + LIMIT 1`, + [coffeeId] + ); + const coffee = coffeeRows?.[0]; + if (!coffee || !coffee.object_storage_id) return; + + const [countRows] = await cx.query( + `SELECT COUNT(*) AS c + FROM coffee_table_images + WHERE coffee_id = ?`, + [coffeeId] + ); + const count = Number(countRows?.[0]?.c || 0); + if (count > 0) return; + + await cx.query( + `INSERT INTO coffee_table_images (coffee_id, object_storage_id, original_filename, sort_order) + VALUES (?, ?, ?, 0)`, + [coffeeId, coffee.object_storage_id, coffee.original_filename || null] + ); + } + + async listPicturesByCoffeeId(coffeeId, conn) { + const cx = conn || db; + const [rows] = await cx.query( + `SELECT id, coffee_id, object_storage_id, original_filename, sort_order, created_at + FROM coffee_table_images + WHERE coffee_id = ? + ORDER BY sort_order ASC, id ASC`, + [coffeeId] + ); + return rows || []; + } + + async deleteAllPicturesByCoffeeId(coffeeId, conn) { + const cx = conn || db; + const toDelete = await this.listPicturesByCoffeeId(coffeeId, cx); + if (!toDelete.length) return []; + + await cx.query('DELETE FROM coffee_table_images WHERE coffee_id = ?', [coffeeId]); + return toDelete; + } + + async deletePicturesByIds(coffeeId, pictureIds, conn) { + const cx = conn || db; + const ids = Array.isArray(pictureIds) + ? pictureIds.map((x) => Number(x)).filter((x) => Number.isFinite(x) && x > 0) + : []; + if (!ids.length) return []; + + const placeholders = ids.map(() => '?').join(','); + const [rows] = await cx.query( + `SELECT id, coffee_id, object_storage_id, original_filename, sort_order, created_at + FROM coffee_table_images + WHERE coffee_id = ? + AND id IN (${placeholders})`, + [coffeeId, ...ids] + ); + if (!(rows || []).length) return []; + + const deletePlaceholders = rows.map(() => '?').join(','); + await cx.query( + `DELETE FROM coffee_table_images + WHERE coffee_id = ? + AND id IN (${deletePlaceholders})`, + [coffeeId, ...rows.map((r) => r.id)] + ); + return rows; + } + + async addPictures(coffeeId, images, conn) { + const cx = conn || db; + const existing = await this.listPicturesByCoffeeId(coffeeId, cx); + let nextSort = existing.length; + + for (const img of (images || [])) { + await cx.query( + `INSERT INTO coffee_table_images (coffee_id, object_storage_id, original_filename, sort_order) + VALUES (?, ?, ?, ?)`, + [ + coffeeId, + img.object_storage_id, + img.original_filename || null, + nextSort, + ] + ); + nextSort += 1; + } + } + + async syncPrimaryPictureFromGallery(coffeeId, conn) { + const cx = conn || db; + const [rows] = await cx.query( + `SELECT object_storage_id, original_filename + FROM coffee_table_images + WHERE coffee_id = ? + ORDER BY sort_order ASC, id ASC + LIMIT 1`, + [coffeeId] + ); + + const first = rows?.[0]; + await cx.query( + `UPDATE coffee_table + SET object_storage_id = ?, + original_filename = ?, + updated_at = NOW() + WHERE id = ?`, + [first?.object_storage_id || null, first?.original_filename || null, coffeeId] + ); + } + + async _attachPictures(rows, conn) { + const cx = conn || db; + if (!Array.isArray(rows) || !rows.length) return rows || []; + + const ids = rows.map((r) => Number(r.id)).filter((id) => Number.isFinite(id)); + if (!ids.length) return rows; + + const placeholders = ids.map(() => '?').join(','); + const [pictureRows] = await cx.query( + `SELECT id, coffee_id, object_storage_id, original_filename, sort_order, created_at + FROM coffee_table_images + WHERE coffee_id IN (${placeholders}) + ORDER BY coffee_id ASC, sort_order ASC, id ASC`, + ids + ); + + const grouped = new Map(); + for (const p of (pictureRows || [])) { + const key = Number(p.coffee_id); + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key).push({ + id: Number(p.id), + coffee_id: Number(p.coffee_id), + object_storage_id: p.object_storage_id, + original_filename: p.original_filename, + sort_order: Number(p.sort_order || 0), + created_at: p.created_at, + }); + } + + for (const row of rows) { + const coffeeId = Number(row.id); + const fromTable = grouped.get(coffeeId) || []; + if (fromTable.length) { + row.pictures = fromTable; + } else if (row.object_storage_id) { + row.pictures = [{ + id: null, + coffee_id: coffeeId, + object_storage_id: row.object_storage_id, + original_filename: row.original_filename || null, + sort_order: 0, + created_at: row.created_at || null, + }]; + } else { + row.pictures = []; + } + } + + return rows; + } + async listAll(conn) { const cx = conn || db; const [rows] = await cx.query('SELECT * FROM coffee_table ORDER BY id DESC'); - return rows || []; + return this._attachPictures(rows || [], cx); } async getById(id, conn) { const cx = conn || db; const [rows] = await cx.query('SELECT * FROM coffee_table WHERE id = ? LIMIT 1', [id]); - return rows && rows[0] ? rows[0] : null; + if (!rows || !rows[0]) return null; + const hydrated = await this._attachPictures([rows[0]], cx); + return hydrated[0] || null; } async create(data, conn) { @@ -35,8 +223,32 @@ class CoffeeRepository { data.state ]; const [result] = await cx.query(sql, params); + const createdId = result.insertId; + + const pictures = Array.isArray(data.images) ? [...data.images] : []; + if (!pictures.length && data.object_storage_id) { + pictures.push({ + object_storage_id: data.object_storage_id, + original_filename: data.original_filename || null, + sort_order: 0, + }); + } + + for (const picture of pictures) { + await cx.query( + `INSERT INTO coffee_table_images (coffee_id, object_storage_id, original_filename, sort_order) + VALUES (?, ?, ?, ?)`, + [ + createdId, + picture.object_storage_id, + picture.original_filename || null, + Number.isFinite(Number(picture.sort_order)) ? Number(picture.sort_order) : 0, + ] + ); + } + logger.info('[CoffeeRepository.create] insert', { id: result.insertId }); - return { id: result.insertId, ...data }; + return this.getById(createdId, cx); } async update(id, data, conn) { @@ -80,7 +292,33 @@ class CoffeeRepository { async listActive(conn) { const cx = conn || db; const [rows] = await cx.query('SELECT * FROM coffee_table WHERE state = TRUE ORDER BY id DESC'); - return rows || []; + return this._attachPictures(rows || [], cx); + } + + async editPictures(coffeeId, { replaceAll = false, removePictureIds = [], images = [] } = {}, conn) { + const cx = conn || db; + + await this.ensureLegacyPictureRow(coffeeId, cx); + + let deleted = []; + if (replaceAll) { + deleted = await this.deleteAllPicturesByCoffeeId(coffeeId, cx); + } else if (Array.isArray(removePictureIds) && removePictureIds.length) { + deleted = await this.deletePicturesByIds(coffeeId, removePictureIds, cx); + } + + if (Array.isArray(images) && images.length) { + await this.addPictures(coffeeId, images, cx); + } + + await this._syncSortOrders(coffeeId, cx); + await this.syncPrimaryPictureFromGallery(coffeeId, cx); + + const updated = await this.getById(coffeeId, cx); + return { + updated, + deleted, + }; } } diff --git a/repositories/template/MailTemplateRepository.js b/repositories/template/MailTemplateRepository.js index b1ddcb9..dc684a1 100644 --- a/repositories/template/MailTemplateRepository.js +++ b/repositories/template/MailTemplateRepository.js @@ -183,6 +183,21 @@ class MailTemplateRepository { return this.getById(id); } + async unarchive(id, userId) { + const [result] = await db.query( + `UPDATE mail_templates + SET is_archived = 0, + archived_at = NULL, + updated_by = ?, + updated_at = NOW() + WHERE id = ?`, + [userId || null, id] + ); + + if (!result?.affectedRows) return null; + return this.getById(id); + } + async delete(id) { const [result] = await db.query('DELETE FROM mail_templates WHERE id = ?', [id]); return Number(result?.affectedRows || 0) > 0; diff --git a/routes/getRoutes.js b/routes/getRoutes.js index f76d12f..82f9108 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -143,7 +143,9 @@ router.get('/company-stamps/all', authMiddleware, adminOnly, forceCompanyForAdmi // Admin: coffee products router.get('/admin/coffee', authMiddleware, adminOnly, CoffeeController.list); router.get('/admin/coffee/active', authMiddleware, adminOnly, CoffeeController.listActive); +router.get('/admin/coffee/:id/pictures', authMiddleware, adminOnly, CoffeeController.getPictures); router.get('/coffee/active', authMiddleware, CoffeeController.listActive); +router.get('/coffee/:id/pictures', authMiddleware, CoffeeController.getPictures); // Matrix GETs diff --git a/routes/patchRoutes.js b/routes/patchRoutes.js index 8469579..a867e6e 100644 --- a/routes/patchRoutes.js +++ b/routes/patchRoutes.js @@ -40,11 +40,14 @@ router.patch('/admin/unarchive-user/:id', authMiddleware, adminOnly, AdminUserCo router.patch('/admin/mail-templates/:id', authMiddleware, adminOnly, MailTemplatesController.update); router.patch('/admin/mail-templates/:id/activate', authMiddleware, adminOnly, MailTemplatesController.activate); router.patch('/admin/mail-templates/:id/archive', authMiddleware, adminOnly, MailTemplatesController.archive); +router.patch('/admin/mail-templates/:id/unarchive', authMiddleware, adminOnly, MailTemplatesController.unarchive); router.patch('/admin/update-verification/:id', authMiddleware, adminOnly, AdminUserController.updateUserVerification); router.patch('/admin/update-user-profile/:id', authMiddleware, adminOnly, AdminUserController.updateUserProfile); router.patch('/admin/update-user-status/:id', authMiddleware, adminOnly, AdminUserController.updateUserStatus); // Admin: set state for coffee product router.patch('/admin/coffee/:id/state', authMiddleware, adminOnly, CoffeeController.setState); +// Admin: edit coffee gallery pictures (upload/remove/replace) +router.patch('/admin/coffee/:id/pictures', authMiddleware, adminOnly, upload.any(), CoffeeController.editPictures); // NEW: Admin pool active status update router.patch('/admin/pools/:id/active', authMiddleware, adminOnly, PoolController.updateActive); // NEW: Admin update pool linked subscription diff --git a/routes/postRoutes.js b/routes/postRoutes.js index 3aaeb02..0e592dd 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -152,8 +152,8 @@ function ensureUserFromBody(req, res, next) { // Company-stamp POST router.post('/company-stamps', authMiddleware, adminOnly, forceCompanyForAdmin, CompanyStampController.upload); -// Admin: create coffee product (supports multipart file 'picture') -router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create); +// Admin: create coffee product (supports multipart files 'pictures' and legacy 'picture') +router.post('/admin/coffee', authMiddleware, adminOnly, upload.any(), CoffeeController.create); // NEW: add user into matrix router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); // already added // NEW: remove matrix user and create vacancy @@ -213,4 +213,4 @@ router.post('/register/guest', (req, res) => { console.log('✅ POST routes configured successfully'); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/services/subscriptions/CoffeeService.js b/services/subscriptions/CoffeeService.js index c492e06..c893d43 100644 --- a/services/subscriptions/CoffeeService.js +++ b/services/subscriptions/CoffeeService.js @@ -99,6 +99,30 @@ class CoffeeService { async listActive() { return CoffeeRepository.listActive(); } + + async editPictures(id, payload = {}) { + const numericId = Number(id); + if (!Number.isFinite(numericId) || numericId <= 0) { + throw new Error('Invalid coffee id'); + } + + const uow = new UnitOfWork(); + try { + await uow.start(); + const existing = await CoffeeRepository.getById(numericId, uow.connection); + if (!existing) { + await uow.rollback(); + return null; + } + + const result = await CoffeeRepository.editPictures(numericId, payload, uow.connection); + await uow.commit(); + return result; + } catch (e) { + try { await uow.rollback(e); } catch(_) {} + throw e; + } + } } module.exports = new CoffeeService(); diff --git a/services/template/MailTemplateService.js b/services/template/MailTemplateService.js index 46f5b3a..a2ac694 100644 --- a/services/template/MailTemplateService.js +++ b/services/template/MailTemplateService.js @@ -107,6 +107,17 @@ class MailTemplateService { return MailTemplateRepository.archive(numericId, userId); } + async unarchive(id, userId = null) { + const numericId = Number(id); + if (!Number.isFinite(numericId) || numericId <= 0) { + const error = new Error('Invalid id'); + error.status = 400; + throw error; + } + + return MailTemplateRepository.unarchive(numericId, userId); + } + async remove(id) { const numericId = Number(id); if (!Number.isFinite(numericId) || numericId <= 0) {