/** * GiftFlow - Main Application Logic */ document.addEventListener('DOMContentLoaded', () => { // --- STATE --- let DB = {}; let ADMIN_DB = { users: [], families: [], links: [] }; // Initialisé avec des tableaux vides const state = { authHeader: null, currentUser: null, userFamilies: [], selectedFamilyId: null, currentView: 'login', // login, familySelector, app, admin appSubView: 'dashboard', viewingMemberId: null, selectedArchiveYear: null, selectedArchiveMemberId: null, adminTab: 'users' }; const translations = {}; let currentLang = 'fr'; // --- DOM --- const loginContainer = document.getElementById('login-container'); const familyContainer = document.getElementById('family-selector-container'); const appContainer = document.getElementById('app-container'); const mainContent = document.getElementById('main-content'); const familyList = document.getElementById('family-list'); const currentUserSpan = document.getElementById('current-user-name'); const navDashboard = document.getElementById('nav-dashboard'); const navArchives = document.getElementById('nav-archives'); const navLogo = document.getElementById('nav-logo'); const navSwitch = document.getElementById('nav-switch'); const navAdmin = document.getElementById('nav-admin'); const langSwitcher = document.getElementById('lang-switcher'); const addFab = document.getElementById('add-fab'); const modal = document.getElementById('add-modal'); const closeModalBtn = document.querySelector('.close-button'); const addForm = document.getElementById('add-form'); const modalTypeSelector = document.getElementById('modal-type-selector'); // --- UTILS --- const findById = (coll, id) => coll.find(i => i.id === id); const formatCurrency = (amt) => new Intl.NumberFormat(currentLang, { style: 'currency', currency: 'EUR' }).format(amt); const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); const t = (key, params = {}) => { let text = key.split('.').reduce((o, i) => o?.[i], translations[currentLang]); if (!text) return `[${key}]`; // Affiche la clé pour déboguer si manquant for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v); return text; }; const getUserRole = (user) => user?.role || (user?.isAdmin ? 'admin' : 'member'); const canManageAdmin = (user) => getUserRole(user) === 'admin'; const canViewArchives = (user) => ['admin', 'privileged'].includes(getUserRole(user)); // Fonction pour appliquer les traductions sur le DOM actuel function applyTranslations() { document.querySelectorAll('[data-translate]').forEach(el => { el.innerHTML = t(el.dataset.translate); }); // Traduction des éléments spécifiques non gérés par data-translate if(state.currentView === 'familySelector') { document.querySelector('#family-selector-container h2').textContent = t('familySelector.title'); } } // --- API & AUTH --- async function apiFetch(url, options = {}) { const headers = { ...options.headers, 'Authorization': state.authHeader, 'Content-Type': 'application/json' }; const res = await fetch(url, { ...options, headers }); if (res.status === 401) { window.handleLogout(); throw new Error("Auth error"); } if (!res.ok) throw new Error("API Error"); if (res.status === 204) return; return res.json(); } window.handleLogout = () => { state.authHeader = null; state.currentUser = null; state.userFamilies = []; state.selectedFamilyId = null; localStorage.removeItem('giftflowUser'); localStorage.removeItem('giftflowAuth'); document.getElementById('login-form').reset(); document.getElementById('login-error').textContent = ''; switchView('login'); }; async function handleLogin(e) { e.preventDefault(); const u = e.target.username.value; const p = e.target.password.value; state.authHeader = `Basic ${btoa(u + ':' + p)}`; try { const res = await apiFetch('/api/login', { method: 'POST' }); state.currentUser = res.user; state.userFamilies = res.families; localStorage.setItem('giftflowAuth', state.authHeader); if (state.userFamilies.length === 1) { selectFamily(state.userFamilies[0].id); } else if (state.userFamilies.length > 1) { switchView('familySelector'); } else if (canManageAdmin(state.currentUser)) { switchView('admin'); } else { alert("No family assigned."); window.handleLogout(); } } catch (e) { document.getElementById('login-error').textContent = t('login.error'); state.authHeader = null; } } // --- INIT --- async function init() { // Load Language const savedLang = localStorage.getItem('giftflow_lang') || 'fr'; langSwitcher.innerHTML = ``; langSwitcher.value = savedLang; langSwitcher.onchange = (e) => loadLang(e.target.value); await loadLang(savedLang); // Setup Events document.getElementById('login-form').onsubmit = handleLogin; document.getElementById('logout-btn').onclick = window.handleLogout; const goToDashboard = (e) => { e.preventDefault(); if(state.selectedFamilyId) { state.appSubView = 'dashboard'; switchView('app'); renderApp(); } else if (state.userFamilies.length > 1) { switchView('familySelector'); } }; navDashboard.onclick = goToDashboard; navLogo.onclick = goToDashboard; navArchives.onclick = (e) => { e.preventDefault(); if(!canViewArchives(state.currentUser)) return; state.appSubView = 'archives'; state.selectedArchiveYear = null; switchView('app'); renderApp(); }; navSwitch.onclick = (e) => { e.preventDefault(); switchView('familySelector'); }; navAdmin.onclick = (e) => { e.preventDefault(); if(canManageAdmin(state.currentUser)) switchView('admin'); }; addFab.onclick = () => openAddModal(); closeModalBtn.onclick = closeModal; window.onclick = (e) => { if(e.target === modal) closeModal(); }; // Check Session const savedAuth = localStorage.getItem('giftflowAuth'); if (savedAuth) { state.authHeader = savedAuth; try { const res = await apiFetch('/api/login', { method: 'POST' }); state.currentUser = res.user; state.userFamilies = res.families; if (state.userFamilies.length === 1) { selectFamily(state.userFamilies[0].id); } else if (state.userFamilies.length > 1) { switchView('familySelector'); } else if (canManageAdmin(state.currentUser)) { switchView('admin'); } } catch (e) { window.handleLogout(); } } else { switchView('login'); } } async function loadLang(lang) { if(!translations[lang]) { try { const res = await fetch(`/locales/${lang}.json`); translations[lang] = await res.json(); } catch(e) { console.error("Lang error"); return; } } currentLang = lang; localStorage.setItem('giftflow_lang', lang); document.documentElement.lang = lang; applyTranslations(); // Applique les traductions statiques // Refresh current view content if(state.currentUser) { currentUserSpan.textContent = `${t('header.loggedInAs')} ${state.currentUser.username}`; if(state.currentView === 'app') renderApp(); if(state.currentView === 'admin') renderAdmin(); // Re-render admin to apply lang if(state.currentView === 'familySelector') renderFamilySelector(); } } // --- VIEW MANAGER --- function switchView(view) { if (view === 'admin' && !canManageAdmin(state.currentUser)) view = 'app'; state.currentView = view; loginContainer.classList.remove('active'); familyContainer.style.display = 'none'; appContainer.classList.remove('active'); // Header Links Visibility if (state.currentUser) { navAdmin.classList.toggle('hidden', !canManageAdmin(state.currentUser)); navArchives.classList.toggle('hidden', !canViewArchives(state.currentUser)); navSwitch.classList.toggle('hidden', state.userFamilies.length <= 1); } if (view === 'login') loginContainer.classList.add('active'); if (view === 'familySelector') { familyContainer.style.display = 'block'; renderFamilySelector(); } if (view === 'app') { appContainer.classList.add('active'); renderApp(); } if (view === 'admin') { appContainer.classList.add('active'); loadAndRenderAdmin(); } applyTranslations(); // Traduit les éléments statiques nouvellement affichés } window.selectFamily = async (id) => { state.selectedFamilyId = id; try { DB = await apiFetch(`/api/family/${id}/data`); state.appSubView = 'dashboard'; switchView('app'); } catch(e) { alert("Error loading family"); } }; function renderFamilySelector() { document.querySelector('#family-selector-container h2').textContent = t('familySelector.title'); familyList.innerHTML = state.userFamilies.map(f => `
${t('familySelector.clickToEnter')}
${iC} ${iC===1?t('dashboard.ideas_one'):t('dashboard.ideas_other')} | ${pC} ${pC===1?t('dashboard.purchases_one'):t('dashboard.purchases_other')}
${t('memberProfile.noIdeas')}
`}${t('memberProfile.noPurchases')}
`}${gc} ${gc===1?t('archives.giftsReceived_one'):t('archives.giftsReceived_other')}
${t('archives.noGiftsForYear')}
`; } } else { const m = findById(DB.members, state.selectedArchiveMemberId); const mg = yearGifts.filter(g => g.targetMemberId === m.id); html += `${t('archives.selectYear')}
`; } mainContent.innerHTML = html; } } // Windows global for inline HTML events window.viewMember = (id) => { state.viewingMemberId = id; state.appSubView = 'memberProfile'; renderApp(); }; window.selectArchiveYear = (y) => { state.selectedArchiveYear = y; state.selectedArchiveMemberId = null; renderApp(); }; window.selectArchiveMember = (id) => { state.selectedArchiveMemberId = id; renderApp(); }; // --- CARDS --- function renderIdeaCard(i) { const creator = findById(DB.members, i.creatorId)?.username || '?'; const comment = i.comment?.trim(); const link = i.link?.trim(); return `${t('purchasedCard.purchasedAt', {store: escapeHtml(g.store)})}
${t('purchasedCard.paidBy', {name: payer})}
${comment ? `| ${t('reimbursement.tableHeaderMember')} | ${t('reimbursement.tableHeaderStatus')} | ${t('reimbursement.tableHeaderActions')} |
|---|---|---|
| ${m.username} ${t('reimbursement.amountDue',{amount:formatCurrency(share)})} | ${sT}${rT} | ${acts} |
${t('modal.for', {name: tm.username})}
`; } else { const opts = DB.members.map(m => ``).join(''); const chks = DB.members.map(m => ``).join(''); h = `${t('modal.for', {name: tm.username})}
`; } addForm.innerHTML = h; addForm.classList.add('visible'); } addForm.onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(addForm); const adminAction = addForm.dataset.adminAction; if (adminAction) { try { if(adminAction === 'addUser') { const role = fd.get('role') || 'member'; await apiFetch('/api/admin/users', { method: 'POST', body: JSON.stringify({username: fd.get('u'), password: fd.get('p'), role, isAdmin: role === 'admin', familyIds: Array.from(document.querySelectorAll('input[name=fams]:checked')).map(c=>c.value) }) }); } if(adminAction === 'editUser') { const role = fd.get('role') || 'member'; await apiFetch(`/api/admin/users/${addForm.dataset.adminId}`, { method: 'PUT', body: JSON.stringify({username: fd.get('u'), password: fd.get('p'), role, isAdmin: role === 'admin', familyIds: Array.from(document.querySelectorAll('input[name=fams]:checked')).map(c=>c.value) }) }); } if(adminAction === 'addFam') { await apiFetch('/api/admin/families', { method: 'POST', body: JSON.stringify({name: fd.get('n')}) }); } if(adminAction === 'editFam') { await apiFetch(`/api/admin/families/${addForm.dataset.adminId}`, { method: 'PUT', body: JSON.stringify({name: fd.get('n')}) }); } // NOUVEAU: Gestion des membres de famille depuis l'onglet famille if(adminAction === 'editFamilyMembers') { const memberIds = Array.from(document.querySelectorAll('input[name=memberIds]:checked')).map(c=>c.value); await apiFetch(`/api/admin/families/${addForm.dataset.adminId}/members`, { method: 'PUT', body: JSON.stringify({ memberIds }) }); } closeModal(); loadAndRenderAdmin(); } catch(e) { console.error(e); alert("Admin Error"); } return; } // App logic const type = addForm.dataset.type; const payload = { familyId: state.selectedFamilyId }; try { if (type === 'idea') { payload.title = fd.get('title'); payload.estimatedPrice = parseFloat(fd.get('estimatedPrice')) || null; payload.link = fd.get('link')?.trim() || null; payload.comment = fd.get('comment')?.trim() || null; if(addForm.dataset.editIdeaId) { await apiFetch(`/api/ideas/${addForm.dataset.editIdeaId}`, { method: 'PUT', body: JSON.stringify(payload) }); } else { payload.targetMemberId = state.viewingMemberId; payload.creatorId = state.currentUser.id; payload.creationDate = new Date().toISOString(); await apiFetch('/api/ideas', { method: 'POST', body: JSON.stringify(payload) }); } } else { payload.name = fd.get('name'); payload.totalPrice = parseFloat(fd.get('totalPrice')); payload.store = fd.get('store'); payload.link = fd.get('link')?.trim() || null; payload.comment = fd.get('comment')?.trim() || null; payload.purchaseDate = new Date(fd.get('purchaseDate')).toISOString(); payload.payerId = parseInt(fd.get('payerId')); payload.targetMemberId = state.viewingMemberId; payload.reimbursementMemberIds = Array.from(document.querySelectorAll('input[name=mids]:checked')).map(c => parseInt(c.value)); if(addForm.dataset.editGiftId) await apiFetch(`/api/gifts/${addForm.dataset.editGiftId}`, { method: 'PUT', body: JSON.stringify(payload) }); else { let url = '/api/gifts'; if(addForm.dataset.convertId) url+=`?deleteIdeaId=${addForm.dataset.convertId}`; await apiFetch(url, { method: 'POST', body: JSON.stringify(payload) }); } } closeModal(); refresh(); } catch(e) { alert("Save Error"); } }; // --- ADMIN DASHBOARD --- async function loadAndRenderAdmin() { try { ADMIN_DB = await apiFetch('/api/admin/data'); // Check if data loaded correctly if (!ADMIN_DB.users) ADMIN_DB.users = []; if (!ADMIN_DB.families) ADMIN_DB.families = []; if (!ADMIN_DB.links) ADMIN_DB.links = []; renderAdmin(); } catch(e) { mainContent.innerHTML = "Admin Load Error"; } } function renderAdmin() { navDashboard.classList.remove('active'); navArchives.classList.remove('active'); navAdmin.classList.add('active'); addFab.style.display = 'none'; mainContent.innerHTML = `| ${t('admin.username')} | ${t('admin.role')} | ${t('admin.families')} | ${t('admin.actions')} |
|---|---|---|---|
| ${u.username} | ${t(`admin.role_${getUserRole(u)}`)} | ${fams} |
| ${t('admin.name')} | ${t('admin.members')} | ${t('admin.actions')} |
|---|---|---|
| ${f.name} | ${c} |