ptw / index.html
alterzick's picture
Add 3 files
f5351ad verified
<!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>
/* Reset and base */
* {
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;
}
/* Top-Level Tabs */
.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 inside Monitor and Approval tab */
.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 styles */
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 input */
.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%;
}
/* Buttons */
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 */
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 */
.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 */
.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;
}
/* Responsive */
@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;
}
}
/* Screen reader only helper */
.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>
<!-- Dynamically populated -->
</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>
<!-- Dynamically populated -->
</tbody>
</table>
<div id="approvalDesc" class="sr-only">This table shows permits filtered by the selected approval status tab and filter above.</div>
</section>
<!-- Modal for viewing and updating permit -->
<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">&times;</button>
</div>
<div id="modalDesc" tabindex="0">
<!-- Permit details and monitoring form -->
</div>
</div>
</div>
<!-- Modal for printing approved permit -->
<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">&times;</button>
</div>
<div id="printModalDesc" tabindex="0" class="printable-permit">
<!-- Printable content -->
</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;
// Utilities
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 '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
case "'": return '&#039;';
default: return m;
}
});
}
function daysBetween(date1, date2) {
return Math.floor((date2 - date1) / (1000 * 60 * 60 * 24));
}
// State
let permits = JSON.parse(localStorage.getItem('permits') || '[]');
let currentEditingPermitId = null;
let currentView = 'active'; // monitor tab status view
let currentApprovalView = 'pending'; // approval tab status view
// DOM Elements
const createForm = document.getElementById('createPermitForm');
const permitsTableBody = document.querySelector('#permitsTable tbody');
const approvalTableBody = document.querySelector('#approvalTable tbody');
// Main tabs
const mainTabs = Array.from(document.querySelectorAll('.main-tab'));
const createTabPanel = document.getElementById('createTabPanel');
const monitorTabPanel = document.getElementById('monitorTabPanel');
const approvalTabPanel = document.getElementById('approvalTabPanel');
// Inner status tabs in monitor tab
const statusTabs = Array.from(monitorTabPanel.querySelectorAll('.tab'));
const monitorFilterInput = document.getElementById('monitorFilter');
// Inner status tabs in approval tab
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');
// Save permits to localStorage
function savePermits() {
localStorage.setItem('permits', JSON.stringify(permits));
}
// Updated status categories according to new flow:
// "Pending Approval" on creation -> after approval = Active (In Progress)
// Non-Active = permits pending longer than 7 days (status Pending Approval + createdDate older than 7 days)
// Complete = Closed
// Filtering permits by current monitor view and filter input
function filterPermitsByView() {
let filtered = [];
const now = new Date();
switch(currentView) {
case 'active':
// Active = Approved permits (status 'Approved') considered active now
filtered = permits.filter(p => p.status === 'Approved' || p.status === 'In Progress');
break;
case 'nonactive':
// Non-Active = Pending Approval permits older than 7 days
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':
// Complete = Closed
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)
);
}
// Filtering permits by current approval view and filter input
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)
);
}
// Render monitoring table rows
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);
// Map new statuses to style classes
let statusClass = 'status-' + p.status.toLowerCase().replace(/\s/g, '');
// Non-Active (custom style)
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);
});
}
// Render approval tab table rows
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);
});
}
// Open permit modal with details
function openPermitModal(permitId) {
currentEditingPermitId = permitId;
const permit = permits.find(p => p.id === permitId);
if (!permit) return;
modalTitle.textContent = `Permit Details - ${permit.id}`;
modalDesc.innerHTML = '';
// Permit basic info display
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);
// Monitoring and update form in modal (only if permit not closed or rejected)
if (permit.status !== 'Closed' && permit.status !== 'Rejected') {
const formUpdate = document.createElement('form');
formUpdate.id = 'monitorForm';
// Safety Checks Completed (checkboxes)
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);
});
}
// Add comment
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);
// Status update buttons
const btnContainer = document.createElement('div');
btnContainer.style.marginTop = '18px';
btnContainer.style.display = 'flex';
btnContainer.style.gap = '12px';
// Depending on status, show possible actions
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);
}
/* Approved permits are Active now, so allow close and monitoring */
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();
}
// Save monitoring data from modal form
function saveMonitoringData() {
const permit = permits.find(p => p.id === currentEditingPermitId);
if (!permit) return;
// Update completed safety checks
const checkboxes = permitModal.querySelectorAll('input[name="safetyCheck"]');
permit.completedSafetyChecks = [];
checkboxes.forEach(cb => {
if(cb.checked){
permit.completedSafetyChecks.push(cb.value);
}
});
// Add new comment if any
const newComment = permitModal.querySelector('#newComment').value.trim();
if(newComment.length > 0){
permit.comments.push(newComment);
permitModal.querySelector('#newComment').value = '';
}
savePermits();
renderPermits();
renderApprovalPermits();
// Refresh modal to show new comments and updated info
openPermitModal(permit.id);
}
// Update permit status from modal buttons
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();
// After approval, consider active/In Progress
permit.status = 'Approved';
}
if(newStatus === 'Closed'){
permit.closedAt = new Date().toISOString();
}
savePermits();
renderPermits();
renderApprovalPermits();
openPermitModal(permit.id);
}
// Submit permit for approval
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');
}
// Approve permit
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();
}
// Reject permit
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();
}
// Open printable permit modal
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();
}
// Close modal functions
function closeModal() {
permitModal.classList.remove('active');
currentEditingPermitId = null;
}
function closePrintModal() {
printModal.classList.remove('active');
}
// Print permit
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();
}
// Create permit form submission
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();
});
// Filter inputs event listeners
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);
// Main tab switching
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();
}
}
// Status sub-tab switching within Monitor tab
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();
}
// Status sub-tab switching within Approval tab
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();
}
// Initial render & setup - default to "Create Permit" tab active and status tab active in monitor
activateMainTab('create');
activateStatusTab('active');
activateApprovalStatusTab('pending');
renderPermits();
renderApprovalPermits();
})();
</script>
</body>
</html>