/* ============================================================ EATME PORTÁL – NEWS SERVICE Role: - EMPLOYEE: čte ALL + EMPLOYEE - MANAGER: čte ALL + MANAGER, může spravovat aktuality - ACCOUNTANT: čte ALL + ACCOUNTANT - ADMIN: čte všechny aktuality, může je spravovat Využívá existující: - NEWS - NEWS_READS ============================================================ */ /* ============================================================ SETUP ============================================================ */ function setupNews() { ensureNewsSheet_( CFG.SHEETS.NEWS, HEADERS.NEWS ); ensureNewsSheet_( CFG.SHEETS.NEWS_READS, HEADERS.NEWS_READS ); Logger.log( 'News setup OK' ); } /* ============================================================ AKTUALITY PRO PŘIHLÁŠENÉHO UŽIVATELE ============================================================ */ function getMyNews(token) { const user = requireUser_( token ); const role = String( user.role || '' ).toUpperCase(); const now = new Date(); const newsRows = rows_( CFG.SHEETS.NEWS ); const readRows = rows_( CFG.SHEETS.NEWS_READS ) .filter( function(row) { return ( String( row.user_id ) === String( user.user_id ) ); } ); const readsByNews = {}; readRows.forEach( function(row) { readsByNews[ String( row.news_id ) ] = row; } ); return newsRows .filter( function(row) { if ( !newsBool_( row.active ) ) { return false; } const target = String( row.target_role || 'ALL' ) .trim() .toUpperCase(); /* * ADMIN vidí všechno. * Ostatní role pouze ALL + svou roli. */ if ( role !== 'ADMIN' && target !== 'ALL' && target !== role ) { return false; } const from = newsDate_( row.published_from ); const to = newsDate_( row.published_to ); if ( from && from.getTime() > now.getTime() ) { return false; } if ( to && to.getTime() < now.getTime() ) { return false; } return true; } ) .map( function(row) { const read = readsByNews[ String( row.news_id ) ] || null; return { news_id: String( row.news_id || '' ), title: String( row.title || '' ), content: String( row.content || '' ), category: String( row.category || '' ), target_role: String( row.target_role || 'ALL' ), published_from: row.published_from || '', published_to: row.published_to || '', require_confirmation: newsBool_( row.require_confirmation ), read_at: read ? read.read_at || '' : '', confirmed_at: read ? read.confirmed_at || '' : '', created_at: row.created_at || '' }; } ) .sort( function(a,b) { const aImportant = String( a.category ).toUpperCase() === 'DŮLEŽITÉ' ? 1 : 0; const bImportant = String( b.category ).toUpperCase() === 'DŮLEŽITÉ' ? 1 : 0; if ( aImportant !== bImportant ) { return ( bImportant - aImportant ); } return ( newsTime_( b.created_at ) - newsTime_( a.created_at ) ); } ); } /* ============================================================ PŘEČTENO / POTVRZENO ============================================================ */ function markNewsRead( token, newsId, confirm ) { const user = requireUser_( token ); const news = findOne_( CFG.SHEETS.NEWS, function(row) { return ( String( row.news_id ) === String( newsId ) ); } ); if ( !news ) { throw new Error( 'Aktualita nebyla nalezena.' ); } const existing = findOne_( CFG.SHEETS.NEWS_READS, function(row) { return ( String( row.news_id ) === String( newsId ) && String( row.user_id ) === String( user.user_id ) ); } ); const stamp = now_(); if ( existing ) { const update = {}; if ( !existing.read_at ) { update.read_at = stamp; } if ( confirm ) { update.confirmed_at = stamp; } if ( Object.keys( update ).length ) { updateBy_( CFG.SHEETS.NEWS_READS, 'news_read_id', existing.news_read_id, update ); } } else { append_( CFG.SHEETS.NEWS_READS, { news_read_id: uuid_( 'NR' ), news_id: newsId, user_id: user.user_id, read_at: stamp, confirmed_at: confirm ? stamp : '' } ); } return { ok:true }; } /* ============================================================ HROMADNÉ OZNAČENÍ AKTUALIT JAKO PŘEČTENÉ Jeden request pro všechny zobrazené aktuality. ============================================================ */ function markNewsListRead( token, newsIds ) { const user = requireUser_( token ); newsIds = Array.isArray( newsIds ) ? newsIds : []; const uniqueIds = Array.from( new Set( newsIds .map( function(id) { return String( id || '' ).trim(); } ) .filter( Boolean ) ) ); if ( !uniqueIds.length ) { return { ok:true }; } const existingRows = rows_( CFG.SHEETS.NEWS_READS ) .filter( function(row) { return ( String( row.user_id ) === String( user.user_id ) ); } ); const existingByNews = {}; existingRows.forEach( function(row) { existingByNews[ String( row.news_id ) ] = row; } ); const stamp = now_(); uniqueIds.forEach( function(newsId) { const existing = existingByNews[ newsId ]; if ( existing ) { if ( !existing.read_at ) { updateBy_( CFG.SHEETS.NEWS_READS, 'news_read_id', existing.news_read_id, { read_at: stamp } ); } } else { append_( CFG.SHEETS.NEWS_READS, { news_read_id: uuid_( 'NR' ), news_id: newsId, user_id: user.user_id, read_at: stamp, confirmed_at: '' } ); } } ); return { ok:true }; } /* ============================================================ ADMIN / MANAGER – SEZNAM AKTUALIT ============================================================ */ function getAdminNews(token) { requireUser_( token, [ 'ADMIN', 'MANAGER' ] ); const newsRows = rows_( CFG.SHEETS.NEWS ); const readRows = rows_( CFG.SHEETS.NEWS_READS ); const users = rows_( CFG.SHEETS.USERS ) .filter( function(user) { return newsBool_( user.active ); } ); return newsRows .map( function(row) { const newsId = String( row.news_id || '' ); const target = String( row.target_role || 'ALL' ).toUpperCase(); const audience = users.filter( function(user) { return ( target === 'ALL' || target === String( user.role || '' ).toUpperCase() ); } ); const reads = readRows.filter( function(read) { return ( String( read.news_id ) === newsId ); } ); return { news_id: newsId, title: String( row.title || '' ), content: String( row.content || '' ), category: String( row.category || '' ), target_role: String( row.target_role || 'ALL' ), published_from: row.published_from || '', published_to: row.published_to || '', require_confirmation: newsBool_( row.require_confirmation ), active: newsBool_( row.active ), created_at: row.created_at || '', audience_count: audience.length, read_count: reads.filter( function(read) { return !!read.read_at; } ).length, confirmed_count: reads.filter( function(read) { return !!read.confirmed_at; } ).length }; } ) .sort( function(a,b) { return ( newsTime_( b.created_at ) - newsTime_( a.created_at ) ); } ); } /* ============================================================ ADMIN / MANAGER – ULOŽIT AKTUALITU ============================================================ */ function saveAdminNews( token, payload ) { const user = requireUser_( token, [ 'ADMIN', 'MANAGER' ] ); payload = payload || {}; const title = String( payload.title || '' ).trim(); const content = String( payload.content || '' ).trim(); if ( !title ) { throw new Error( 'Vyplň nadpis aktuality.' ); } if ( !content ) { throw new Error( 'Vyplň text aktuality.' ); } const allowedTargets = [ 'ALL', 'EMPLOYEE', 'MANAGER', 'ACCOUNTANT', 'ADMIN' ]; const targetRole = String( payload.target_role || 'ALL' ).toUpperCase(); if ( !allowedTargets.includes( targetRole ) ) { throw new Error( 'Neplatná cílová role.' ); } const newsId = String( payload.news_id || '' ).trim(); const data = { title: title, content: content, category: String( payload.category || 'Provoz' ), company_id: String( payload.company_id || '' ), location_id: String( payload.location_id || '' ), target_role: targetRole, published_from: payload.published_from || now_(), published_to: payload.published_to || '', require_confirmation: !!payload.require_confirmation, active: payload.active !== false }; if ( newsId ) { const old = findOne_( CFG.SHEETS.NEWS, function(row) { return ( String( row.news_id ) === newsId ); } ); if ( !old ) { throw new Error( 'Aktualita nebyla nalezena.' ); } updateBy_( CFG.SHEETS.NEWS, 'news_id', newsId, data ); audit_( user.user_id, 'NEWS_UPDATED', 'NEWS', newsId, old, data ); return { ok:true, news_id:newsId }; } const newId = uuid_( 'NEWS' ); append_( CFG.SHEETS.NEWS, { news_id: newId, title: data.title, content: data.content, category: data.category, company_id: data.company_id, location_id: data.location_id, target_role: data.target_role, published_from: data.published_from, published_to: data.published_to, require_confirmation: data.require_confirmation, active: data.active, created_by: user.user_id, created_at: now_() } ); audit_( user.user_id, 'NEWS_CREATED', 'NEWS', newId, '', data ); return { ok:true, news_id:newId }; } /* ============================================================ SKRÝT / ZOBRAZIT ============================================================ */ function setAdminNewsActive( token, newsId, active ) { const user = requireUser_( token, [ 'ADMIN', 'MANAGER' ] ); const news = findOne_( CFG.SHEETS.NEWS, function(row) { return ( String( row.news_id ) === String( newsId ) ); } ); if ( !news ) { throw new Error( 'Aktualita nebyla nalezena.' ); } updateBy_( CFG.SHEETS.NEWS, 'news_id', newsId, { active: !!active } ); audit_( user.user_id, active ? 'NEWS_SHOWN' : 'NEWS_HIDDEN', 'NEWS', newsId, news.active, !!active ); return { ok:true }; } /* ============================================================ ADMIN – KDO PŘEČETL / POTVRDIL ============================================================ */ function getAdminNewsReaders( token, newsId ) { requireUser_( token, [ 'ADMIN', 'MANAGER' ] ); const news = findOne_( CFG.SHEETS.NEWS, function(row) { return ( String( row.news_id ) === String( newsId ) ); } ); if ( !news ) { throw new Error( 'Aktualita nebyla nalezena.' ); } const target = String( news.target_role || 'ALL' ).toUpperCase(); const users = rows_( CFG.SHEETS.USERS ) .filter( function(user) { if ( !newsBool_( user.active ) ) { return false; } return ( target === 'ALL' || target === String( user.role || '' ).toUpperCase() ); } ); const employees = rows_( CFG.SHEETS.EMPLOYEES ); const reads = rows_( CFG.SHEETS.NEWS_READS ) .filter( function(row) { return ( String( row.news_id ) === String( newsId ) ); } ); return users .map( function(user) { const read = reads.find( function(row) { return ( String( row.user_id ) === String( user.user_id ) ); } ) || null; const employee = employees.find( function(row) { return ( String( row.employee_id ) === String( user.employee_id || '' ) ); } ) || null; return { user_id: String( user.user_id || '' ), name: employee ? ( String( employee.first_name || '' ) + ' ' + String( employee.last_name || '' ) ).trim() : String( user.email || '' ), email: String( user.email || '' ), role: String( user.role || '' ), read_at: read ? read.read_at || '' : '', confirmed_at: read ? read.confirmed_at || '' : '' }; } ); } /* ============================================================ HELPERS ============================================================ */ function newsBool_(value) { return ( value === true || String( value ).toUpperCase() === 'TRUE' || String( value ) === '1' ); } function newsDate_(value) { if ( !value ) { return null; } const date = value instanceof Date ? value : new Date( value ); return isNaN( date.getTime() ) ? null : date; } function newsTime_(value) { const date = newsDate_( value ); return date ? date.getTime() : 0; } function ensureNewsSheet_( sheetName, headers ) { try { const existing = sh_( sheetName ); if ( existing ) { return existing; } } catch(error) { /* * List ještě neexistuje. */ } let spreadsheet = null; try { spreadsheet = sh_( CFG.SHEETS.USERS ).getParent(); } catch(error) { /* * Fallback níže. */ } if ( !spreadsheet ) { spreadsheet = SpreadsheetApp .getActiveSpreadsheet(); } if ( !spreadsheet ) { throw new Error( 'Nepodařilo se otevřít databázový spreadsheet.' ); } let sheet = spreadsheet.getSheetByName( sheetName ); if ( !sheet ) { sheet = spreadsheet.insertSheet( sheetName ); sheet .getRange( 1, 1, 1, headers.length ) .setValues( [ headers ] ); sheet.setFrozenRows( 1 ); } return sheet; }