fix: rewrite attendance print to match PHP design 1:1
- Dark table headers (#333), proper column widths, uppercase labels - User header bar with gray background and total hours - Records from userData.records (not filtered from global records) - Fund row with covered/total and status badge - Leave summary with vacation remaining - Print wrapper table for repeating header - Matching CSS: borders, fonts, spacing, badges Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -245,110 +245,154 @@ function buildLeaveSummaryHtml(userId: string, userData: Record<string, any>, pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildUserSectionHtml(userId: string, userData: Record<string, any>, printData: Record<string, any>): string {
|
function buildUserSectionHtml(userId: string, userData: Record<string, any>, printData: Record<string, any>): string {
|
||||||
const records = (printData.records || []).filter((r: any) => String(r.user_id) === userId)
|
const leaveHtml = printData.leave_balances[userId]
|
||||||
let rows = ''
|
? buildLeaveSummaryHtml(userId, userData, printData)
|
||||||
for (const record of records) {
|
: ''
|
||||||
|
|
||||||
|
const recordRows = (userData.records || []).map((record: any) => {
|
||||||
const leaveType = record.leave_type || 'work'
|
const leaveType = record.leave_type || 'work'
|
||||||
const typeBadge = leaveType === 'work'
|
const isLeave = leaveType !== 'work'
|
||||||
? 'Práce'
|
const workMinutes = calculateWorkMinutesPrint(record)
|
||||||
: `<span class="leave-badge ${getLeaveTypeBadgeClass(leaveType)}">${getLeaveTypeName(leaveType)}</span>`
|
const hours = Math.floor(workMinutes / 60)
|
||||||
const workMins = calculateWorkMinutesPrint(record)
|
const mins = workMinutes % 60
|
||||||
const hoursStr = workMins > 0 ? `${Math.floor(workMins / 60)}:${String(workMins % 60).padStart(2, '0')}` : (leaveType !== 'work' ? `${record.leave_hours || 8}h` : '—')
|
const breakCell = (isLeave || !record.break_start || !record.break_end)
|
||||||
rows += `<tr>
|
? '—'
|
||||||
|
: `${formatTimeOrDatetimePrint(record.break_start, record.shift_date)} - ${formatTimeOrDatetimePrint(record.break_end, record.shift_date)}`
|
||||||
|
|
||||||
|
return `<tr>
|
||||||
<td>${formatDate(record.shift_date)}</td>
|
<td>${formatDate(record.shift_date)}</td>
|
||||||
<td>${typeBadge}</td>
|
<td><span class="leave-badge ${getLeaveTypeBadgeClass(leaveType)}">${getLeaveTypeName(leaveType)}</span></td>
|
||||||
<td>${formatTimeOrDatetimePrint(record.arrival_time, record.shift_date)}</td>
|
<td class="text-center">${isLeave ? '—' : formatTimeOrDatetimePrint(record.arrival_time, record.shift_date)}</td>
|
||||||
<td>${record.break_start ? formatTimeOrDatetimePrint(record.break_start, record.shift_date) + ' – ' + formatTimeOrDatetimePrint(record.break_end, record.shift_date) : '—'}</td>
|
<td class="text-center">${breakCell}</td>
|
||||||
<td>${formatTimeOrDatetimePrint(record.departure_time, record.shift_date)}</td>
|
<td class="text-center">${isLeave ? '—' : formatTimeOrDatetimePrint(record.departure_time, record.shift_date)}</td>
|
||||||
<td>${hoursStr}</td>
|
<td class="text-center">${workMinutes > 0 ? `${hours}:${String(mins).padStart(2, '0')}` : '—'}</td>
|
||||||
<td>${buildProjectLogsHtml(record)}</td>
|
<td style="font-size:8px">${buildProjectLogsHtml(record)}</td>
|
||||||
<td>${record.notes || ''}</td>
|
<td>${record.notes || ''}</td>
|
||||||
</tr>`
|
</tr>`
|
||||||
}
|
}).join('')
|
||||||
|
|
||||||
const leaveSummary = printData.leave_balances[userId] ? buildLeaveSummaryHtml(userId, userData, printData) : ''
|
const fundRow = userData.fund !== null
|
||||||
|
? `<tr>
|
||||||
|
<td colspan="6" class="text-right">Fond měsíce:</td>
|
||||||
|
<td class="text-center">${userData.covered}h / ${userData.fund}h</td>
|
||||||
|
<td colspan="2">${renderFundStatus(userData)}</td>
|
||||||
|
</tr>`
|
||||||
|
: ''
|
||||||
|
|
||||||
return `<div class="user-section">
|
return `<div class="user-section">
|
||||||
<h2>${userData.name}</h2>
|
<div class="user-header">
|
||||||
${leaveSummary}
|
<h3>${userData.name}</h3>
|
||||||
|
<span class="total">Odpracováno: ${formatMinutes(userData.minutes)} h</span>
|
||||||
|
</div>
|
||||||
|
${leaveHtml}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead><tr>
|
||||||
<tr>
|
<th style="width:70px">Datum</th>
|
||||||
<th>Datum</th><th>Typ</th><th>Příchod</th><th>Pauza</th><th>Odchod</th><th>Hodiny</th><th>Projekty</th><th>Poznámka</th>
|
<th style="width:70px">Typ</th>
|
||||||
</tr>
|
<th class="text-center" style="width:70px">Příchod</th>
|
||||||
</thead>
|
<th class="text-center" style="width:90px">Pauza</th>
|
||||||
<tbody>${rows}</tbody>
|
<th class="text-center" style="width:70px">Odchod</th>
|
||||||
|
<th class="text-center" style="width:80px">Hodiny</th>
|
||||||
|
<th>Projekty</th>
|
||||||
|
<th>Poznámka</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>${recordRows}</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="total-row">
|
<tr>
|
||||||
<td colspan="5"><strong>Celkem odpracováno</strong></td>
|
<td colspan="6" class="text-right">Odpracováno:</td>
|
||||||
<td><strong>${formatMinutes(userData.minutes)}</strong></td>
|
<td class="text-center">${formatMinutes(userData.minutes)} h</td>
|
||||||
<td colspan="2"></td>
|
<td colspan="2"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
${fundRow}
|
||||||
<td colspan="5"><strong>Fond: ${userData.fund ?? '—'}h (${userData.business_days ?? '—'} prac. dnů)</strong></td>
|
|
||||||
<td colspan="3">${renderFundStatus(userData)}</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>`
|
</div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPrintHtml(pData: Record<string, any>, userSections: string, emptyMsg: string, filterNote: string): string {
|
function buildPrintHtml(pData: Record<string, any>, userSections: string, emptyMsg: string, filterNote: string): string {
|
||||||
const now = new Date()
|
|
||||||
const generated = now.toLocaleDateString('cs-CZ') + ' ' + now.toLocaleTimeString('cs-CZ', { hour: '2-digit', minute: '2-digit' })
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="cs">
|
<html lang="cs">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Docházka - ${pData.month_name}</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<title>Docházka - ${pData.month_name}</title>
|
||||||
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #1a1a1a; padding: 10mm; }
|
body {
|
||||||
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #1e40af; padding-bottom: 10px; margin-bottom: 15px; }
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
.header-left { display: flex; align-items: center; gap: 15px; }
|
font-size: 11px; line-height: 1.4; color: #000; background: #fff; padding: 15mm;
|
||||||
.header-left img { height: 40px; }
|
}
|
||||||
.header-title { font-size: 16px; font-weight: bold; color: #1e40af; }
|
.print-header {
|
||||||
.header-company { font-size: 11px; color: #666; }
|
display: flex; justify-content: space-between; align-items: flex-start;
|
||||||
.header-right { text-align: right; font-size: 10px; color: #666; }
|
margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #333;
|
||||||
.filters { background: #f0f4ff; padding: 5px 10px; border-radius: 4px; margin-bottom: 10px; font-size: 10px; }
|
}
|
||||||
.user-section { margin-bottom: 20px; page-break-inside: avoid; }
|
.print-header-left { display: flex; align-items: center; gap: 12px; }
|
||||||
.user-section h2 { font-size: 13px; color: #1e40af; border-bottom: 1px solid #ddd; padding-bottom: 3px; margin-bottom: 5px; }
|
.print-logo { height: 40px; width: auto; }
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 10px; margin-bottom: 5px; }
|
.print-header-text { text-align: left; }
|
||||||
th { background: #f1f5f9; text-align: left; padding: 4px 6px; border: 1px solid #ddd; font-weight: 600; }
|
.print-header-right { text-align: right; }
|
||||||
td { padding: 3px 6px; border: 1px solid #eee; }
|
.print-header h1 { font-size: 18px; font-weight: 700; margin-bottom: 3px; }
|
||||||
tbody tr:nth-child(even) { background: #fafafa; }
|
.print-header .company { font-size: 11px; color: #666; }
|
||||||
.total-row td { background: #f1f5f9; font-weight: bold; border-top: 2px solid #1e40af; }
|
.print-header .period { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 2px; }
|
||||||
.leave-summary { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 4px; padding: 5px 8px; margin-bottom: 5px; font-size: 10px; }
|
.print-header .filters { font-size: 10px; color: #666; }
|
||||||
.leave-badge { padding: 1px 6px; border-radius: 3px; font-size: 9px; font-weight: 600; }
|
.print-header .generated { font-size: 9px; color: #888; margin-top: 5px; }
|
||||||
.badge-vacation { background: #dbeafe; color: #1e40af; }
|
.user-section { margin-bottom: 25px; page-break-inside: avoid; }
|
||||||
|
.user-header {
|
||||||
|
background: #f5f5f5; border: 1px solid #ddd; padding: 10px 15px;
|
||||||
|
margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;
|
||||||
|
}
|
||||||
|
.user-header h3 { font-size: 13px; font-weight: 600; }
|
||||||
|
.user-header .total { font-size: 12px; font-weight: 600; }
|
||||||
|
.user-section table { width: 100%; border-collapse: collapse; margin-bottom: 15px; }
|
||||||
|
.user-section th, .user-section td { border: 1px solid #333; padding: 6px 8px; text-align: left; }
|
||||||
|
.user-section th { background: #333; color: #fff; font-weight: 600; font-size: 10px; text-transform: uppercase; }
|
||||||
|
.user-section td { font-size: 10px; }
|
||||||
|
.user-section tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.text-right { text-align: right; }
|
||||||
|
.user-section tfoot td { background: #eee; font-weight: 600; }
|
||||||
|
.leave-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 9px; font-weight: 500; }
|
||||||
|
.badge-vacation { background: #dbeafe; color: #1d4ed8; }
|
||||||
.badge-sick { background: #fee2e2; color: #dc2626; }
|
.badge-sick { background: #fee2e2; color: #dc2626; }
|
||||||
.badge-holiday { background: #fef3c7; color: #92400e; }
|
.badge-holiday { background: #dcfce7; color: #16a34a; }
|
||||||
.badge-overtime { background: #d1fae5; color: #065f46; }
|
.badge-unpaid { background: #f3f4f6; color: #6b7280; }
|
||||||
.badge-unpaid { background: #f3f4f6; color: #374151; }
|
.badge-overtime { background: #fef3c7; color: #d97706; }
|
||||||
|
.leave-summary {
|
||||||
|
margin-top: 10px; padding: 8px 15px; background: #f9f9f9;
|
||||||
|
border: 1px solid #ddd; font-size: 10px;
|
||||||
|
}
|
||||||
|
.print-wrapper-table { width: 100%; border-collapse: collapse; border: none; }
|
||||||
|
.print-wrapper-table > thead > tr > td,
|
||||||
|
.print-wrapper-table > tbody > tr > td { padding: 0; border: none; background: none; }
|
||||||
@media print {
|
@media print {
|
||||||
body { padding: 0; }
|
body { padding: 0; margin: 0; }
|
||||||
|
@page { size: A4 portrait; margin: 10mm; }
|
||||||
.user-section { page-break-inside: avoid; }
|
.user-section { page-break-inside: avoid; }
|
||||||
}
|
}
|
||||||
@page { size: A4 portrait; margin: 10mm; }
|
</style>
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<table class="print-wrapper-table">
|
||||||
<div class="header-left">
|
<thead><tr><td>
|
||||||
<img src="/images/logo-light.png" alt="Logo">
|
<div class="print-header">
|
||||||
<div>
|
<div class="print-header-left">
|
||||||
<div class="header-title">EVIDENCE DOCHÁZKY</div>
|
<img src="/images/logo-light.png" alt="BOHA" class="print-logo" />
|
||||||
<div class="header-company">BOHA Automation s.r.o.</div>
|
<div class="print-header-text">
|
||||||
</div>
|
<h1>EVIDENCE DOCHÁZKY</h1>
|
||||||
</div>
|
<div class="company">BOHA Automation s.r.o.</div>
|
||||||
<div class="header-right">
|
|
||||||
<div><strong>Období:</strong> ${pData.month_name}</div>
|
|
||||||
<div>Vygenerováno: ${generated}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="print-header-right">
|
||||||
|
<div class="period">${pData.month_name}</div>
|
||||||
${filterNote}
|
${filterNote}
|
||||||
${emptyMsg}
|
<div class="generated">Vygenerováno: ${new Date().toLocaleString('cs-CZ')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td></tr></thead>
|
||||||
|
<tbody><tr><td>
|
||||||
${userSections}
|
${userSections}
|
||||||
|
${emptyMsg}
|
||||||
|
</td></tr></tbody>
|
||||||
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user