// Főalkalmazás — bejelentkezés, szerep-váltás, navigáció const { useState, useEffect, useReducer } = React; const Login = ({ onLogin }) => { const [pickedUser, setPickedUser] = useState(null); const [pwd, setPwd] = useState(''); const [pwdErr, setPwdErr] = useState(''); const [busy, setBusy] = useState(false); const [query, setQuery] = useState(''); const pwdRef = React.useRef(null); const searchRef = React.useRef(null); React.useEffect(() => { if (pickedUser && pwdRef.current) pwdRef.current.focus(); else if (!pickedUser && searchRef.current) searchRef.current.focus(); }, [pickedUser]); const users = React.useMemo(() => { const all = (window.USERS || []).slice().sort((a, b) => a.name.localeCompare(b.name, 'hu', { sensitivity: 'base' }) ); const q = query.trim().toLowerCase(); if (!q) return all; return all.filter(u => u.name.toLowerCase().includes(q) || (u.email || '').toLowerCase().includes(q) ); }, [query]); const tryLogin = async () => { if (pwd.length < 1) { setPwdErr('Add meg a jelszót'); return; } setBusy(true); setPwdErr(''); try { await window.api.login(pickedUser.id, pwd); await window.refreshData(); onLogin(window.CURRENT_USER || pickedUser); } catch (err) { if (err && err.status === 401) { setPwdErr('Hibás jelszó'); } else { setPwdErr(window.apiErrorMessage(err)); } } finally { setBusy(false); } }; return (
H1
H1
Újáépítés
{!pickedUser ? ( <>
setQuery(e.target.value)} placeholder="Keresés név vagy e-mail alapján…" autoComplete="off" spellCheck={false} /> {query && ( )}
{users.length === 0 ? (
Nincs találat
) : users.map(u => ( ))}
) : ( <>
{pickedUser.name}
{ setPwd(e.target.value); setPwdErr(''); }} onKeyDown={e => e.key === 'Enter' && tryLogin()} placeholder="••••••••" autoComplete="current-password" style={pwdErr ? { borderColor: 'var(--danger)' } : {}}/> {pwdErr &&
{pwdErr}
}
)}
v1.0
); }; // === Fő app === const App = () => { const [user, setUser] = useState(window.CURRENT_USER || null); const [view, setView] = useState('home'); const [logger, setLogger] = useState(false); const [approvalLog, setApprovalLog] = useState(null); const [payoutContractor, setPayoutContractor] = useState(null); const [showInvoice, setShowInvoice] = useState(false); const [showChat, setShowChat] = useState(false); const [toast, setToast] = useState(null); const [, force] = useReducer(x => x + 1, 0); // Subscribe to global data refreshes — every successful mutation fires // window.dispatchEvent(new CustomEvent('h1:data')), which re-renders the // whole tree from the freshly bootstrapped window.X arrays. useEffect(() => { const onData = () => { // Keep the local user reference in sync with the latest USERS list. if (user) { const fresh = window.USERS.find(u => u.id === user.id); if (fresh) setUser(fresh); } force(); }; window.addEventListener('h1:data', onData); return () => window.removeEventListener('h1:data', onData); }, [user]); // On mount, ask the server who is logged in (BOOT may have been emitted // for an empty session if the SSR include failed). The response also // refills window.X globals, so we just take currentUser. useEffect(() => { if (user) return; // already populated from BOOT (async () => { try { await window.api.bootstrap(); if (window.CURRENT_USER) setUser(window.CURRENT_USER); force(); } catch (e) { // Stay on Login. } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const showToast = (msg) => { setToast(msg); setTimeout(() => setToast(null), 2500); }; if (!user) { return { setUser(u); setView('home'); }}/>; } const handleSubmitLog = async (data) => { try { const fd = new FormData(); fd.append('phase_id', String(data.phaseId)); fd.append('parent_task_id', String(data.parentTaskId)); fd.append('log_date', data.date); fd.append('hourly_rate', String(data.hourlyRate || 0)); fd.append('material_cost', String(data.materialCost || 0)); fd.append('description', data.description || ''); fd.append('entry_type', data.entryType === 'labor' ? 'work' : data.entryType); fd.append('tiers', JSON.stringify(data.tiers || [])); (data.photoFiles || []).forEach(f => fd.append('photos[]', f)); await window.api.workLogs.create(fd); await window.refreshData(); setLogger(false); showToast('✓ Beküldve jóváhagyásra'); } catch (err) { showToast('Hiba: ' + window.apiErrorMessage(err)); } }; const handleApprove = async (decision, amount, note) => { if (!approvalLog) return; try { await window.api.workLogs.decision({ work_log_id: approvalLog.id, decision, note: note || '', approved_amount: decision === 'approved' ? (amount || 0) : 0, }); await window.refreshData(); setApprovalLog(null); showToast(decision === 'approved' ? '✓ Jóváhagyva' : '✓ Elutasítva'); } catch (err) { showToast('Hiba: ' + window.apiErrorMessage(err)); } }; const handlePayout = async (data) => { try { const fd = new FormData(); fd.append('contractor_id', String(data.contractorId)); fd.append('amount', String(data.originalAmount)); fd.append('currency', data.currency); fd.append('fx_rate', String(data.fxRate)); fd.append('paid_at', new Date().toISOString().slice(0, 10)); fd.append('note', data.note || ''); if (data.invoicePhoto) fd.append('invoice_photo', data.invoicePhoto); await window.api.payments.create(fd); await window.refreshData(); setPayoutContractor(null); showToast('✓ Kifizetés rögzítve'); } catch (err) { showToast('Hiba: ' + window.apiErrorMessage(err)); } }; // Nav config szerep szerint const navByRole = { contractor: [ { v: 'home', l: 'Áttekintés', icon: 'meter' }, { v: 'history', l: 'Munkáim', icon: 'list' }, { v: 'dash', l: 'Költségvetés', icon: 'coins' }, { v: 'timeline', l: 'Ütemterv', icon: 'gantt' }, { v: 'docs', l: 'Doksik', icon: 'docs' }, ], approver: [ { v: 'home', l: 'Áttekintés', icon: 'meter' }, { v: 'inbox', l: 'Jóváhagyás', icon: 'thumbs-up' }, { v: 'payouts', l: 'Kifizetés', icon: 'wallet' }, { v: 'dash', l: 'Költségvetés', icon: 'coins' }, { v: 'timeline', l: 'Ütemterv', icon: 'gantt' }, { v: 'docs', l: 'Doksik', icon: 'docs' }, ], admin: [ { v: 'home', l: 'Áttekintés', icon: 'meter' }, { v: 'inbox', l: 'Jóváhagyás', icon: 'thumbs-up' }, { v: 'dash', l: 'Költségvetés', icon: 'coins' }, { v: 'timeline', l: 'Ütemterv', icon: 'gantt' }, { v: 'docs', l: 'Doksik', icon: 'docs' }, { v: 'users', l: 'Beállítások', icon: 'settings' }, ], }; const nav = navByRole[user.role]; // Render content const renderView = () => { if (user.role === 'contractor') { switch (view) { case 'home': return setLogger(true)}/>; case 'history': return ; case 'dash': return ; case 'timeline': return ; case 'docs': return ; } } if (user.role === 'approver') { switch (view) { case 'home': return setApprovalLog(l)} openPayout={c => setPayoutContractor(c)}/>; case 'inbox': return setApprovalLog(l)}/>; case 'payouts': return setPayoutContractor(c)} openInvoiceScan={() => setShowInvoice(true)}/>; case 'dash': return ; case 'timeline': return ; case 'docs': return ; } } if (user.role === 'admin') { switch (view) { case 'home': return ; case 'inbox': return setApprovalLog(l)}/>; case 'dash': return ; case 'timeline': return ; case 'docs': return ; case 'users': return ; case 'budget': return ; case 'timing': return ; case 'permissions': return ; } } return null; }; const roleLabels = { contractor: 'Vállalkozó', approver: 'Jóváhagyó', admin: 'Admin' }; return (
{/* Top bar */}

H1

{user.name} · {roleLabels[user.role]}
H1
{/* Tartalom */} {renderView()} {/* FAB — csak vállalkozónak */} {user.role === 'contractor' && !logger && ( )} {/* Bottom nav */}
{nav.map(n => ( ))}
{/* Modálok */} {logger && setLogger(false)} onSubmit={handleSubmitLog}/>} {approvalLog && setApprovalLog(null)} onDecide={handleApprove}/>} {payoutContractor && setPayoutContractor(null)} onSubmit={handlePayout}/>} {showInvoice && setShowInvoice(false)} onSave={() => showToast('✓ Számla rögzítve')}/>} {showChat && setShowChat(false)} onToast={showToast}/>} {toast && (
{toast}
)}
); }; ReactDOM.createRoot(document.getElementById('root')).render();