// 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
Ú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 => (
))}
>
) : (
<>
{ 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]}
{/* 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();