Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Rig Operation Procedure Builder - LEGO Style</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/docx/7.2.0/docx.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Poppins', sans-serif; | |
| } | |
| .procedure-block { | |
| @apply bg-white border-2 border-dashed border-gray-300 rounded-lg shadow-sm p-4 mb-4 cursor-move transition-all hover:shadow-md; | |
| } | |
| .procedure-block.dragging { | |
| @apply opacity-50 border-blue-400; | |
| } | |
| .upload-area { | |
| @apply border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer hover:bg-gray-50 transition-all relative overflow-hidden; | |
| } | |
| .upload-area input[type="file"] { | |
| @apply absolute inset-0 w-full h-full opacity-0 cursor-pointer; | |
| } | |
| .drag-over { | |
| @apply bg-blue-50 border-blue-400; | |
| } | |
| .step-number { | |
| @apply bg-blue-600 text-white w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold; | |
| } | |
| .flex-between-center { | |
| @apply flex items-center justify-between; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Header --> | |
| <header class="text-center mb-8"> | |
| <h1 class="text-4xl font-bold text-gray-800 mb-2">🧱 Rig Operation Procedure Builder</h1> | |
| <p class="text-lg text-gray-600">Build your procedures like LEGO blocks. Drag, edit, and publish!</p> | |
| </header> | |
| <!-- Main Toolbar --> | |
| <div class="bg-white rounded-lg shadow-lg p-6 mb-6"> | |
| <div class="flex-between-center mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-700">🛠️ Procedure Toolbox</h2> | |
| <div class="flex space-x-3"> | |
| <button id="saveBtn" class="bg-green-600 hover:bg-green-700 text-white px-5 py-2 rounded-lg text-sm font-medium flex items-center gap-1 transition"> | |
| <i class="fas fa-save"></i> Save | |
| </button> | |
| <button id="printBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg text-sm font-medium flex items-center gap-1 transition"> | |
| <i class="fas fa-print"></i> Print | |
| </button> | |
| <button id="downloadWordBtn" class="bg-purple-600 hover:bg-purple-700 text-white px-5 py-2 rounded-lg text-sm font-medium flex items-center gap-1 transition"> | |
| <i class="fas fa-file-word"></i> Download Word | |
| </button> | |
| <button id="downloadRTFBtn" class="bg-teal-600 hover:bg-teal-700 text-white px-5 py-2 rounded-lg text-sm font-medium flex items-center gap-1 transition"> | |
| <i class="fas fa-file-alt"></i> Download RTF | |
| </button> | |
| <button id="approvalBtn" class="bg-orange-500 hover:bg-orange-600 text-white px-5 py-2 rounded-lg text-sm font-medium flex items-center gap-1 transition"> | |
| <i class="fas fa-check-circle"></i> Submit for Approval | |
| </button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"> | |
| <div class="toolbox-item p-4 bg-blue-50 border border-blue-200 rounded-lg cursor-pointer hover:bg-blue-100 transition text-center" data-type="image"> | |
| <i class="fas fa-image text-blue-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-blue-800">Upload Image</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-green-50 border border-green-200 rounded-lg cursor-pointer hover:bg-green-100 transition text-center" data-type="time"> | |
| <i class="fas fa-clock text-green-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-green-800">Time Estimate</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-red-50 border border-red-200 rounded-lg cursor-pointer hover:bg-red-100 transition text-center" data-type="material"> | |
| <i class="fas fa-tools text-red-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-red-800">Materials</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-yellow-50 border border-yellow-200 rounded-lg cursor-pointer hover:bg-yellow-100 transition text-center" data-type="safety"> | |
| <i class="fas fa-shield-alt text-yellow-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-yellow-800">Safety Concerns</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-indigo-50 border border-indigo-200 rounded-lg cursor-pointer hover:bg-indigo-100 transition text-center" data-type="personnel"> | |
| <i class="fas fa-users text-indigo-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-indigo-800">Personnel</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-gray-50 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-100 transition text-center" data-type="note"> | |
| <i class="fas fa-sticky-note text-gray-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-gray-800">Notes</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-pink-50 border border-pink-200 rounded-lg cursor-pointer hover:bg-pink-100 transition text-center" data-type="warning"> | |
| <i class="fas fa-exclamation-triangle text-pink-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-pink-800">Warning</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-teal-50 border border-teal-200 rounded-lg cursor-pointer hover:bg-teal-100 transition text-center" data-type="checklist"> | |
| <i class="fas fa-check-square text-teal-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-teal-800">Checklist</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-purple-50 border border-purple-200 rounded-lg cursor-pointer hover:bg-purple-100 transition text-center" data-type="reference"> | |
| <i class="fas fa-book text-purple-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-purple-800">Reference Doc</p> | |
| </div> | |
| <div class="toolbox-item p-4 bg-gray-50 border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition text-center" data-type="step"> | |
| <i class="fas fa-list-ol text-gray-600 text-2xl mb-2"></i> | |
| <p class="text-sm font-medium text-gray-800">Text Step</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Procedure Builder Area --> | |
| <div id="procedureBuilder" class="bg-white rounded-lg shadow-lg p-6 min-h-96 mb-6"> | |
| <div class="text-center text-gray-500 mb-4" id="placeholder"> | |
| <i class="fas fa-plus-circle text-3xl mb-2 text-gray-400"></i> | |
| <p>Drag item dari toolbox atau klik untuk menambahkan blok prosedur</p> | |
| </div> | |
| </div> | |
| <!-- Database & Approval Panel --> | |
| <div class="bg-white rounded-lg shadow-lg p-6"> | |
| <h2 class="text-xl font-semibold text-gray-700 mb-4">📁 Database & Approval Status</h2> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full text-sm"> | |
| <thead> | |
| <tr class="border-b"> | |
| <th class="text-left py-2">Procedure ID</th> | |
| <th class="text-left py-2">Title</th> | |
| <th class="text-left py-2">Last Edited</th> | |
| <th class="text-left py-2">Status</th> | |
| <th class="text-left py-2">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="procedureTableBody"> | |
| <!-- Placeholder row --> | |
| <tr class="border-b"> | |
| <td class="py-2">PRC-001</td> | |
| <td>Main Drilling Sequence</td> | |
| <td>2023-10-15 14:30</td> | |
| <td><span class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs">Pending</span></td> | |
| <td> | |
| <button class="text-blue-600 text-sm hover:underline">Edit</button> | | |
| <button class="text-gray-600 text-sm hover:underline">View</button> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Template Blocks (Hidden) --> | |
| <div class="hidden"> | |
| <!-- Image Block --> | |
| <div id="template-image" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Image Upload</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <div class="upload-area" data-type="image"> | |
| <i class="fas fa-cloud-upload-alt text-gray-400 text-3xl mb-2"></i> | |
| <p class="text-gray-500 text-sm">Klik atau seret gambar ke sini</p> | |
| <input type="file" accept="image/*" class="upload-input"> | |
| <img class="hidden mt-2 max-h-48 rounded border" /> | |
| </div> | |
| </div> | |
| <!-- Time Block --> | |
| <div id="template-time" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Time Estimate</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-xs font-semibold text-gray-700 mb-1">Start Time:</label> | |
| <input type="time" class="w-full border border-gray-300 rounded px-2 py-1 text-sm" value="08:00"> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-gray-700 mb-1">End Time:</label> | |
| <input type="time" class="w-full border border-gray-300 rounded px-2 py-1 text-sm" value="10:30"> | |
| </div> | |
| </div> | |
| <div class="mt-2"> | |
| <label class="block text-xs font-semibold text-gray-700 mb-1">Total Duration:</label> | |
| <input type="text" class="w-full border border-gray-300 rounded px-2 py-1 text-sm bg-gray-100" value="2h 30m" readonly> | |
| </div> | |
| </div> | |
| <!-- Material Block --> | |
| <div id="template-material" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Required Materials</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <div class="material-list space-y-2 mb-2"></div> | |
| <button class="add-material bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs flex items-center gap-1"> | |
| <i class="fas fa-plus text-xs"></i> Add Material | |
| </button> | |
| </div> | |
| <!-- Safety Block --> | |
| <div id="template-safety" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Safety Concerns</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <textarea class="w-full border border-red-200 rounded px-3 py-2 text-sm bg-red-50 resize-none focus:ring-2 focus:ring-red-300" | |
| placeholder="Describe safety concerns, PPE requirements, emergency procedures..."></textarea> | |
| </div> | |
| <!-- Personnel Block --> | |
| <div id="template-personnel" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Required Personnel</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <div class="personnel-list space-y-2 mb-2"></div> | |
| <button class="add-personnel bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs flex items-center gap-1"> | |
| <i class="fas fa-plus text-xs"></i> Add Personnel | |
| </button> | |
| </div> | |
| <!-- Note Block --> | |
| <div id="template-note" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Additional Notes</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <textarea class="w-full border border-gray-200 rounded px-3 py-2 text-sm resize-none focus:ring-2 focus:ring-gray-300" | |
| placeholder="Enter additional information or instructions..."></textarea> | |
| </div> | |
| <!-- Warning Block --> | |
| <div id="template-warning" class="procedure-block border-l-4 border-l-orange-500"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">⚠️ Critical Warning</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <textarea class="w-full border border-orange-200 rounded px-3 py-2 text-sm bg-orange-50 resize-none focus:ring-2 focus:ring-orange-300 font-medium" | |
| placeholder="Enter critical warning message..."></textarea> | |
| </div> | |
| <!-- Checklist Block --> | |
| <div id="template-checklist" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Checklist</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <div class="checklist-items space-y-2 mb-2"></div> | |
| <button class="add-checklist bg-gray-200 hover:bg-gray-300 text-gray-700 px-3 py-1 rounded text-xs flex items-center gap-1"> | |
| <i class="fas fa-plus text-xs"></i> Add Item | |
| </button> | |
| </div> | |
| <!-- Reference Block --> | |
| <div id="template-reference" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Reference Document</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <div class="upload-area" data-type="reference"> | |
| <i class="fas fa-file-pdf text-gray-400 text-3xl mb-2"></i> | |
| <p class="text-gray-500 text-sm">Klik atau seret dokumen referensi</p> | |
| <input type="file" class="upload-input"> | |
| </div> | |
| </div> | |
| <!-- Text Step Block --> | |
| <div id="template-step" class="procedure-block"> | |
| <div class="flex-between-center mb-3"> | |
| <div class="step-number">?</div> | |
| <div class="text-sm text-gray-500">Procedure Step</div> | |
| <div class="flex gap-1"> | |
| <button class="edit-block text-blue-600 hover:text-blue-800 text-xs"><i class="fas fa-edit"></i></button> | |
| <button class="remove-block text-red-600 hover:text-red-800 text-xs"><i class="fas fa-trash"></i></button> | |
| </div> | |
| </div> | |
| <textarea class="w-full border border-gray-200 rounded px-3 py-2 text-sm focus:ring-2 focus:ring-blue-300" | |
| placeholder="Describe the operation procedure step by step..."></textarea> | |
| </div> | |
| </div> | |
| <script> | |
| // Global state | |
| let procedureCounter = 1; | |
| let blocks = []; | |
| let selectedProcedure = null; | |
| // Elements | |
| const builder = document.getElementById('procedureBuilder'); | |
| const placeholder = document.getElementById('placeholder'); | |
| const toolboxItems = document.querySelectorAll('.toolbox-item'); | |
| const saveBtn = document.getElementById('saveBtn'); | |
| const printBtn = document.getElementById('printBtn'); | |
| const downloadWordBtn = document.getElementById('downloadWordBtn'); | |
| const downloadRTFBtn = document.getElementById('downloadRTFBtn'); | |
| const approvalBtn = document.getElementById('approvalBtn'); | |
| // Initialize | |
| function init() { | |
| setupToolbox(); | |
| setupBuilder(); | |
| setupEvents(); | |
| loadProcedures(); | |
| } | |
| // Setup toolbox drag and click | |
| function setupToolbox() { | |
| toolboxItems.forEach(item => { | |
| const type = item.dataset.type; | |
| // Click to add | |
| item.addEventListener('click', () => { | |
| addBlock(type); | |
| }); | |
| // Drag start | |
| item.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('text/plain', type); | |
| item.style.opacity = '0.5'; | |
| }); | |
| item.addEventListener('dragend', () => { | |
| item.style.opacity = '1'; | |
| }); | |
| }); | |
| } | |
| // Setup builder area | |
| function setupBuilder() { | |
| // Allow drop | |
| builder.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| const uploadAreas = builder.querySelectorAll('.upload-area'); | |
| uploadAreas.forEach(area => { | |
| if (area.contains(e.target) || area === e.target) { | |
| area.classList.add('drag-over'); | |
| } | |
| }); | |
| }); | |
| builder.addEventListener('dragleave', (e) => { | |
| const uploadAreas = builder.querySelectorAll('.upload-area'); | |
| uploadAreas.forEach(area => { | |
| if (area.contains(e.target) || area === e.target) { | |
| area.classList.remove('drag-over'); | |
| } | |
| }); | |
| }); | |
| builder.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| const uploadAreas = builder.querySelectorAll('.upload-area'); | |
| uploadAreas.forEach(area => { | |
| area.classList.remove('drag-over'); | |
| }); | |
| const type = e.dataTransfer.getData('text/plain'); | |
| if (type) { | |
| addBlock(type); | |
| } | |
| }); | |
| // Click on builder to add | |
| builder.addEventListener('click', (e) => { | |
| if (e.target === builder || e.target === placeholder) { | |
| if (blocks.length === 0) { | |
| addBlock('step'); | |
| } | |
| } | |
| }); | |
| } | |
| // Add block to builder | |
| function addBlock(type) { | |
| const template = document.getElementById(`template-${type}`).cloneNode(true); | |
| const id = `block-${Date.now()}`; | |
| template.id = id; | |
| template.style.display = 'block'; | |
| const stepNumber = template.querySelector('.step-number'); | |
| stepNumber.textContent = procedureCounter++; | |
| if (type === 'material') { | |
| setupMaterialBlock(template); | |
| } else if (type === 'personnel') { | |
| setupPersonnelBlock(template); | |
| } else if (type === 'checklist') { | |
| setupChecklistBlock(template); | |
| } else if (type === 'image' || type === 'reference') { | |
| setupUploadArea(template); | |
| } | |
| template.querySelector('.remove-block').addEventListener('click', () => { | |
| if (confirm('Are you sure you want to remove this block?')) { | |
| template.remove(); | |
| updateStepNumbers(); | |
| } | |
| }); | |
| template.querySelector('.edit-block').addEventListener('click', () => { | |
| alert('Edit mode. Modify content directly.'); | |
| }); | |
| template.setAttribute('draggable', true); | |
| template.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('text/plain', 'move'); | |
| e.dataTransfer.setDragImage(template, 0, 0); | |
| template.classList.add('dragging'); | |
| }); | |
| template.addEventListener('dragend', () => { | |
| template.classList.remove('dragging'); | |
| }); | |
| builder.appendChild(template); | |
| placeholder.style.display = 'none'; | |
| blocks.push({ id, type, element: template }); | |
| } | |
| function setupMaterialBlock(block) { | |
| const list = block.querySelector('.material-list'); | |
| const addButton = block.querySelector('.add-material'); | |
| function addMaterialItem() { | |
| const item = document.createElement('div'); | |
| item.className = 'flex gap-2 items-center'; | |
| item.innerHTML = ` | |
| <input type="text" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" placeholder="Material name"> | |
| <input type="number" class="w-20 border border-gray-300 rounded px-2 py-1 text-sm" placeholder="Qty" value="1"> | |
| <select class="border border-gray-300 rounded px-2 py-1 text-sm"> | |
| <option>pcs</option> | |
| <option>kg</option> | |
| <option>liter</option> | |
| <option>unit</option> | |
| </select> | |
| <button class="remove-material text-red-600 hover:text-red-800 text-xs"><i class="fas fa-times"></i></button> | |
| `; | |
| list.appendChild(item); | |
| item.querySelector('.remove-material').addEventListener('click', () => { | |
| item.remove(); | |
| }); | |
| } | |
| addButton.addEventListener('click', addMaterialItem); | |
| addMaterialItem(); | |
| } | |
| function setupPersonnelBlock(block) { | |
| const list = block.querySelector('.personnel-list'); | |
| const addButton = block.querySelector('.add-personnel'); | |
| function addPersonnelItem() { | |
| const item = document.createElement('div'); | |
| item.className = 'flex gap-2 items-center'; | |
| item.innerHTML = ` | |
| <input type="text" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" placeholder="Name"> | |
| <select class="w-32 border border-gray-300 rounded px-2 py-1 text-sm"> | |
| <option>Driller</option> | |
| <option>Engineer</option> | |
| <option>Supervisor</option> | |
| <option>Assistant</option> | |
| <option>Inspector</option> | |
| </select> | |
| <button class="remove-personnel text-red-600 hover:text-red-800 text-xs"><i class="fas fa-times"></i></button> | |
| `; | |
| list.appendChild(item); | |
| item.querySelector('.remove-personnel').addEventListener('click', () => { | |
| item.remove(); | |
| }); | |
| } | |
| addButton.addEventListener('click', addPersonnelItem); | |
| addPersonnelItem(); | |
| } | |
| function setupChecklistBlock(block) { | |
| const list = block.querySelector('.checklist-items'); | |
| const addButton = block.querySelector('.add-checklist'); | |
| function addChecklistItem() { | |
| const item = document.createElement('div'); | |
| item.className = 'flex items-center gap-2'; | |
| item.innerHTML = ` | |
| <input type="checkbox" class="w-4 h-4"> | |
| <input type="text" class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm" placeholder="Task to check"> | |
| <button class="remove-checklist text-red-600 hover:text-red-800 text-xs"><i class="fas fa-times"></i></button> | |
| `; | |
| list.appendChild(item); | |
| item.querySelector('.remove-checklist').addEventListener('click', () => { | |
| item.remove(); | |
| }); | |
| } | |
| addButton.addEventListener('click', addChecklistItem); | |
| addChecklistItem(); | |
| } | |
| function setupUploadArea(block) { | |
| const area = block.querySelector('.upload-area'); | |
| const input = area.querySelector('input[type="file"]'); | |
| const img = area.querySelector('img'); | |
| area.addEventListener('click', () => { | |
| input.click(); | |
| }); | |
| input.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (file && img) { | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| img.src = e.target.result; | |
| img.classList.remove('hidden'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| area.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| const file = e.dataTransfer.files[0]; | |
| if (file) { | |
| input.files = e.dataTransfer.files; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| if (img) { | |
| img.src = e.target.result; | |
| img.classList.remove('hidden'); | |
| } | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| } | |
| function updateStepNumbers() { | |
| procedureCounter = 1; | |
| document.querySelectorAll('.procedure-block').forEach(block => { | |
| const num = block.querySelector('.step-number'); | |
| if (num) { | |
| num.textContent = procedureCounter++; | |
| } | |
| }); | |
| } | |
| function setupEvents() { | |
| builder.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| const afterElement = getDragAfterElement(builder, e.clientY); | |
| const draggable = document.querySelector('.dragging'); | |
| if (afterElement == null) { | |
| builder.appendChild(draggable); | |
| } else { | |
| builder.insertBefore(draggable, afterElement); | |
| } | |
| }); | |
| saveBtn.addEventListener('click', () => { | |
| const title = prompt('Enter procedure title:', 'New Procedure') || 'Untitled'; | |
| const id = 'PRC-' + Date.now().toString().substr(-6).toUpperCase(); | |
| const now = new Date().toISOString().replace('T', ' ').substr(0, 16); | |
| const procedure = { | |
| id, | |
| title, | |
| lastEdited: now, | |
| status: 'Draft', | |
| blocks: Array.from(document.querySelectorAll('.procedure-block')).map(block => { | |
| return { | |
| id: block.id, | |
| type: block.querySelector('.toolbox-item')?.dataset.type || 'unknown', | |
| html: block.outerHTML | |
| }; | |
| }) | |
| }; | |
| let procedures = JSON.parse(localStorage.getItem('rigProcedures') || '[]'); | |
| procedures.push(procedure); | |
| localStorage.setItem('rigProcedures', JSON.stringify(procedures)); | |
| alert(`Procedure "${title}" saved successfully!`); | |
| loadProcedures(); | |
| }); | |
| printBtn.addEventListener('click', () => { | |
| window.print(); | |
| }); | |
| // Download as DOCX | |
| downloadWordBtn.addEventListener('click', () => { | |
| const { Document, Paragraph, TextRun, ImageRun, Packer } = docx; | |
| const blocks = document.querySelectorAll('.procedure-block'); | |
| const elements = []; | |
| blocks.forEach(block => { | |
| const type = block.querySelector('.text-sm').textContent.trim(); | |
| // Extract text content | |
| let content = ''; | |
| const textarea = block.querySelector('textarea'); | |
| const inputs = block.querySelectorAll('input[type="text"], input[type="number"], select'); | |
| const timeInputs = block.querySelectorAll('input[type="time"]'); | |
| const checklist = block.querySelectorAll('.checklist-items input[type="checkbox"]'); | |
| const img = block.querySelector('img'); | |
| // Add step title | |
| elements.push(new Paragraph({ | |
| children: [ | |
| new TextRun({ | |
| text: `Step ${block.querySelector('.step-number').textContent}: ${type}`, | |
| bold: true, | |
| size: 28 | |
| }) | |
| ], | |
| spacing: { after: 100 } | |
| })); | |
| if (textarea && textarea.value) { | |
| elements.push(new Paragraph({ | |
| children: [new TextRun(textarea.value)], | |
| spacing: { after: 100 } | |
| })); | |
| } | |
| if (inputs.length > 0) { | |
| const tableRows = Array.from(inputs).reduce((rows, input, i, arr) => { | |
| if (i % 3 === 0) rows.push([]); | |
| const value = input.value || input.options?.[input.selectedIndex]?.text || ''; | |
| rows[rows.length - 1].push(value); | |
| return rows; | |
| }, []).map(row => { | |
| return new docx.TableRow({ | |
| children: row.map(cell => { | |
| return new docx.TableCell({ | |
| children: [new Paragraph(cell)], | |
| width: { size: 33, type: docx.WidthType.PERCENTAGE } | |
| }); | |
| }) | |
| }); | |
| }); | |
| if (tableRows.length > 0) { | |
| elements.push(new docx.Table({ | |
| rows: tableRows, | |
| width: { size: 100, type: docx.WidthType.PERCENTAGE } | |
| })); | |
| } | |
| } | |
| if (timeInputs.length > 1) { | |
| const start = timeInputs[0].value; | |
| const end = timeInputs[1].value; | |
| elements.push(new Paragraph({ | |
| children: [new TextRun(`Duration: ${start} - ${end}`)], | |
| spacing: { after: 100 } | |
| })); | |
| } | |
| if (checklist.length > 0) { | |
| Array.from(block.querySelectorAll('.checklist-items > div')).forEach(itemDiv => { | |
| const checkbox = itemDiv.querySelector('input[type="checkbox"]'); | |
| const label = itemDiv.querySelector('input[type="text"]'); | |
| elements.push(new Paragraph({ | |
| children: [ | |
| new TextRun({ | |
| text: `[${checkbox.checked ? 'x' : ' '}] ${label.value}`, | |
| size: 24 | |
| }) | |
| ] | |
| })); | |
| }); | |
| } | |
| if (img && !img.classList.contains('hidden')) { | |
| const imgData = img.src; | |
| elements.push(new Paragraph({ | |
| children: [ | |
| new ImageRun({ | |
| data: atob(imgData.split(',')[1]), | |
| transformation: { | |
| width: 300, | |
| height: 200 | |
| } | |
| }) | |
| ], | |
| spacing: { after: 100 } | |
| })); | |
| } | |
| elements.push(new Paragraph({ spacing: { after: 100 } })); // spacer | |
| }); | |
| const doc = new Document({ | |
| sections: [{ | |
| properties: {}, | |
| children: elements | |
| }] | |
| }); | |
| Packer.toBlob(doc).then(blob => { | |
| saveAs(blob, 'Rig_Operation_Procedure.docx'); | |
| }); | |
| }); | |
| // Download as RTF (simplified RTF string) | |
| downloadRTFBtn.addEventListener('click', () => { | |
| let rtfContent = '{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Times New Roman;}}\n'; | |
| rtfContent += '\\viewkind4\\uc1\\pard\\f0\\fs24\\b Rig Operation Procedure\\b0\\par\n'; | |
| rtfContent += '\\par\n'; | |
| document.querySelectorAll('.procedure-block').forEach(block => { | |
| const stepNum = block.querySelector('.step-number').textContent; | |
| const type = block.querySelector('.text-sm').textContent.trim(); | |
| rtfContent += `\\b Step ${stepNum}: ${type}\\b0\\par\n`; | |
| const textarea = block.querySelector('textarea'); | |
| if (textarea && textarea.value) { | |
| rtfContent += textarea.value.replace(/\n/g, '\\line ') + '\\par\n'; | |
| } | |
| const timeInputs = block.querySelectorAll('input[type="time"]'); | |
| if (timeInputs.length === 2) { | |
| const start = timeInputs[0].value; | |
| const end = timeInputs[1].value; | |
| rtfContent += `Duration: ${start} - ${end}\\par\n`; | |
| } | |
| block.querySelectorAll('.material-list > div').forEach(item => { | |
| const name = item.querySelector('input[type="text"]')?.value || ''; | |
| const qty = item.querySelector('input[type="number"]')?.value || ''; | |
| const unit = item.querySelector('select')?.options?.[item.querySelector('select')?.selectedIndex]?.text || ''; | |
| if (name) rtfContent += `\\bullet\\t${qty} ${unit} of ${name}\\par\n`; | |
| }); | |
| block.querySelectorAll('.personnel-list > div').forEach(item => { | |
| const name = item.querySelector('input[type="text"]')?.value || ''; | |
| const role = item.querySelector('select')?.options?.[item.querySelector('select')?.selectedIndex]?.text || ''; | |
| if (name) rtfContent += `\\bullet\\t${name} (${role})\\par\n`; | |
| }); | |
| block.querySelectorAll('.checklist-items > div').forEach(item => { | |
| const label = item.querySelector('input[type="text"]')?.value || ''; | |
| const checked = item.querySelector('input[type="checkbox"]')?.checked ? 'x' : ' '; | |
| rtfContent += `\\bullet\\t[${checked}] ${label}\\par\n`; | |
| }); | |
| rtfContent += '\\par\n'; | |
| }); | |
| rtfContent += '}'; | |
| const blob = new Blob([rtfContent], { type: 'application/rtf' }); | |
| saveAs(blob, 'Rig_Operation_Procedure.rtf'); | |
| }); | |
| approvalBtn.addEventListener('click', () => { | |
| if (blocks.length === 0) { | |
| alert('Please add at least one block before submitting.'); | |
| return; | |
| } | |
| if (confirm('Submit this procedure for approval? It will be reviewed by the safety team.')) { | |
| const procedures = JSON.parse(localStorage.getItem('rigProcedures') || '[]'); | |
| if (procedures.length > 0) { | |
| procedures[procedures.length - 1].status = 'Pending'; | |
| localStorage.setItem('rigProcedures', JSON.stringify(procedures)); | |
| alert('Procedure submitted for approval!'); | |
| loadProcedures(); | |
| } | |
| } | |
| }); | |
| } | |
| function getDragAfterElement(container, y) { | |
| const draggableElements = [...container.querySelectorAll('.procedure-block:not(.dragging)')]; | |
| return draggableElements.reduce((closest, child) => { | |
| const box = child.getBoundingClientRect(); | |
| const offset = y - box.top - box.height / 2; | |
| if (offset < 0 && offset > closest.offset) { | |
| return { offset: offset, element: child }; | |
| } else { | |
| return closest; | |
| } | |
| }, { offset: Number.NEGATIVE_INFINITY }).element; | |
| } | |
| function loadProcedures() { | |
| const tableBody = document.getElementById('procedureTableBody'); | |
| tableBody.innerHTML = ''; | |
| const procedures = JSON.parse(localStorage.getItem('rigProcedures') || '[]'); | |
| procedures.forEach(proc => { | |
| const tr = document.createElement('tr'); | |
| tr.className = 'border-b'; | |
| tr.innerHTML = ` | |
| <td class="py-2">${proc.id}</td> | |
| <td>${proc.title}</td> | |
| <td>${proc.lastEdited}</td> | |
| <td><span class="px-2 py-1 rounded text-xs ${ | |
| proc.status === 'Approved' ? 'bg-green-100 text-green-800' : | |
| proc.status === 'Rejected' ? 'bg-red-100 text-red-800' : | |
| 'bg-yellow-100 text-yellow-800' | |
| }">${proc.status}</span></td> | |
| <td> | |
| <button class="text-blue-600 text-sm hover:underline edit-proc" data-id="${proc.id}">Edit</button> | | |
| <button class="text-gray-600 text-sm hover:underline view-proc" data-id="${proc.id}">View</button> | |
| </td> | |
| `; | |
| tableBody.appendChild(tr); | |
| }); | |
| document.querySelectorAll('.edit-proc').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const id = e.target.dataset.id; | |
| loadProcedure(id); | |
| }); | |
| }); | |
| document.querySelectorAll('.view-proc').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const id = e.target.dataset.id; | |
| loadProcedure(id, true); | |
| }); | |
| }); | |
| } | |
| function loadProcedure(id, readOnly = false) { | |
| const procedures = JSON.parse(localStorage.getItem('rigProcedures') || '[]'); | |
| const procedure = procedures.find(p => p.id === id); | |
| if (!procedure) return; | |
| builder.innerHTML = ''; | |
| blocks = []; | |
| procedureCounter = 1; | |
| procedure.blocks.forEach(blockObj => { | |
| const div = document.createElement('div'); | |
| div.innerHTML = blockObj.html; | |
| const block = div.firstElementChild; | |
| block.style.display = 'block'; | |
| if (readOnly) { | |
| block.querySelectorAll('input, textarea, button').forEach(el => { | |
| if (el.type !== 'checkbox') { | |
| el.disabled = true; | |
| } | |
| if (el.classList.contains('remove-block') || el.classList.contains('edit-block')) { | |
| el.style.display = 'none'; | |
| } | |
| }); | |
| block.removeAttribute('draggable'); | |
| } else { | |
| block.querySelector('.remove-block').addEventListener('click', () => { | |
| if (confirm('Remove this block?')) { | |
| block.remove(); | |
| updateStepNumbers(); | |
| } | |
| }); | |
| block.querySelector('.edit-block').addEventListener('click', () => { | |
| alert('Edit mode. Modify content directly.'); | |
| }); | |
| } | |
| builder.appendChild(block); | |
| blocks.push({ id: block.id, type: blockObj.type, element: block }); | |
| }); | |
| placeholder.style.display = blocks.length === 0 ? 'block' : 'none'; | |
| updateStepNumbers(); | |
| } | |
| window.addEventListener('DOMContentLoaded', init); | |
| </script> | |
| <style media="print"> | |
| .toolbox, .toolbox-item, #placeholder, .edit-block, .remove-block { | |
| display: none ; | |
| } | |
| .procedure-block { | |
| page-break-inside: avoid; | |
| border: 1px dashed #ccc; | |
| } | |
| body { | |
| background: white; | |
| font-size: 11pt; | |
| } | |
| .upload-area img { | |
| max-height: 200px; | |
| } | |
| </style> | |
| <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-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/rig-ops-procedure-v2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |