dd
Ewidencja Czasu Pracy i Rozliczeń Pracowników tailwind.config = { darkMode: 'class', theme: { extend: { colors: { brand: { 50: '#eef2ff', 100: '#e0e7ff', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca', } } } } } @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); body { font-family: 'Inter', sans-serif; } .editable-cell { transition: all 0.2s; cursor: pointer; } .editable-cell:hover { background-color: #f1f5f9; } .editable-cell:focus { background-color: #ffffff; outline: 2px solid #6366f1; outline-offset: -2px; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2); } /* Custom scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: #f1f5f9; } ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } @media print { .no-print { display: none !important; } body { background: white; color: black; } .print-table { box-shadow: none !important; border: 1px solid #e2e8f0; } } Wszyscy pracownicy
`; tbody.appendChild(tr); }); } // Update footer totals document.getElementById('tfootHours').innerText = `${totalHours.toFixed(1)} h`; document.getElementById('tfootAdvance').innerText = `${totalAdvance.toFixed(2)} zł`; document.getElementById('tfootTotal').innerText = `${totalEarned.toFixed(2)} zł`; document.getElementById('tfootBalance').innerText = `${totalNet.toFixed(2)} zł`; // Update top stats cards document.getElementById('statTotalHours').innerText = `${totalHours.toFixed(1)} h`; document.getElementById('statAvgHours').innerText = `Śr. ${filtered.length > 0 ? (totalHours / filtered.length).toFixed(1) : 0} h / wpis`; document.getElementById('statTotalAdvance').innerText = `${totalAdvance.toFixed(2)} zł`; const advCount = filtered.filter(f => parseFloat(f.advance) > 0).length; document.getElementById('statAdvanceCount').innerText = `${advCount} wypłat zaliczkowych`; document.getElementById('statTotalEarned').innerText = `${totalEarned.toFixed(2)} zł`; document.getElementById('statNetToPay').innerText = `${totalNet.toFixed(2)} zł`; document.getElementById('recordCountBadge').innerText = `${filtered.length} wpisów`; applyColumnVisibility(); lucide.createIcons(); } // Live Field Update function updateField(id, field, value) { const index = records.findIndex(r => r.id === id); if (index !== -1) { records[index][field] = value.trim(); saveRecords(); showToast('Zaktualizowano dane'); } } function updateNumberField(id, field, value) { const index = records.findIndex(r => r.id === id); if (index !== -1) { // replace comma with dot const cleanVal = parseFloat(value.replace(',', '.').replace(/[^\d.-]/g, '')) || 0; records[index][field] = cleanVal; saveRecords(); showToast('Zaktualizowano kwotę/godziny'); } } // Add Entry Form Submit function handleFormSubmit(e) { e.preventDefault(); const date = document.getElementById('formDate').value; const employee = document.getElementById('formEmployee').value.trim(); const location = document.getElementById('formLocation').value.trim(); const hours = parseFloat(document.getElementById('formHours').value) || 0; const rate = parseFloat(document.getElementById('formRate').value) || 0; const advance = parseFloat(document.getElementById('formAdvance').value) || 0; const notes = document.getElementById('formNotes').value.trim(); if (!employee || !location || !date) { alert('Proszę wypełnić wszystkie wymagane pola (Data, Pracownik, Miejsce pracy).'); return; } const newRecord = { id: Date.now(), date, employee, location, hours, rate, advance, notes }; records.unshift(newRecord); saveRecords(); closeModal('addEntryModal'); showToast('Dodano nowy wpis do rejestru'); // Reset notes & advance, keep location/employee for convenient next entry document.getElementById('formAdvance').value = 0; document.getElementById('formNotes').value = ''; } // Add Quick Empty Row function addQuickRow() { const today = new Date().toISOString().split('T')[0]; const newRecord = { id: Date.now(), date: today, employee: 'Nowy Pracownik', location: 'Budowa - Nowa lokalizacja', hours: 8, rate: 30, advance: 0, notes: '' }; records.unshift(newRecord); saveRecords(); showToast('Dodano szybki wiersz. Kliknij aby edytować.'); } // Duplicate Row function duplicateRow(id) { const item = records.find(r => r.id === id); if (item) { const clone = { ...item, id: Date.now() }; records.unshift(clone); saveRecords(); showToast('Powielono wpis'); } } // Delete Row function deleteRecord(id) { if (confirm('Czy na pewno chcesz usunąć ten wpis z ewidencji?')) { records = records.filter(r => r.id !== id); saveRecords(); showToast('Wpis został usunięty'); } } // Reset to initial demo function confirmResetData() { if (confirm('Czy chcesz przywrócić domyślne przykładowe wpisy? Twoje bieżące zmiany zostaną zastąpione danymi demo.')) { records = [...DEFAULT_RECORDS]; saveRecords(); showToast('Przywrócono dane przykładowe'); } } // Update Worker selection dropdowns function updateWorkerFilters() { const workers = Array.from(new Set(records.map(r => r.employee).filter(Boolean))).sort(); const locations = Array.from(new Set(records.map(r => r.location).filter(Boolean))).sort(); // Filter select const workerFilter = document.getElementById('workerFilter'); const currentSelected = workerFilter.value; workerFilter.innerHTML = 'Wszyscy pracownicy'; workers.forEach(w => { const opt = document.createElement('option'); opt.value = w; opt.innerText = w; if (w === currentSelected) opt.selected = true; workerFilter.appendChild(opt); }); // Datalists for autocomplete const empDatalist = document.getElementById('employeesDatalist'); empDatalist.innerHTML = workers.map(w => ``).join(''); const locDatalist = document.getElementById('locationsDatalist'); locDatalist.innerHTML = locations.map(l => ``).join(''); } function resetFilters() { document.getElementById('searchInput').value = ''; document.getElementById('workerFilter').value = ''; document.getElementById('monthFilter').value = ''; renderTable(); showToast('Filtry wyczyszczone'); } // Event Listeners for Filters function setupEventListeners() { document.getElementById('searchInput').addEventListener('input', renderTable); document.getElementById('workerFilter').addEventListener('change', renderTable); document.getElementById('monthFilter').addEventListener('change', renderTable); } // Modal Controls function openModal(modalId) { document.getElementById(modalId).classList.remove('hidden'); } function closeModal(modalId) { document.getElementById(modalId).classList.add('hidden'); } // Charts & Analysis Modal function toggleChartModal() { const modal = document.getElementById('chartModal'); if (modal.classList.contains('hidden')) { modal.classList.remove('hidden'); renderCharts(); } else { modal.classList.add('hidden'); } } function renderCharts() { const filtered = getFilteredRecords(); // Group by employee const workerMap = {}; filtered.forEach(r => { if (!workerMap[r.employee]) { workerMap[r.employee] = { hours: 0, advance: 0, earned: 0 }; } const h = parseFloat(r.hours) || 0; const adv = parseFloat(r.advance) || 0; const rate = parseFloat(r.rate) || 0; workerMap[r.employee].hours += h; workerMap[r.employee].advance += adv; workerMap[r.employee].earned += (h * rate); }); const labels = Object.keys(workerMap); const hoursData = labels.map(l => workerMap[l].hours); const advanceData = labels.map(l => workerMap[l].advance); const netData = labels.map(l => workerMap[l].earned - workerMap[l].advance); // Populate summary table in modal const summaryBody = document.getElementById('workerSummaryBody'); summaryBody.innerHTML = ''; labels.forEach(w => { const tr = document.createElement('tr'); tr.innerHTML = ` ${escapeHtml(w)} ${workerMap[w].hours.toFixed(1)} h ${workerMap[w].advance.toFixed(2)} zł ${(workerMap[w].earned - workerMap[w].advance).toFixed(2)} zł `; summaryBody.appendChild(tr); }); // Hours Chart (Bar) const ctxHours = document.getElementById('chartHours').getContext('2d'); if (chartHoursInstance) chartHoursInstance.destroy(); chartHoursInstance = new Chart(ctxHours, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Suma godzin (h)', data: hoursData, backgroundColor: '#6366f1', borderRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } } }); // Finances Chart (Doughnut) const totalAdv = advanceData.reduce((a, b) => a + b, 0); const totalNet = netData.reduce((a, b) => a + b, 0); const ctxFin = document.getElementById('chartFinances').getContext('2d'); if (chartFinancesInstance) chartFinancesInstance.destroy(); chartFinancesInstance = new Chart(ctxFin, { type: 'doughnut', data: { labels: ['Pobrane Zaliczki (zł)', 'Do wypłaty netto (zł)'], datasets: [{ data: [totalAdv, Math.max(0, totalNet)], backgroundColor: ['#f43f5e', '#10b981'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 11 } } } } } }); } // CSV Export function exportToCSV() { const filtered = getFilteredRecords(); if (filtered.length === 0) { alert('Brak danych do wyeksportowania!'); return; } const headers = ['Data', 'Pracownik', 'Miejsce pracy / Budowa', 'Godziny (h)', 'Stawka/h (zł)', 'Pobrana zaliczka (zł)', 'Wartość brutto (zł)', 'Do wypłaty (zł)', 'Uwagi']; const rows = filtered.map(r => { const hours = parseFloat(r.hours) || 0; const rate = parseFloat(r.rate) || 0; const adv = parseFloat(r.advance) || 0; const total = hours * rate; const balance = total - adv; return [ `"${r.date}"`, `"${(r.employee || '').replace(/"/g, '""')}"`, `"${(r.location || '').replace(/"/g, '""')}"`, hours.toFixed(2), rate.toFixed(2), adv.toFixed(2), total.toFixed(2), balance.toFixed(2), `"${(r.notes || '').replace(/"/g, '""')}"` ].join(';'); }); const csvContent = '\uFEFF' + [headers.join(';'), ...rows].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', `ewidencja_pracy_${new Date().toISOString().split('T')[0]}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); showToast('Eksport CSV został pobrany'); } // Toast feedback function showToast(msg) { const toast = document.getElementById('toast'); document.getElementById('toastMsg').innerText = msg; toast.classList.remove('translate-y-20', 'opacity-0'); toast.classList.add('translate-y-0', 'opacity-100'); setTimeout(() => { toast.classList.remove('translate-y-0', 'opacity-100'); toast.classList.add('translate-y-20', 'opacity-0'); }, 2500); } // Safe HTML utility function escapeHtml(text) { if (!text) return ''; return text .toString() .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }
WorkLog Pro
Rejestracja godzin & rozliczenia
Dostosuj Kolumny Eksport CSV Drukuj Dodaj wpis
Przepracowane godziny
0 h
Śr. 0 h / wpis
Pobrane zaliczki
0 zł
0 wypłat zaliczkowych
Wartość pracy (Wycena)
0 zł
Wg stawek godzinowych
Pozostało do rozliczenia
0 zł
Po odliczeniu zaliczek
Wyczyść filtry Wykresy & Analiza
Rejestr Dzienny Czasu Pracy 0 wpisów
Kliknij w dowolną komórkę tabeli, aby edytować dane na żywo. Zmiany zapisują się automatycznie.
Szybki wiersz
| Lp. | Data | Pracownik | Miejsce pracy / Budowa | Godziny (h) | Stawka/h (zł) | Zaliczka (zł) | Wartość (zł) | Do wypłaty | Uwagi | Akcje |
|---|---|---|---|---|---|---|---|---|---|---|
| Podsumowanie tabeli: | 0 h | 0 zł | 0 zł | 0 zł | ||||||
Brak zarejestrowanych wpisów
Brak danych dla wybranych filtrów lub nie dodano jeszcze żadnego rekordu.
Dodaj pierwszy wpis Wskazówka: Kliknij dwukrotnie lub naciśnij Enter na dowolnym polu, aby edytować dane inline. Wartości podsumowań, wyliczenia stawek i salda aktualizują się automatycznie!
WorkLog Pro © 2025 - System rejestracji godzin i zaliczek pracowników Dane przechowywane lokalnie w pamięci przeglądarki (LocalStorage)
Operacja wykonana pomyślnie
// Sample Initial Data const DEFAULT_RECORDS = [ { id: 1, date: '2025-05-12', employee: 'Jan Kowalski', location: 'Budowa - Osiedle Leśne, Warszawa', hours: 8.5, rate: 35, advance: 100, notes: 'Montaż instalacji elektrycznej' }, { id: 2, date: '2025-05-12', employee: 'Piotr Nowak', location: 'Remont Biura Al. Jerozolimskie', hours: 10, rate: 32, advance: 0, notes: 'Szpachlowanie i malowanie ścian' }, { id: 3, date: '2025-05-13', employee: 'Marek Wiśniewski', location: 'Budowa - Osiedle Leśne, Warszawa', hours: 7.5, rate: 30, advance: 150, notes: 'Pobrano zaliczkę na zakup osprzętu' }, { id: 4, date: '2025-05-13', employee: 'Jan Kowalski', location: 'Hala Magazynowa Pruszków', hours: 9, rate: 35, advance: 50, notes: 'Dodatkowa 1h nadgodzin zatwierdzona' }, { id: 5, date: '2025-05-14', employee: 'Tomasz Zieliński', location: 'Dom jednorodzinny Piaseczno', hours: 8, rate: 30, advance: 0, notes: 'Układanie płytek w kotłowni' }, { id: 6, date: '2025-05-14', employee: 'Piotr Nowak', location: 'Dom jednorodzinny Piaseczno', hours: 8.5, rate: 32, advance: 200, notes: 'Zaliczka weekendowa' } ]; // Column definitions const COLUMNS_CONFIG = [ { key: 'date', label: 'Data', defaultVisible: true }, { key: 'employee', label: 'Pracownik', defaultVisible: true }, { key: 'location', label: 'Miejsce pracy / Budowa', defaultVisible: true }, { key: 'hours', label: 'Godziny (h)', defaultVisible: true }, { key: 'rate', label: 'Stawka/h (zł)', defaultVisible: true }, { key: 'advance', label: 'Pobrana zaliczka (zł)', defaultVisible: true }, { key: 'total', label: 'Wartość brutto (zł)', defaultVisible: true }, { key: 'balance', label: 'Do wypłaty (zł)', defaultVisible: true }, { key: 'notes', label: 'Uwagi i komentarze', defaultVisible: true } ]; // State variables let records = []; let columnVisibility = {}; let chartHoursInstance = null; let chartFinancesInstance = null; // Initialize App document.addEventListener('DOMContentLoaded', () => { loadSettings(); loadRecords(); initColumnManager(); initDateFilter(); renderTable(); updateWorkerFilters(); setupEventListeners(); lucide.createIcons(); }); // Local Storage Handlers function loadRecords() { const saved = localStorage.getItem('worklog_records'); if (saved) { try { records = JSON.parse(saved); } catch(e) { records = [...DEFAULT_RECORDS]; } } else { records = [...DEFAULT_RECORDS]; saveRecords(); } } function saveRecords() { localStorage.setItem('worklog_records', JSON.stringify(records)); updateWorkerFilters(); renderTable(); } function loadSettings() { const savedCols = localStorage.getItem('worklog_columns'); if (savedCols) { try { columnVisibility = JSON.parse(savedCols); } catch(e) { initDefaultColumns(); } } else { initDefaultColumns(); } } function initDefaultColumns() { columnVisibility = {}; COLUMNS_CONFIG.forEach(col => { columnVisibility[col.key] = col.defaultVisible; }); localStorage.setItem('worklog_columns', JSON.stringify(columnVisibility)); } function initDateFilter() { const today = new Date().toISOString().split('T')[0]; document.getElementById('formDate').value = today; } // Setup Column Management Modal function initColumnManager() { const container = document.getElementById('columnToggles'); container.innerHTML = ''; COLUMNS_CONFIG.forEach(col => { const isChecked = columnVisibility[col.key] !== false; const div = document.createElement('label'); div.className = 'flex items-center justify-between p-2.5 rounded-xl border border-slate-200 hover:bg-slate-50 cursor-pointer transition'; div.innerHTML = ` ${col.label} `; const checkbox = div.querySelector('input'); checkbox.addEventListener('change', (e) => { columnVisibility[col.key] = e.target.checked; localStorage.setItem('worklog_columns', JSON.stringify(columnVisibility)); applyColumnVisibility(); }); container.appendChild(div); }); document.getElementById('btnManageColumns').addEventListener('click', () => { openModal('columnModal'); }); applyColumnVisibility(); } function applyColumnVisibility() { COLUMNS_CONFIG.forEach(col => { const isVisible = columnVisibility[col.key] !== false; const elements = document.querySelectorAll(`.col-${col.key}`); elements.forEach(el => { if (isVisible) { el.classList.remove('hidden'); } else { el.classList.add('hidden'); } }); }); } // Filter and Search logic function getFilteredRecords() { const search = document.getElementById('searchInput').value.toLowerCase().trim(); const worker = document.getElementById('workerFilter').value; const month = document.getElementById('monthFilter').value; return records.filter(item => { // Search filter const matchesSearch = !search || item.employee.toLowerCase().includes(search) || item.location.toLowerCase().includes(search) || (item.notes && item.notes.toLowerCase().includes(search)); // Worker filter const matchesWorker = !worker || item.employee === worker; // Month filter (YYYY-MM) const matchesMonth = !month || item.date.startsWith(month); return matchesSearch && matchesWorker && matchesMonth; }); } // Render Table & Recalculate KPIs function renderTable() { const tbody = document.getElementById('recordsTableBody'); const emptyState = document.getElementById('emptyState'); const filtered = getFilteredRecords(); tbody.innerHTML = ''; let totalHours = 0; let totalAdvance = 0; let totalEarned = 0; let totalNet = 0; if (filtered.length === 0) { emptyState.classList.remove('hidden'); document.getElementById('mainTable').querySelector('tfoot').classList.add('hidden'); } else { emptyState.classList.add('hidden'); document.getElementById('mainTable').querySelector('tfoot').classList.remove('hidden'); // Sort newest date first const sorted = [...filtered].sort((a, b) => new Date(b.date) - new Date(a.date)); sorted.forEach((row, index) => { const rowHours = parseFloat(row.hours) || 0; const rowRate = parseFloat(row.rate) || 0; const rowAdvance = parseFloat(row.advance) || 0; const rowEarned = rowHours * rowRate; const rowBalance = rowEarned - rowAdvance; totalHours += rowHours; totalAdvance += rowAdvance; totalEarned += rowEarned; totalNet += rowBalance; const tr = document.createElement('tr'); tr.className = 'hover:bg-indigo-50/30 transition-colors group'; tr.setAttribute('data-id', row.id); tr.innerHTML = ` ${index + 1} ${escapeHtml(row.employee)} ${escapeHtml(row.location)}
${rowHours.toFixed(1)} ${rowRate.toFixed(2)} ${rowAdvance.toFixed(2)} ${rowEarned.toFixed(2)} zł ${rowBalance.toFixed(2)} zł ${escapeHtml(row.notes || '')} 