| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>Penalty Contractor Recorder</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); |
| |
| * { |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Roboto', sans-serif; |
| background: #f4f7fb; |
| margin: 0; |
| padding: 0; |
| min-height: 100vh; |
| color: #333; |
| } |
| |
| .container { |
| max-width: 900px; |
| width: 100%; |
| background: #fff; |
| margin: 40px auto 60px; |
| padding: 30px 40px; |
| border-radius: 10px; |
| box-shadow: 0 10px 25px rgba(0,0,0,0.1); |
| } |
| |
| h1 { |
| text-align: center; |
| color: #1976d2; |
| margin-bottom: 30px; |
| } |
| |
| |
| .tabs { |
| display: flex; |
| border-bottom: 3px solid #1976d2; |
| margin-bottom: 25px; |
| } |
| |
| .tab { |
| flex: 1; |
| padding: 15px 0; |
| text-align: center; |
| font-weight: 700; |
| color: #1976d2; |
| cursor: pointer; |
| border-bottom: 3px solid transparent; |
| transition: border-color 0.3s, background-color 0.3s; |
| user-select: none; |
| } |
| |
| .tab.active { |
| border-bottom-color: #1976d2; |
| background-color: #e6f0fa; |
| } |
| |
| |
| form { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 20px 30px; |
| } |
| |
| label { |
| font-weight: 700; |
| margin-bottom: 8px; |
| display: block; |
| color: #444; |
| } |
| |
| input[type="text"], |
| input[type="date"], |
| input[type="number"], |
| textarea, |
| input[type="file"] { |
| width: 100%; |
| padding: 8px 12px; |
| border: 2px solid #ddd; |
| border-radius: 6px; |
| font-size: 1rem; |
| transition: border-color 0.3s; |
| } |
| |
| input[type="text"]:focus, |
| input[type="date"]:focus, |
| input[type="number"]:focus, |
| textarea:focus, |
| input[type="file"]:focus { |
| border-color: #1976d2; |
| outline: none; |
| } |
| |
| textarea { |
| resize: vertical; |
| min-height: 60px; |
| } |
| |
| .full-width { |
| grid-column: 1 / -1; |
| } |
| |
| button { |
| grid-column: 1 / -1; |
| background-color: #1976d2; |
| color: white; |
| border: none; |
| padding: 14px; |
| font-size: 1.1rem; |
| border-radius: 8px; |
| cursor: pointer; |
| font-weight: 600; |
| transition: background-color 0.3s; |
| } |
| |
| button:hover { |
| background-color: #155a9f; |
| } |
| |
| |
| .penalty-list { |
| margin-top: 10px; |
| } |
| .penalty-list h2 { |
| color: #1976d2; |
| border-bottom: 2px solid #1976d2; |
| padding-bottom: 8px; |
| margin-bottom: 10px; |
| } |
| table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 12px; |
| } |
| th, td { |
| text-align: left; |
| padding: 12px 15px; |
| border-bottom: 1px solid #ddd; |
| vertical-align: middle; |
| } |
| th { |
| background-color: #1976d2; |
| color: white; |
| text-transform: uppercase; |
| font-weight: 700; |
| } |
| tr:hover { |
| background-color: #f1f9ff; |
| } |
| .btn-delete { |
| background-color: #e53935; |
| color: white; |
| border: none; |
| padding: 6px 10px; |
| border-radius: 5px; |
| cursor: pointer; |
| font-weight: 700; |
| transition: background-color 0.3s; |
| } |
| .btn-delete:hover { |
| background-color: #b71c1c; |
| } |
| |
| |
| .filters { |
| margin-bottom: 20px; |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); |
| gap: 15px 20px; |
| } |
| |
| .filters label { |
| font-weight: 600; |
| margin-bottom: 4px; |
| } |
| |
| .filters input[type="text"], |
| .filters input[type="date"], |
| .filters input[type="number"] { |
| padding: 6px 10px; |
| font-size: 0.95rem; |
| border-radius: 5px; |
| border: 1.5px solid #ccc; |
| transition: border-color 0.3s; |
| } |
| |
| .filters input[type="text"]:focus, |
| .filters input[type="date"]:focus, |
| .filters input[type="number"]:focus { |
| border-color: #1976d2; |
| outline: none; |
| } |
| |
| |
| @media (max-width: 600px) { |
| form { |
| grid-template-columns: 1fr; |
| } |
| .filters { |
| grid-template-columns: 1fr; |
| } |
| } |
| |
| .filename-display { |
| margin-top: 4px; |
| font-size: 0.9rem; |
| color: #1976d2; |
| font-style: italic; |
| } |
| |
| .no-data { |
| text-align: center; |
| margin-top: 20px; |
| color: #777; |
| font-style: italic; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>Penalty Contractor Recorder</h1> |
| <div class="tabs"> |
| <div class="tab active" data-tab="add">Add Penalty</div> |
| <div class="tab" data-tab="history">Historical Data</div> |
| </div> |
|
|
| |
| <div id="tab-add" class="tab-content"> |
| <form id="penaltyForm" autocomplete="off"> |
| <div> |
| <label for="contractorName">Contractor Name *</label> |
| <input type="text" id="contractorName" name="contractorName" required placeholder="John Doe" /> |
| </div> |
| <div> |
| <label for="companyName">Company Name *</label> |
| <input type="text" id="companyName" name="companyName" required placeholder="Acme Construction" /> |
| </div> |
| <div> |
| <label for="penaltyDate">Penalty Date *</label> |
| <input type="date" id="penaltyDate" name="penaltyDate" required /> |
| </div> |
| <div> |
| <label for="penaltyAmount">Penalty Amount ($) *</label> |
| <input type="number" id="penaltyAmount" name="penaltyAmount" min="0" step="0.01" required placeholder="100.00" /> |
| </div> |
| <div class="full-width"> |
| <label for="penaltyReason">Penalty Reason *</label> |
| <textarea id="penaltyReason" name="penaltyReason" required placeholder="Describe the reason for penalty"></textarea> |
| </div> |
| <div class="full-width"> |
| <label for="penaltyDocument">Upload Document</label> |
| <input type="file" id="penaltyDocument" name="penaltyDocument" accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.txt" /> |
| <div id="fileNameDisplay" class="filename-display" aria-live="polite"></div> |
| </div> |
| <div class="full-width"> |
| <button type="submit">Add Penalty Record</button> |
| </div> |
| </form> |
| </div> |
|
|
| |
| <div id="tab-history" class="tab-content" style="display:none;"> |
| <div class="filters" aria-label="Filters for penalty records"> |
| <div> |
| <label for="filterContractor">Contractor Name</label> |
| <input type="text" id="filterContractor" placeholder="Filter by contractor" /> |
| </div> |
| <div> |
| <label for="filterCompany">Company Name</label> |
| <input type="text" id="filterCompany" placeholder="Filter by company" /> |
| </div> |
| <div> |
| <label for="filterDateFrom">Date From</label> |
| <input type="date" id="filterDateFrom" /> |
| </div> |
| <div> |
| <label for="filterDateTo">Date To</label> |
| <input type="date" id="filterDateTo" /> |
| </div> |
| <div> |
| <label for="filterAmountMin">Min Amount ($)</label> |
| <input type="number" id="filterAmountMin" min="0" step="0.01" placeholder="0.00" /> |
| </div> |
| <div> |
| <label for="filterAmountMax">Max Amount ($)</label> |
| <input type="number" id="filterAmountMax" min="0" step="0.01" placeholder="0.00" /> |
| </div> |
| </div> |
|
|
| <section class="penalty-list" id="penaltyListSection"> |
| <h2>Recorded Penalties</h2> |
| <table id="penaltyTable" aria-live="polite" aria-relevant="all"> |
| <thead> |
| <tr> |
| <th>Contractor</th> |
| <th>Company</th> |
| <th>Date</th> |
| <th>Amount ($)</th> |
| <th>Reason</th> |
| <th>Document</th> |
| <th>Actions</th> |
| </tr> |
| </thead> |
| <tbody id="penaltyTableBody"></tbody> |
| </table> |
| <div id="noDataMessage" class="no-data" style="display:none;">No penalty records to display.</div> |
| </section> |
| </div> |
| </div> |
|
|
| <script> |
| (function() { |
| const tabs = [...document.querySelectorAll('.tab')]; |
| const tabContents = { |
| add: document.getElementById('tab-add'), |
| history: document.getElementById('tab-history') |
| }; |
| |
| function switchTab(selectedTab) { |
| tabs.forEach(tab => { |
| const isActive = tab.dataset.tab === selectedTab; |
| tab.classList.toggle('active', isActive); |
| tabContents[tab.dataset.tab].style.display = isActive ? 'block' : 'none'; |
| }); |
| |
| if (selectedTab === 'history') { |
| renderPenalties(); |
| } |
| } |
| |
| tabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| switchTab(tab.dataset.tab); |
| }); |
| }); |
| |
| |
| const STORAGE_KEY = 'penaltyRecords'; |
| |
| |
| let penaltyRecords = JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; |
| |
| |
| const form = document.getElementById('penaltyForm'); |
| const fileInput = document.getElementById('penaltyDocument'); |
| const fileNameDisplay = document.getElementById('fileNameDisplay'); |
| |
| |
| const filterContractor = document.getElementById('filterContractor'); |
| const filterCompany = document.getElementById('filterCompany'); |
| const filterDateFrom = document.getElementById('filterDateFrom'); |
| const filterDateTo = document.getElementById('filterDateTo'); |
| const filterAmountMin = document.getElementById('filterAmountMin'); |
| const filterAmountMax = document.getElementById('filterAmountMax'); |
| |
| const penaltyTableBody = document.getElementById('penaltyTableBody'); |
| const noDataMessage = document.getElementById('noDataMessage'); |
| |
| |
| fileInput.addEventListener('change', () => { |
| if (fileInput.files.length > 0) { |
| fileNameDisplay.textContent = `Selected: ${fileInput.files[0].name}`; |
| } else { |
| fileNameDisplay.textContent = ''; |
| } |
| }); |
| |
| |
| function formatDate(dateStr) { |
| const d = new Date(dateStr); |
| if (isNaN(d)) return ''; |
| return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); |
| } |
| |
| |
| function saveRecords() { |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(penaltyRecords)); |
| } |
| |
| |
| function renderPenalties() { |
| penaltyTableBody.innerHTML = ''; |
| const filtered = filterRecords(penaltyRecords); |
| |
| if (filtered.length === 0) { |
| noDataMessage.style.display = 'block'; |
| return; |
| } |
| noDataMessage.style.display = 'none'; |
| |
| filtered.forEach((record, index) => { |
| const tr = document.createElement('tr'); |
| |
| const contractorTD = document.createElement('td'); |
| contractorTD.textContent = record.contractorName; |
| tr.appendChild(contractorTD); |
| |
| const companyTD = document.createElement('td'); |
| companyTD.textContent = record.companyName; |
| tr.appendChild(companyTD); |
| |
| const dateTD = document.createElement('td'); |
| dateTD.textContent = formatDate(record.penaltyDate); |
| tr.appendChild(dateTD); |
| |
| const amountTD = document.createElement('td'); |
| amountTD.textContent = Number(record.penaltyAmount).toFixed(2); |
| tr.appendChild(amountTD); |
| |
| const reasonTD = document.createElement('td'); |
| reasonTD.textContent = record.penaltyReason; |
| tr.appendChild(reasonTD); |
| |
| const docTD = document.createElement('td'); |
| if (record.documentName) { |
| |
| if(record.documentData){ |
| |
| const link = document.createElement('a'); |
| link.textContent = record.documentName; |
| link.href = record.documentData; |
| link.download = record.documentName; |
| link.style.color = '#1976d2'; |
| link.style.textDecoration = 'underline'; |
| docTD.appendChild(link); |
| } else { |
| |
| docTD.textContent = record.documentName; |
| docTD.title = "Document upload not available for past sessions."; |
| docTD.style.fontStyle = "italic"; |
| docTD.style.color = "#777"; |
| } |
| } else { |
| docTD.textContent = '-'; |
| docTD.style.color = "#777"; |
| docTD.style.fontStyle = "italic"; |
| } |
| tr.appendChild(docTD); |
| |
| const actionTD = document.createElement('td'); |
| const deleteBtn = document.createElement('button'); |
| deleteBtn.textContent = 'Delete'; |
| deleteBtn.className = 'btn-delete'; |
| deleteBtn.addEventListener('click', () => { |
| if (confirm(`Delete penalty record for "${record.contractorName}"?`)) { |
| |
| const originalIndex = penaltyRecords.findIndex(r => r.id === record.id); |
| if (originalIndex !== -1) { |
| penaltyRecords.splice(originalIndex, 1); |
| saveRecords(); |
| renderPenalties(); |
| } |
| } |
| }); |
| actionTD.appendChild(deleteBtn); |
| tr.appendChild(actionTD); |
| |
| penaltyTableBody.appendChild(tr); |
| }); |
| } |
| |
| |
| function generateId() { |
| return '_' + Math.random().toString(36).substr(2, 9) + Date.now(); |
| } |
| |
| |
| function filterRecords(records) { |
| const cFilter = filterContractor.value.trim().toLowerCase(); |
| const coFilter = filterCompany.value.trim().toLowerCase(); |
| const dFrom = filterDateFrom.value; |
| const dTo = filterDateTo.value; |
| const aMin = parseFloat(filterAmountMin.value); |
| const aMax = parseFloat(filterAmountMax.value); |
| |
| return records.filter(r => { |
| if (cFilter && !r.contractorName.toLowerCase().includes(cFilter)) return false; |
| if (coFilter && !r.companyName.toLowerCase().includes(coFilter)) return false; |
| if (dFrom && r.penaltyDate < dFrom) return false; |
| if (dTo && r.penaltyDate > dTo) return false; |
| if (!isNaN(aMin) && r.penaltyAmount < aMin) return false; |
| if (!isNaN(aMax) && r.penaltyAmount > aMax) return false; |
| return true; |
| }); |
| } |
| |
| |
| function clearFilters() { |
| filterContractor.value = ''; |
| filterCompany.value = ''; |
| filterDateFrom.value = ''; |
| filterDateTo.value = ''; |
| filterAmountMin.value = ''; |
| filterAmountMax.value = ''; |
| } |
| |
| |
| [filterContractor, filterCompany, filterDateFrom, filterDateTo, filterAmountMin, filterAmountMax].forEach(input => { |
| input.addEventListener('input', renderPenalties); |
| }); |
| |
| |
| form.addEventListener('submit', event => { |
| event.preventDefault(); |
| |
| const contractorName = form.contractorName.value.trim(); |
| const companyName = form.companyName.value.trim(); |
| const penaltyDate = form.penaltyDate.value; |
| const penaltyAmount = parseFloat(form.penaltyAmount.value); |
| const penaltyReason = form.penaltyReason.value.trim(); |
| |
| if (!contractorName || !companyName || !penaltyDate || isNaN(penaltyAmount) || !penaltyReason) { |
| alert('Please fill in all required fields.'); |
| return; |
| } |
| if (penaltyAmount < 0) { |
| alert('Penalty amount cannot be negative.'); |
| return; |
| } |
| |
| |
| const newRecord = { |
| id: generateId(), |
| contractorName, |
| companyName, |
| penaltyDate, |
| penaltyAmount, |
| penaltyReason, |
| documentName: fileInput.files.length > 0 ? fileInput.files[0].name : null, |
| |
| |
| documentData: null |
| }; |
| |
| if (fileInput.files.length > 0) { |
| |
| const file = fileInput.files[0]; |
| newRecord.documentData = URL.createObjectURL(file); |
| } |
| |
| |
| penaltyRecords.push(newRecord); |
| saveRecords(); |
| |
| |
| form.reset(); |
| fileNameDisplay.textContent = ''; |
| form.contractorName.focus(); |
| |
| |
| switchTab('history'); |
| }); |
| |
| |
| window.addEventListener('beforeunload', () => { |
| penaltyRecords.forEach(r => { |
| if (r.documentData) { |
| URL.revokeObjectURL(r.documentData); |
| } |
| }); |
| }); |
| |
| |
| switchTab('add'); |
| })(); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=alterzick/penalty" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |
|
|
|
|