// Jóváhagyó (Approver) nézet
const ApproverHome = ({ user, setView, openApproval, openPayout }) => {
const pending = window.WORK_LOGS.filter(l => l.status === 'pending');
const totalPending = window.totalPending();
const totalApproved = window.totalSpent();
const totalPaid = window.totalPaid();
const totalDebt = totalApproved - totalPaid;
return (
Jóváhagyó · {user.name}
{pending.length > 0 ? `${pending.length} jóváhagyásra vár` : 'Minden naprakész'}
{/* Összegek */}
{/* Jóváhagyásra váró */}
{pending.length > 0 && (
Jóváhagyásra vár
{pending.slice(0, 3).map(l =>
openApproval(l)}/>)}
)}
{/* Vállalkozói egyenlegek */}
Egyenlegek
{window.getContractors().slice(0, 3).map(c => {
const b = window.contractorBalance(c.id);
if (b.balance <= 0) return null;
return
openPayout(c)}/>;
})}
);
};
const StatCard = ({ label, value, sub, accent = 'ink' }) => {
const colors = { ink: 'var(--ink)', warn: '#8A6200', ok: 'var(--ok)', danger: 'var(--danger)' };
return (
{label}
{value}
{sub &&
{sub}
}
);
};
const ApprovalCard = ({ log, onClick }) => {
const c = window.getUser(log.contractorId);
const phase = window.getPhase(log.phaseId);
const task = window.getParentTask(log.parentTaskId);
const { total } = window.computeWorkLog(log);
const taskBudget = task.budget;
const taskSpent = window.parentTaskSpent(log.parentTaskId);
const remaining = taskBudget - taskSpent;
const overrun = total > remaining;
return (
{c.name}
{window.formatDateShort(log.date)} · {phase?.code}
{window.formatHufShort(total)} Ft
{task.name}
{log.description}
{(() => {
const s = window.workLogStats(log);
if (s.isPurchase || !s.hasLabor) return Beszerzés;
return {s.totalWorkers}fő × {s.totalHours}ó;
})()}
{log.materialCost > 0 && <> · anyag {window.formatHufShort(log.materialCost)}>}
{(() => {
const list = Array.isArray(log.photoList) ? log.photoList : (Array.isArray(log.photos) ? log.photos : null);
const n = list ? list.length : (typeof log.photos === 'number' ? log.photos : 0);
return n > 0 ? <> · {n}> : null;
})()}
{overrun ? '!' : ''} keret: {window.formatHufShort(remaining)}
);
};
const ContractorBalanceCard = ({ contractor, balance, onClick }) => (
{contractor.name}
{contractor.skill}
{window.formatHufShort(balance.balance)} Ft
kifizetésre vár
);
const ApproverInbox = ({ openApproval }) => {
const [filter, setFilter] = React.useState('pending');
const all = window.WORK_LOGS.slice().reverse();
const filtered = filter === 'all' ? all : all.filter(l => l.status === filter);
const counts = {
all: all.length,
pending: all.filter(l => l.status === 'pending').length,
approved: all.filter(l => l.status === 'approved').length,
rejected: all.filter(l => l.status === 'rejected').length,
};
return (
Bejövő teljesítések
{[
{ k: 'pending', l: 'Várakozik' },
{ k: 'approved', l: 'Jóváhagyva' },
{ k: 'rejected', l: 'Elutasítva' },
{ k: 'all', l: 'Mind' },
].map(t => (
))}
{filtered.map(l =>
openApproval(l)}/>)}
{filtered.length === 0 && Nincs ilyen tétel.
}
);
};
// Részletes jóváhagyási nézet
const ApprovalDetail = ({ log, onClose, onDecide }) => {
const c = window.getUser(log.contractorId);
const phase = window.getPhase(log.phaseId);
const task = window.getParentTask(log.parentTaskId);
const { labor, total } = window.computeWorkLog(log);
const taskBudget = task.budget;
const taskSpent = window.parentTaskSpent(log.parentTaskId);
const remaining = taskBudget - taskSpent - (log.status === 'pending' ? 0 : 0);
const [approvedAmount, setApprovedAmount] = React.useState(total);
const [note, setNote] = React.useState('');
const [showReject, setShowReject] = React.useState(false);
return (
Teljesítés #{String(log.id).padStart(4, '0')}
{/* Kontaktor + dátum */}
{c.name}
{c.skill} · {window.formatHuf(c.hourlyRate)}/óra
{window.formatDate(log.date)}
{/* Mit */}
{task.name}
{log.description}
{/* Számítás */}
Számítás
{(() => {
const s = window.workLogStats(log);
if (s.hasLabor) {
return
;
}
return null;
})()}
{log.materialCost > 0 && }
{/* Költségvetés állapot */}
Főfeladat költségvetése
0 ? 'var(--ok)' : 'var(--danger)'}/>
{total > remaining && (
⚠ A tétel {window.formatHuf(total - remaining)}-tal túllépi a fennmaradó keretet.
)}
{/* Fotók */}
{(() => {
const photoArr = Array.isArray(log.photoList) ? log.photoList
: (Array.isArray(log.photos) ? log.photos : []);
const photoCount = photoArr.length || (typeof log.photos === 'number' ? log.photos : 0);
return (
Fotók ({photoCount})
{photoArr.length > 0
? photoArr.map(p => (
))
: Array.from({ length: photoCount }).map((_, i) => (
))}
{photoCount === 0 &&
Nincs feltöltött fotó.
}
);
})()}
{log.status === 'pending' && (
<>
>
)}
{/* CTA */}
{log.status === 'pending' && (
)}
{showReject && (
setShowReject(false)} title="Elutasítás indoka">
)}
);
};
const CalcRow = ({ label, value, bold, muted, color }) => (
{label}
{window.formatHuf(value)}
);
// === Kifizetés képernyő ===
const Payouts = ({ openPayout, openInvoiceScan }) => {
const contractors = window.getContractors();
return (
Kifizetések
Vállalkozók
{contractors.map(c => {
const b = window.contractorBalance(c.id);
return
openPayout(c)}/>;
})}
Legutóbbi kifizetések
{window.PAYMENTS.slice().reverse().map(p => {
const c = window.getUser(p.contractorId);
return (
{c.name}
{window.formatDate(p.paidAt)}
{window.formatHuf(p.amountHuf)}
{p.currency !== 'HUF' &&
{p.originalAmount} {p.currency} @ {p.fxRate}
}
);
})}
);
};
// Kifizetés végrehajtás
const PayoutSheet = ({ contractor, onClose, onSubmit }) => {
const balance = window.contractorBalance(contractor.id);
const [currency, setCurrency] = React.useState('HUF');
const [amount, setAmount] = React.useState(balance.balance);
const [note, setNote] = React.useState('');
const [invoicePhoto, setInvoicePhoto] = React.useState(null);
const invoiceRef = React.useRef(null);
const fx = window.FX_RATES[currency];
const amountInHuf = currency === 'HUF' ? amount : Math.round(amount * fx);
const remaining = balance.balance - amountInHuf;
return (
{['HUF', 'EUR', 'USD'].map(cur => (
))}
setAmount(parseFloat(e.target.value) || 0)}/>
{currency}
{currency !== 'HUF' && (
Mai MNB árfolyam: 1 {currency} = {fx} Ft → {window.formatHuf(amountInHuf)}
)}
setNote(e.target.value)} placeholder="pl. anyagvásárlásra"/>
setInvoicePhoto(e.target.files?.[0] || null)}/>
);
};
// Számla scan / OCR mock
const InvoiceScan = ({ onClose, onSave }) => {
const [step, setStep] = React.useState('camera');
const [vendor, setVendor] = React.useState('');
const [amount, setAmount] = React.useState(0);
const [phaseId, setPhaseId] = React.useState(null);
const fakeScan = () => {
setStep('result');
setVendor('PRAKTIKER Kft.');
setAmount(187420);
setPhaseId(9);
};
return (
{step === 'camera' && (
Készíts fotót a számláról
Az OCR automatikusan kitölti a mezőket — javíthatod.
)}
{step === 'result' && (
)}
);
};
Object.assign(window, { ApproverHome, ApproverInbox, ApprovalDetail, Payouts, PayoutSheet, InvoiceScan, ApprovalCard, ContractorBalanceCard, StatCard, CalcRow });