| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>Comprehensive Permit to Work (PTW) Application</title> |
| <style> |
| |
| * { |
| box-sizing: border-box; |
| } |
| body { |
| margin: 0; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background: #f4f7fa; |
| color: #333; |
| display: flex; |
| justify-content: center; |
| padding: 20px; |
| } |
| #app { |
| max-width: 1100px; |
| width: 100%; |
| background: #fff; |
| padding: 30px 40px; |
| box-shadow: 0 0 20px rgb(0 0 0 / 0.1); |
| border-radius: 12px; |
| } |
| h1 { |
| margin-bottom: 10px; |
| color: #00467f; |
| } |
| h2 { |
| color: #006bb3; |
| margin-top: 40px; |
| margin-bottom: 15px; |
| border-bottom: 3px solid #0077cc; |
| padding-bottom: 5px; |
| } |
| |
| |
| .main-tabs { |
| display: flex; |
| margin-top: 15px; |
| border-bottom: 2px solid #d3d9e6; |
| gap: 20px; |
| user-select: none; |
| } |
| .main-tab { |
| padding: 12px 26px; |
| font-weight: 700; |
| cursor: pointer; |
| border-radius: 10px 10px 0 0; |
| background: #e9f0f6; |
| color: #0059b3; |
| box-shadow: inset 0 3px 7px rgb(0 71 127 / 0.1); |
| transition: background-color 0.3s ease, color 0.3s ease; |
| } |
| .main-tab:hover:not(.active) { |
| background-color: #c6d9f2; |
| } |
| .main-tab.active { |
| background-color: #0077cc; |
| color: white; |
| box-shadow: 0 6px 10px rgb(0 119 204 / 0.3); |
| border-bottom: 2px solid white; |
| } |
| |
| |
| .tabs { |
| display: flex; |
| margin-top: 15px; |
| border-bottom: 2px solid #d3d9e6; |
| gap: 20px; |
| user-select: none; |
| } |
| .tab { |
| padding: 10px 22px; |
| font-weight: 700; |
| cursor: pointer; |
| border-radius: 10px 10px 0 0; |
| background: #e9f0f6; |
| color: #0059b3; |
| box-shadow: inset 0 3px 7px rgb(0 71 127 / 0.1); |
| transition: background-color 0.3s ease, color 0.3s ease; |
| } |
| .tab:hover:not(.active) { |
| background-color: #c6d9f2; |
| } |
| .tab.active { |
| background-color: #0077cc; |
| color: white; |
| box-shadow: 0 6px 10px rgb(0 119 204 / 0.3); |
| border-bottom: 2px solid white; |
| } |
| |
| |
| form { |
| background: #e9f0f6; |
| padding: 20px; |
| border-radius: 8px; |
| box-shadow: inset 1px 1px 5px rgb(0 71 127 / 0.1); |
| } |
| form .row { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 15px 20px; |
| } |
| form label { |
| flex: 1 1 160px; |
| font-weight: 600; |
| margin-bottom: 5px; |
| color: #004d99; |
| } |
| form input, form textarea, form select { |
| flex: 2 1 280px; |
| padding: 8px 10px; |
| border: 1.5px solid #a9c4db; |
| border-radius: 6px; |
| font-size: 16px; |
| transition: border-color 0.3s ease; |
| } |
| form input:focus, form textarea:focus, form select:focus { |
| outline: none; |
| border-color: #0077cc; |
| } |
| form textarea { |
| resize: vertical; |
| min-height: 60px; |
| } |
| .form-group { |
| margin-bottom: 18px; |
| display: flex; |
| flex-wrap: wrap; |
| align-items: center; |
| } |
| |
| |
| .filter-container { |
| margin-top: 10px; |
| margin-bottom: 10px; |
| } |
| .filter-container label { |
| font-weight: 600; |
| color: #004d99; |
| margin-right: 10px; |
| } |
| .filter-container input { |
| padding: 6px 10px; |
| border-radius: 6px; |
| border: 1.5px solid #a9c4db; |
| font-size: 16px; |
| width: 220px; |
| max-width: 100%; |
| } |
| |
| |
| button { |
| background-color: #0077cc; |
| color: white; |
| border: none; |
| border-radius: 8px; |
| padding: 12px 24px; |
| font-size: 16px; |
| font-weight: 700; |
| cursor: pointer; |
| margin-top: 10px; |
| transition: background-color 0.3s ease; |
| } |
| button:disabled { |
| background-color: #7aaed9; |
| cursor: not-allowed; |
| } |
| button:hover:not(:disabled) { |
| background-color: #005fa3; |
| } |
| .btn-approve { |
| background-color: #308446; |
| } |
| .btn-approve:hover { |
| background-color: #257234; |
| } |
| .btn-reject { |
| background-color: #cc3b3b; |
| } |
| .btn-reject:hover { |
| background-color: #a92b2b; |
| } |
| .btn-print { |
| background-color: #0077cc; |
| } |
| .btn-print:hover { |
| background-color: #005fa3; |
| } |
| |
| |
| table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 15px; |
| } |
| thead { |
| background: #0077cc; |
| color: #fff; |
| } |
| thead th { |
| padding: 12px 15px; |
| text-align: left; |
| } |
| tbody tr { |
| border-bottom: 1.5px solid #d6e4f3; |
| transition: background-color 0.2s ease; |
| } |
| tbody tr:hover { |
| background-color: #f0f7ff; |
| cursor: pointer; |
| } |
| tbody td { |
| padding: 12px 15px; |
| vertical-align: middle; |
| } |
| .status-created { |
| color: #3066BE; |
| font-weight: 600; |
| } |
| .status-inprogress { |
| color: #F5AB35; |
| font-weight: 600; |
| } |
| .status-closed { |
| color: #308446; |
| font-weight: 600; |
| } |
| .status-pending { |
| color: #f5a623; |
| font-weight: 600; |
| } |
| .status-approved { |
| color: #2e8b57; |
| font-weight: 600; |
| } |
| .status-rejected { |
| color: #cc3b3b; |
| font-weight: 600; |
| } |
| .status-nonactive { |
| color: #888888; |
| font-weight: 600; |
| } |
| |
| |
| .modal { |
| display: none; |
| position: fixed; |
| z-index: 100; |
| left: 0; top: 0; right: 0; bottom: 0; |
| background-color: rgba(0,0,0,0.6); |
| justify-content: center; |
| align-items: center; |
| } |
| .modal.active { |
| display: flex; |
| } |
| .modal-content { |
| background: white; |
| max-width: 600px; |
| width: 90%; |
| border-radius: 12px; |
| padding: 25px 30px; |
| box-shadow: 0 8px 30px rgb(0 0 0 / 0.15); |
| max-height: 90vh; |
| overflow-y: auto; |
| position: relative; |
| } |
| .modal-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 15px; |
| } |
| .modal-header h3 { |
| margin: 0; |
| color: #00467f; |
| } |
| .modal-close { |
| font-size: 24px; |
| font-weight: 700; |
| cursor: pointer; |
| color: #777; |
| border: none; |
| background: none; |
| } |
| .modal-close:hover { |
| color: #00467f; |
| } |
| |
| |
| .printable-permit { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| padding: 20px; |
| border: 1px solid #ccc; |
| } |
| .printable-permit h2 { |
| text-align: center; |
| margin-bottom: 20px; |
| } |
| .printable-permit dl { |
| display: grid; |
| grid-template-columns: 1fr 2fr; |
| gap: 8px 20px; |
| } |
| .printable-permit dt { |
| font-weight: 700; |
| color: #00467f; |
| } |
| .printable-permit dd { |
| margin: 0 0 8px 0; |
| white-space: pre-line; |
| } |
| |
| |
| @media (max-width: 720px) { |
| form .row { |
| flex-direction: column; |
| } |
| form label, form input, form textarea, form select { |
| flex: 1 1 100%; |
| } |
| .form-group { |
| flex-direction: column; |
| align-items: flex-start; |
| } |
| tbody td { |
| padding: 10px 8px; |
| } |
| .printable-permit dl { |
| grid-template-columns: 1fr 1fr; |
| } |
| } |
| |
| |
| .sr-only { |
| position: absolute; |
| width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; |
| } |
| </style> |
| </head> |
| <body> |
| <div id="app"> |
| <h1>Permit to Work (PTW) Management System</h1> |
| <p>Manage work permits with creation, approval, monitoring, and closing for HSE compliance.</p> |
|
|
| <nav class="main-tabs" role="tablist" aria-label="Main application tabs"> |
| <div class="main-tab active" data-tab="create" role="tab" aria-selected="true" tabindex="0" aria-controls="createTabPanel" id="mainTabCreate">Create Permit</div> |
| <div class="main-tab" data-tab="monitor" role="tab" aria-selected="false" tabindex="-1" aria-controls="monitorTabPanel" id="mainTabMonitor">Monitor Permits</div> |
| <div class="main-tab" data-tab="approval" role="tab" aria-selected="false" tabindex="-1" aria-controls="approvalTabPanel" id="mainTabApproval">Approval</div> |
| </nav> |
|
|
| <section id="createTabPanel" role="tabpanel" tabindex="0" aria-labelledby="mainTabCreate"> |
| <h2>Create New Permit</h2> |
| <form id="createPermitForm" autocomplete="off"> |
| <div class="form-group row"> |
| <label for="workTitle">Work Title<span style="color:#cc0000">*</span></label> |
| <input type="text" id="workTitle" name="workTitle" required placeholder="Brief work title" /> |
| </div> |
|
|
| <div class="form-group row"> |
| <label for="workDescription">Work Description<span style="color:#cc0000">*</span></label> |
| <textarea id="workDescription" name="workDescription" required placeholder="Describe the job details"></textarea> |
| </div> |
|
|
| <div class="form-group row"> |
| <label for="location">Location<span style="color:#cc0000">*</span></label> |
| <input type="text" id="location" name="location" required placeholder="Work location" /> |
| </div> |
|
|
| <div class="form-group row"> |
| <label for="responsiblePerson">Responsible Person<span style="color:#cc0000">*</span></label> |
| <input type="text" id="responsiblePerson" name="responsiblePerson" required placeholder="Name of person responsible" /> |
| </div> |
|
|
| <div class="form-group row"> |
| <label for="startDate">Start Date & Time<span style="color:#cc0000">*</span></label> |
| <input type="datetime-local" id="startDate" name="startDate" required /> |
| </div> |
|
|
| <div class="form-group row"> |
| <label for="endDate">Expected End Date & Time<span style="color:#cc0000">*</span></label> |
| <input type="datetime-local" id="endDate" name="endDate" required /> |
| </div> |
|
|
| <div class="form-group row"> |
| <label for="safetyChecks">Safety Checks (comma separated)</label> |
| <input type="text" id="safetyChecks" name="safetyChecks" placeholder="e.g. PPE check, Lockout tagout" /> |
| </div> |
|
|
| <button type="submit">Create Permit</button> |
| </form> |
| </section> |
|
|
| <section id="monitorTabPanel" role="tabpanel" tabindex="0" aria-labelledby="mainTabMonitor" hidden> |
| <h2>Monitor Permits</h2> |
|
|
| <div class="filter-container"> |
| <label for="monitorFilter">Filter (search in ID, Title, Responsible Person, Location): </label> |
| <input type="text" id="monitorFilter" placeholder="Type to filter..." aria-label="Filter permits for monitoring" /> |
| </div> |
|
|
| <nav class="tabs" role="tablist" aria-label="Permit Status Filter Tabs"> |
| <div class="tab active" data-view="active" role="tab" aria-selected="true" tabindex="0" aria-controls="permitsTable" id="tabActive">Active</div> |
| <div class="tab" data-view="nonactive" role="tab" aria-selected="false" tabindex="-1" aria-controls="permitsTable" id="tabNonActive">Non-Active</div> |
| <div class="tab" data-view="complete" role="tab" aria-selected="false" tabindex="-1" aria-controls="permitsTable" id="tabComplete">Complete</div> |
| </nav> |
|
|
| <table id="permitsTable" aria-live="polite" aria-describedby="permitsDesc" aria-label="List of permits"> |
| <thead> |
| <tr> |
| <th>Permit ID</th> |
| <th>Work Title</th> |
| <th>Responsible Person</th> |
| <th>Location</th> |
| <th>Start Date</th> |
| <th>End Date</th> |
| <th>Status</th> |
| </tr> |
| </thead> |
| <tbody> |
| |
| </tbody> |
| </table> |
|
|
| <div id="permitsDesc" class="sr-only">This table shows permits filtered by the selected status tab and filter above.</div> |
| </section> |
|
|
| <section id="approvalTabPanel" role="tabpanel" tabindex="0" aria-labelledby="mainTabApproval" hidden> |
| <h2>Approval Management</h2> |
|
|
| <div class="filter-container"> |
| <label for="approvalFilter">Filter (search in ID, Title, Responsible Person, Location): </label> |
| <input type="text" id="approvalFilter" placeholder="Type to filter..." aria-label="Filter permits for approval" /> |
| </div> |
|
|
| <nav class="tabs" role="tablist" aria-label="Approval Status Filter Tabs"> |
| <div class="tab active" data-view="pending" role="tab" aria-selected="true" tabindex="0" aria-controls="approvalTable" id="tabPending">Pending Approval</div> |
| <div class="tab" data-view="approved" role="tab" aria-selected="false" tabindex="-1" aria-controls="approvalTable" id="tabApproved">Approved</div> |
| <div class="tab" data-view="rejected" role="tab" aria-selected="false" tabindex="-1" aria-controls="approvalTable" id="tabRejected">Rejected</div> |
| </nav> |
|
|
| <table id="approvalTable" aria-live="polite" aria-describedby="approvalDesc" aria-label="List of permits for approval"> |
| <thead> |
| <tr> |
| <th>Permit ID</th> |
| <th>Work Title</th> |
| <th>Responsible Person</th> |
| <th>Location</th> |
| <th>Start Date</th> |
| <th>End Date</th> |
| <th>Status</th> |
| <th>Actions</th> |
| </tr> |
| </thead> |
| <tbody> |
| |
| </tbody> |
| </table> |
|
|
| <div id="approvalDesc" class="sr-only">This table shows permits filtered by the selected approval status tab and filter above.</div> |
| </section> |
|
|
| |
| <div id="permitModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc"> |
| <div class="modal-content" tabindex="0"> |
| <div class="modal-header"> |
| <h3 id="modalTitle">Permit Details</h3> |
| <button class="modal-close" id="modalCloseBtn" aria-label="Close modal">×</button> |
| </div> |
| <div id="modalDesc" tabindex="0"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="printModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="printModalTitle" aria-describedby="printModalDesc"> |
| <div class="modal-content" tabindex="0"> |
| <div class="modal-header"> |
| <h3 id="printModalTitle">Printable Permit</h3> |
| <button class="modal-close" id="printModalCloseBtn" aria-label="Close print modal">×</button> |
| </div> |
| <div id="printModalDesc" tabindex="0" class="printable-permit"> |
| |
| </div> |
| <div style="margin-top: 20px; text-align: center;"> |
| <button type="button" id="printBtn" class="btn-print">Print Permit</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| (() => { |
| const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; |
| |
| |
| function formatDateTimeLocal(date) { |
| if (!date) return ''; |
| const d = new Date(date); |
| return d.toLocaleString([], { year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit' }); |
| } |
| function generatePermitID() { |
| return 'PTW-' + Math.random().toString(36).substr(2, 6).toUpperCase(); |
| } |
| function escapeHtml(unsafe) { |
| return unsafe.replace(/[&<"'>]/g, function(m) { |
| switch(m) { |
| case '&': return '&'; |
| case '<': return '<'; |
| case '>': return '>'; |
| case '"': return '"'; |
| case "'": return '''; |
| default: return m; |
| } |
| }); |
| } |
| function daysBetween(date1, date2) { |
| return Math.floor((date2 - date1) / (1000 * 60 * 60 * 24)); |
| } |
| |
| |
| let permits = JSON.parse(localStorage.getItem('permits') || '[]'); |
| let currentEditingPermitId = null; |
| let currentView = 'active'; |
| let currentApprovalView = 'pending'; |
| |
| |
| const createForm = document.getElementById('createPermitForm'); |
| const permitsTableBody = document.querySelector('#permitsTable tbody'); |
| const approvalTableBody = document.querySelector('#approvalTable tbody'); |
| |
| |
| const mainTabs = Array.from(document.querySelectorAll('.main-tab')); |
| const createTabPanel = document.getElementById('createTabPanel'); |
| const monitorTabPanel = document.getElementById('monitorTabPanel'); |
| const approvalTabPanel = document.getElementById('approvalTabPanel'); |
| |
| |
| const statusTabs = Array.from(monitorTabPanel.querySelectorAll('.tab')); |
| const monitorFilterInput = document.getElementById('monitorFilter'); |
| |
| |
| const approvalStatusTabs = Array.from(approvalTabPanel.querySelectorAll('.tab')); |
| const approvalFilterInput = document.getElementById('approvalFilter'); |
| |
| const permitModal = document.getElementById('permitModal'); |
| const modalCloseBtn = document.getElementById('modalCloseBtn'); |
| const modalDesc = document.getElementById('modalDesc'); |
| const modalTitle = document.getElementById('modalTitle'); |
| |
| const printModal = document.getElementById('printModal'); |
| const printModalCloseBtn = document.getElementById('printModalCloseBtn'); |
| const printModalDesc = document.getElementById('printModalDesc'); |
| const printBtn = document.getElementById('printBtn'); |
| |
| |
| function savePermits() { |
| localStorage.setItem('permits', JSON.stringify(permits)); |
| } |
| |
| |
| |
| |
| |
| |
| |
| function filterPermitsByView() { |
| let filtered = []; |
| const now = new Date(); |
| |
| switch(currentView) { |
| case 'active': |
| |
| filtered = permits.filter(p => p.status === 'Approved' || p.status === 'In Progress'); |
| break; |
| case 'nonactive': |
| |
| filtered = permits.filter(p => { |
| if(p.status !== 'Pending Approval') return false; |
| const createdDate = new Date(p.createdDate || p.submittedDate || p.startDate || now); |
| return (now - createdDate) > SEVEN_DAYS_MS; |
| }); |
| break; |
| case 'complete': |
| |
| filtered = permits.filter(p => p.status === 'Closed'); |
| break; |
| default: |
| filtered = permits; |
| } |
| const filterText = monitorFilterInput.value.trim().toLowerCase(); |
| if(filterText.length === 0) return filtered; |
| |
| return filtered.filter(p => |
| p.id.toLowerCase().includes(filterText) || |
| p.workTitle.toLowerCase().includes(filterText) || |
| p.responsiblePerson.toLowerCase().includes(filterText) || |
| p.location.toLowerCase().includes(filterText) |
| ); |
| } |
| |
| |
| function filterPermitsByApprovalView() { |
| let filtered = []; |
| switch(currentApprovalView) { |
| case 'pending': |
| filtered = permits.filter(p => p.status === 'Pending Approval'); |
| break; |
| case 'approved': |
| filtered = permits.filter(p => p.status === 'Approved' || p.status === 'In Progress'); |
| break; |
| case 'rejected': |
| filtered = permits.filter(p => p.status === 'Rejected'); |
| break; |
| default: |
| filtered = []; |
| } |
| const filterText = approvalFilterInput.value.trim().toLowerCase(); |
| if(filterText.length === 0) return filtered; |
| |
| return filtered.filter(p => |
| p.id.toLowerCase().includes(filterText) || |
| p.workTitle.toLowerCase().includes(filterText) || |
| p.responsiblePerson.toLowerCase().includes(filterText) || |
| p.location.toLowerCase().includes(filterText) |
| ); |
| } |
| |
| |
| function renderPermits() { |
| permitsTableBody.innerHTML = ''; |
| let filteredPermits = filterPermitsByView(); |
| |
| if (filteredPermits.length === 0) { |
| const tr = document.createElement('tr'); |
| const td = document.createElement('td'); |
| td.colSpan = 7; |
| td.style.textAlign = 'center'; |
| td.style.color = '#777'; |
| td.textContent = { |
| active: 'No active permits found.', |
| nonactive: 'No non-active permits found.', |
| complete: 'No completed permits found.' |
| }[currentView] || 'No permits found.'; |
| tr.appendChild(td); |
| permitsTableBody.appendChild(tr); |
| return; |
| } |
| filteredPermits.forEach(p => { |
| const tr = document.createElement('tr'); |
| tr.tabIndex = 0; |
| tr.setAttribute('data-id', p.id); |
| |
| let td = document.createElement('td'); |
| td.textContent = p.id; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = p.workTitle; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = p.responsiblePerson; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = p.location; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = formatDateTimeLocal(p.startDate); |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = formatDateTimeLocal(p.endDate); |
| tr.appendChild(td); |
| |
| |
| let statusClass = 'status-' + p.status.toLowerCase().replace(/\s/g, ''); |
| |
| if(currentView === 'nonactive') statusClass = 'status-nonactive'; |
| td = document.createElement('td'); |
| td.textContent = p.status; |
| td.classList.add(statusClass); |
| tr.appendChild(td); |
| |
| tr.addEventListener('click', () => openPermitModal(p.id)); |
| tr.addEventListener('keypress', e => { |
| if(e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| openPermitModal(p.id); |
| } |
| }); |
| |
| permitsTableBody.appendChild(tr); |
| }); |
| } |
| |
| |
| function renderApprovalPermits() { |
| approvalTableBody.innerHTML = ''; |
| let filteredPermits = filterPermitsByApprovalView(); |
| |
| if (filteredPermits.length === 0) { |
| const tr = document.createElement('tr'); |
| const td = document.createElement('td'); |
| td.colSpan = 8; |
| td.style.textAlign = 'center'; |
| td.style.color = '#777'; |
| td.textContent = { |
| pending: 'No permits pending approval.', |
| approved: 'No approved permits found.', |
| rejected: 'No rejected permits found.' |
| }[currentApprovalView] || 'No permits found.'; |
| tr.appendChild(td); |
| approvalTableBody.appendChild(tr); |
| return; |
| } |
| |
| filteredPermits.forEach(p => { |
| const tr = document.createElement('tr'); |
| tr.tabIndex = 0; |
| tr.setAttribute('data-id', p.id); |
| |
| let td = document.createElement('td'); |
| td.textContent = p.id; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = p.workTitle; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = p.responsiblePerson; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = p.location; |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = formatDateTimeLocal(p.startDate); |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = formatDateTimeLocal(p.endDate); |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| td.textContent = p.status; |
| td.classList.add('status-'+p.status.toLowerCase().replace(/\s/g, '')); |
| tr.appendChild(td); |
| |
| td = document.createElement('td'); |
| if(p.status === 'Pending Approval') { |
| const btnApprove = document.createElement('button'); |
| btnApprove.textContent = 'Approve'; |
| btnApprove.className = 'btn-approve'; |
| btnApprove.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| approvePermit(p.id); |
| }); |
| td.appendChild(btnApprove); |
| |
| const btnReject = document.createElement('button'); |
| btnReject.textContent = 'Reject'; |
| btnReject.className = 'btn-reject'; |
| btnReject.style.marginLeft = '10px'; |
| btnReject.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| rejectPermit(p.id); |
| }); |
| td.appendChild(btnReject); |
| } else if(p.status === 'Approved' || p.status === 'In Progress') { |
| const btnPrint = document.createElement('button'); |
| btnPrint.textContent = 'Print'; |
| btnPrint.className = 'btn-print'; |
| btnPrint.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| openPrintModal(p.id); |
| }); |
| td.appendChild(btnPrint); |
| } else { |
| td.textContent = '-'; |
| } |
| tr.appendChild(td); |
| |
| tr.addEventListener('click', () => openPermitModal(p.id)); |
| tr.addEventListener('keypress', e => { |
| if(e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| openPermitModal(p.id); |
| } |
| }); |
| |
| approvalTableBody.appendChild(tr); |
| }); |
| } |
| |
| |
| function openPermitModal(permitId) { |
| currentEditingPermitId = permitId; |
| const permit = permits.find(p => p.id === permitId); |
| if (!permit) return; |
| |
| modalTitle.textContent = `Permit Details - ${permit.id}`; |
| modalDesc.innerHTML = ''; |
| |
| |
| const infoList = document.createElement('dl'); |
| infoList.style.marginBottom = '20px'; |
| |
| function addInfoItem(label, value) { |
| const dt = document.createElement('dt'); |
| dt.textContent = label; |
| dt.style.fontWeight = '600'; |
| dt.style.marginTop = '8px'; |
| const dd = document.createElement('dd'); |
| dd.textContent = value; |
| dd.style.marginLeft = '0'; |
| dd.style.marginBottom = '8px'; |
| infoList.appendChild(dt); |
| infoList.appendChild(dd); |
| } |
| |
| addInfoItem('Work Title:', permit.workTitle); |
| addInfoItem('Work Description:', permit.workDescription); |
| addInfoItem('Location:', permit.location); |
| addInfoItem('Responsible Person:', permit.responsiblePerson); |
| addInfoItem('Start Date & Time:', formatDateTimeLocal(permit.startDate)); |
| addInfoItem('Expected End Date & Time:', formatDateTimeLocal(permit.endDate)); |
| addInfoItem('Status:', permit.status); |
| addInfoItem('Approval Status:', ['Pending Approval', 'Approved', 'Rejected'].includes(permit.status) ? permit.status : 'Not submitted for approval'); |
| addInfoItem('Safety Checks:', permit.safetyChecks.length > 0 ? permit.safetyChecks.join(', ') : 'None'); |
| addInfoItem('Comments:', permit.comments.length > 0 ? permit.comments.join('; ') : 'None'); |
| |
| modalDesc.appendChild(infoList); |
| |
| |
| if (permit.status !== 'Closed' && permit.status !== 'Rejected') { |
| const formUpdate = document.createElement('form'); |
| formUpdate.id = 'monitorForm'; |
| |
| |
| if(permit.safetyChecks.length>0){ |
| const checksLabel = document.createElement('label'); |
| checksLabel.textContent = 'Mark Safety Checks Completed:'; |
| checksLabel.style.fontWeight = '600'; |
| formUpdate.appendChild(checksLabel); |
| |
| permit.safetyChecks.forEach((check,idx)=>{ |
| const div = document.createElement('div'); |
| div.style.marginBottom = '6px'; |
| |
| const input = document.createElement('input'); |
| input.type = 'checkbox'; |
| input.id = 'check_'+idx; |
| input.name = 'safetyCheck'; |
| input.value = check; |
| input.checked = permit.completedSafetyChecks.includes(check); |
| |
| const label = document.createElement('label'); |
| label.htmlFor = 'check_'+idx; |
| label.textContent = ' ' + check; |
| |
| div.appendChild(input); |
| div.appendChild(label); |
| formUpdate.appendChild(div); |
| }); |
| } |
| |
| |
| const commentLabel = document.createElement('label'); |
| commentLabel.htmlFor = 'newComment'; |
| commentLabel.textContent = 'Add Monitoring Comment:'; |
| commentLabel.style.fontWeight = '600'; |
| commentLabel.style.marginTop = '12px'; |
| formUpdate.appendChild(commentLabel); |
| |
| const commentInput = document.createElement('textarea'); |
| commentInput.id = 'newComment'; |
| commentInput.placeholder = 'Add notes or observations here...'; |
| commentInput.rows = 3; |
| formUpdate.appendChild(commentInput); |
| |
| |
| const btnContainer = document.createElement('div'); |
| btnContainer.style.marginTop = '18px'; |
| btnContainer.style.display = 'flex'; |
| btnContainer.style.gap = '12px'; |
| |
| |
| if (permit.status === 'Created') { |
| const submitApprovalBtn = document.createElement('button'); |
| submitApprovalBtn.type = 'button'; |
| submitApprovalBtn.textContent = 'Submit for Approval'; |
| submitApprovalBtn.style.backgroundColor = '#0066cc'; |
| submitApprovalBtn.style.color = 'white'; |
| submitApprovalBtn.addEventListener('click', () => submitForApproval(permit.id)); |
| btnContainer.appendChild(submitApprovalBtn); |
| } |
| |
| if (permit.status === 'Approved' || permit.status === 'In Progress') { |
| const closeBtn = document.createElement('button'); |
| closeBtn.type = 'button'; |
| closeBtn.textContent = 'Close Permit'; |
| closeBtn.style.backgroundColor = '#308446'; |
| closeBtn.style.color = 'white'; |
| closeBtn.addEventListener('click', () => updatePermitStatus('Closed')); |
| btnContainer.appendChild(closeBtn); |
| } |
| |
| const saveBtn = document.createElement('button'); |
| saveBtn.type = 'submit'; |
| saveBtn.textContent = 'Save Monitoring Updates'; |
| btnContainer.appendChild(saveBtn); |
| |
| formUpdate.appendChild(btnContainer); |
| |
| formUpdate.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| saveMonitoringData(); |
| }); |
| |
| modalDesc.appendChild(formUpdate); |
| } else if (permit.status === 'Rejected') { |
| const rejectedNote = document.createElement('p'); |
| rejectedNote.textContent = 'This permit has been rejected and cannot be updated.'; |
| rejectedNote.style.fontStyle = 'italic'; |
| rejectedNote.style.color = '#cc3b3b'; |
| modalDesc.appendChild(rejectedNote); |
| } else { |
| const closedNote = document.createElement('p'); |
| closedNote.textContent = 'This permit is closed and can no longer be updated.'; |
| closedNote.style.fontStyle = 'italic'; |
| closedNote.style.color = '#308446'; |
| modalDesc.appendChild(closedNote); |
| } |
| |
| permitModal.classList.add('active'); |
| modalDesc.focus(); |
| } |
| |
| |
| function saveMonitoringData() { |
| const permit = permits.find(p => p.id === currentEditingPermitId); |
| if (!permit) return; |
| |
| |
| const checkboxes = permitModal.querySelectorAll('input[name="safetyCheck"]'); |
| permit.completedSafetyChecks = []; |
| checkboxes.forEach(cb => { |
| if(cb.checked){ |
| permit.completedSafetyChecks.push(cb.value); |
| } |
| }); |
| |
| |
| const newComment = permitModal.querySelector('#newComment').value.trim(); |
| if(newComment.length > 0){ |
| permit.comments.push(newComment); |
| permitModal.querySelector('#newComment').value = ''; |
| } |
| |
| savePermits(); |
| renderPermits(); |
| renderApprovalPermits(); |
| |
| |
| openPermitModal(permit.id); |
| } |
| |
| |
| function updatePermitStatus(newStatus) { |
| const permit = permits.find(p => p.id === currentEditingPermitId); |
| if (!permit) return; |
| |
| permit.status = newStatus; |
| if(newStatus === 'In Progress'){ |
| permit.monitoringStart = new Date().toISOString(); |
| } |
| if(newStatus === 'Approved'){ |
| permit.approvedDate = new Date().toISOString(); |
| |
| permit.status = 'Approved'; |
| } |
| if(newStatus === 'Closed'){ |
| permit.closedAt = new Date().toISOString(); |
| } |
| |
| savePermits(); |
| renderPermits(); |
| renderApprovalPermits(); |
| openPermitModal(permit.id); |
| } |
| |
| |
| function submitForApproval(id) { |
| const permit = permits.find(p => p.id === id); |
| if (!permit) return; |
| if (permit.status !== 'Created') { |
| alert('Only permits in "Created" status can be submitted for approval.'); |
| return; |
| } |
| permit.status = 'Pending Approval'; |
| permit.submittedDate = new Date().toISOString(); |
| savePermits(); |
| alert('Permit submitted for leader approval.'); |
| renderPermits(); |
| renderApprovalPermits(); |
| activateMainTab('approval'); |
| activateApprovalStatusTab('pending'); |
| } |
| |
| |
| function approvePermit(id) { |
| const permit = permits.find(p => p.id === id); |
| if (!permit) return; |
| if (permit.status !== 'Pending Approval') { |
| alert('Only permits pending approval can be approved.'); |
| return; |
| } |
| permit.status = 'Approved'; |
| permit.approvedDate = new Date().toISOString(); |
| savePermits(); |
| alert('Permit approved.'); |
| renderApprovalPermits(); |
| renderPermits(); |
| } |
| |
| |
| function rejectPermit(id) { |
| const permit = permits.find(p => p.id === id); |
| if (!permit) return; |
| if (permit.status !== 'Pending Approval') { |
| alert('Only permits pending approval can be rejected.'); |
| return; |
| } |
| permit.status = 'Rejected'; |
| savePermits(); |
| alert('Permit rejected.'); |
| renderApprovalPermits(); |
| renderPermits(); |
| } |
| |
| |
| function openPrintModal(id) { |
| const permit = permits.find(p => p.id === id); |
| if (!permit) return; |
| |
| let content = ` |
| <h2>Permit to Work (PTW)</h2> |
| <dl> |
| <dt>Permit ID:</dt><dd>${escapeHtml(permit.id)}</dd> |
| <dt>Work Title:</dt><dd>${escapeHtml(permit.workTitle)}</dd> |
| <dt>Work Description:</dt><dd>${escapeHtml(permit.workDescription)}</dd> |
| <dt>Location:</dt><dd>${escapeHtml(permit.location)}</dd> |
| <dt>Responsible Person:</dt><dd>${escapeHtml(permit.responsiblePerson)}</dd> |
| <dt>Start Date & Time:</dt><dd>${formatDateTimeLocal(permit.startDate)}</dd> |
| <dt>Expected End Date & Time:</dt><dd>${formatDateTimeLocal(permit.endDate)}</dd> |
| <dt>Status:</dt><dd>${escapeHtml(permit.status)}</dd> |
| <dt>Safety Checks:</dt><dd>${permit.safetyChecks.length > 0 ? escapeHtml(permit.safetyChecks.join(', ')) : 'None'}</dd> |
| <dt>Comments:</dt><dd>${permit.comments.length > 0 ? escapeHtml(permit.comments.join('\n')) : 'None'}</dd> |
| </dl> |
| `; |
| printModalDesc.innerHTML = content; |
| printModal.classList.add('active'); |
| printModalDesc.focus(); |
| } |
| |
| |
| function closeModal() { |
| permitModal.classList.remove('active'); |
| currentEditingPermitId = null; |
| } |
| function closePrintModal() { |
| printModal.classList.remove('active'); |
| } |
| |
| |
| function printPermit() { |
| const printWindow = window.open('', '', 'width=800,height=600'); |
| if (!printWindow) { |
| alert('Pop-up blocked. Please allow pop-ups for this website to print.'); |
| return; |
| } |
| printWindow.document.write(` |
| <html> |
| <head> |
| <title>Print Permit</title> |
| <style> |
| body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 20px; } |
| h2 { text-align: center; margin-bottom: 20px; } |
| dl { display: grid; grid-template-columns: 1fr 2fr; gap: 8px 20px; } |
| dt { font-weight: 700; color: #00467f; } |
| dd { margin: 0 0 8px 0; white-space: pre-line; } |
| </style> |
| </head> |
| <body> |
| ${printModalDesc.innerHTML} |
| <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/ptw" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |
| `); |
| printWindow.document.close(); |
| printWindow.focus(); |
| printWindow.print(); |
| printWindow.close(); |
| } |
| |
| |
| createForm.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| |
| const start = createForm.startDate.value; |
| const end = createForm.endDate.value; |
| if(start >= end){ |
| alert('Start date/time must be before end date/time.'); |
| return; |
| } |
| |
| const newPermit = { |
| id: generatePermitID(), |
| workTitle: createForm.workTitle.value.trim(), |
| workDescription: createForm.workDescription.value.trim(), |
| location: createForm.location.value.trim(), |
| responsiblePerson: createForm.responsiblePerson.value.trim(), |
| startDate: start, |
| endDate: end, |
| safetyChecks: createForm.safetyChecks.value.trim() ? createForm.safetyChecks.value.split(',').map(s => s.trim()).filter(s => s.length > 0) : [], |
| completedSafetyChecks: [], |
| comments: [], |
| status: 'Pending Approval', |
| monitoringStart: null, |
| closedAt: null, |
| submittedDate: new Date().toISOString() |
| }; |
| |
| permits.unshift(newPermit); |
| savePermits(); |
| renderPermits(); |
| renderApprovalPermits(); |
| |
| activateMainTab('approval'); |
| activateApprovalStatusTab('pending'); |
| |
| createForm.reset(); |
| }); |
| |
| |
| monitorFilterInput.addEventListener('input', () => { |
| renderPermits(); |
| }); |
| |
| approvalFilterInput.addEventListener('input', () => { |
| renderApprovalPermits(); |
| }); |
| |
| modalCloseBtn.addEventListener('click', closeModal); |
| permitModal.addEventListener('click', (e) => { |
| if(e.target === permitModal){ |
| closeModal(); |
| } |
| }); |
| |
| printModalCloseBtn.addEventListener('click', closePrintModal); |
| printModal.addEventListener('click', (e) => { |
| if(e.target === printModal){ |
| closePrintModal(); |
| } |
| }); |
| printBtn.addEventListener('click', printPermit); |
| |
| |
| mainTabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| if(tab.classList.contains('active')) return; |
| deactivateAllMainTabs(); |
| activateMainTab(tab.getAttribute('data-tab')); |
| }); |
| tab.addEventListener('keypress', e => { |
| if(e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| tab.click(); |
| } |
| }); |
| }); |
| |
| function deactivateAllMainTabs() { |
| mainTabs.forEach(tab => { |
| tab.classList.remove('active'); |
| tab.setAttribute('aria-selected', 'false'); |
| tab.tabIndex = -1; |
| }); |
| createTabPanel.hidden = true; |
| monitorTabPanel.hidden = true; |
| approvalTabPanel.hidden = true; |
| } |
| function activateMainTab(tabName) { |
| deactivateAllMainTabs(); |
| const tab = mainTabs.find(t => t.getAttribute('data-tab') === tabName); |
| if(!tab) return; |
| tab.classList.add('active'); |
| tab.setAttribute('aria-selected', 'true'); |
| tab.tabIndex = 0; |
| if(tabName === 'create') { |
| createTabPanel.hidden = false; |
| createTabPanel.focus(); |
| } else if(tabName === 'monitor') { |
| monitorTabPanel.hidden = false; |
| monitorTabPanel.focus(); |
| } else if(tabName === 'approval') { |
| approvalTabPanel.hidden = false; |
| approvalTabPanel.focus(); |
| } |
| } |
| |
| |
| statusTabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| if(tab.classList.contains('active')) return; |
| deactivateAllStatusTabs(); |
| activateStatusTab(tab.getAttribute('data-view')); |
| }); |
| tab.addEventListener('keypress', e => { |
| if(e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| tab.click(); |
| } |
| }); |
| }); |
| |
| function deactivateAllStatusTabs() { |
| statusTabs.forEach(tab => { |
| tab.classList.remove('active'); |
| tab.setAttribute('aria-selected', 'false'); |
| tab.tabIndex = -1; |
| }); |
| } |
| function activateStatusTab(viewName) { |
| deactivateAllStatusTabs(); |
| const tab = statusTabs.find(t => t.getAttribute('data-view') === viewName); |
| if(!tab) return; |
| tab.classList.add('active'); |
| tab.setAttribute('aria-selected', 'true'); |
| tab.tabIndex = 0; |
| currentView = viewName; |
| renderPermits(); |
| } |
| |
| |
| approvalStatusTabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| if(tab.classList.contains('active')) return; |
| deactivateAllApprovalStatusTabs(); |
| activateApprovalStatusTab(tab.getAttribute('data-view')); |
| }); |
| tab.addEventListener('keypress', e => { |
| if(e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| tab.click(); |
| } |
| }); |
| }); |
| |
| function deactivateAllApprovalStatusTabs() { |
| approvalStatusTabs.forEach(tab => { |
| tab.classList.remove('active'); |
| tab.setAttribute('aria-selected', 'false'); |
| tab.tabIndex = -1; |
| }); |
| } |
| function activateApprovalStatusTab(viewName) { |
| deactivateAllApprovalStatusTabs(); |
| const tab = approvalStatusTabs.find(t => t.getAttribute('data-view') === viewName); |
| if(!tab) return; |
| tab.classList.add('active'); |
| tab.setAttribute('aria-selected', 'true'); |
| tab.tabIndex = 0; |
| currentApprovalView = viewName; |
| renderApprovalPermits(); |
| } |
| |
| |
| activateMainTab('create'); |
| activateStatusTab('active'); |
| activateApprovalStatusTab('pending'); |
| |
| renderPermits(); |
| renderApprovalPermits(); |
| })(); |
| </script> |
| </body> |
| </html> |
|
|
|
|