Spaces:
Running
Running
| <html lang="id"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Pre-Spud Checklist - Workover & Well Intervention</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js"></script> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #f8f9fa; | |
| color: #212529; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| background-color: #0d6efd; | |
| color: white; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| } | |
| .tabs { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| border-bottom: 2px solid #dee2e6; | |
| margin-bottom: 30px; | |
| } | |
| .tab-button { | |
| padding: 10px 20px; | |
| background: #e9ecef; | |
| border: none; | |
| border-radius: 8px 8px 0 0; | |
| cursor: pointer; | |
| font-weight: 500; | |
| transition: all 0.2s ease; | |
| } | |
| .tab-button.active { | |
| background: white; | |
| font-weight: 600; | |
| border-bottom: 2px solid transparent; | |
| box-shadow: 0 -2px 0 #0d6efd inset; | |
| } | |
| .tab-content { | |
| display: none; | |
| background: white; | |
| padding: 25px; | |
| border-radius: 8px; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
| border: 1px solid #dee2e6; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| form { | |
| display: grid; | |
| gap: 15px; | |
| } | |
| label { | |
| font-weight: 600; | |
| color: #495057; | |
| } | |
| input[type="text"], | |
| input[type="date"], | |
| select, | |
| textarea { | |
| width: 100%; | |
| padding: 12px 15px; | |
| border: 1px solid #ced4da; | |
| border-radius: 4px; | |
| box-sizing: border-box; | |
| transition: border-color 0.2s ease; | |
| } | |
| input[type="text"]:focus, | |
| input[type="date"]:focus, | |
| select:focus, | |
| textarea:focus { | |
| outline: none; | |
| border-color: #0d6efd; | |
| box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); | |
| } | |
| textarea { | |
| resize: vertical; | |
| min-height: 80px; | |
| } | |
| .checklist-container { | |
| display: grid; | |
| gap: 12px; | |
| margin: 15px 0; | |
| background-color: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 8px; | |
| } | |
| .checklist-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px; | |
| background: white; | |
| border-radius: 4px; | |
| border: 1px solid #dee2e6; | |
| } | |
| .checklist-item input[type="text"] { | |
| flex: 1; | |
| } | |
| button { | |
| background-color: #0d6efd; | |
| color: white; | |
| padding: 12px 20px; | |
| border: none; | |
| border-radius: 4px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| transition: background-color 0.2s ease; | |
| font-weight: 500; | |
| } | |
| button:hover { | |
| background-color: #0b5ed7; | |
| } | |
| button.edit-record, button.delete-record { | |
| background-color: #ffc107; | |
| padding: 6px 12px; | |
| font-size: 14px; | |
| margin-right: 5px; | |
| } | |
| button.edit-record:hover { | |
| background-color: #e5a206; | |
| } | |
| button.delete-record { | |
| background-color: #dc3545; | |
| padding: 6px 12px; | |
| font-size: 14px; | |
| } | |
| button.delete-record:hover { | |
| background-color: #c52a3a; | |
| } | |
| button.remove { | |
| background-color: #dc3545; | |
| padding: 6px 12px; | |
| font-size: 14px; | |
| } | |
| button.remove:hover { | |
| background-color: #c52a3a; | |
| } | |
| .suggestion-list { | |
| list-style: none; | |
| padding: 0; | |
| margin: 5px 0 0; | |
| border: 1px solid #ced4da; | |
| border-top: none; | |
| max-height: 150px; | |
| overflow-y: auto; | |
| background: white; | |
| position: absolute; | |
| z-index: 1000; | |
| width: calc(100% - 22px); | |
| border-radius: 0 0 4px 4px; | |
| } | |
| .suggestion-list li { | |
| padding: 8px 10px; | |
| cursor: pointer; | |
| border-bottom: 1px solid #eee; | |
| transition: background-color 0.15s ease; | |
| } | |
| .suggestion-list li:hover { | |
| background-color: #f8f9fa; | |
| } | |
| .action-buttons { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .history-group { | |
| margin-bottom: 20px; | |
| border: 1px solid #dee2e6; | |
| border-radius: 8px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| transition: transform 0.2s ease; | |
| } | |
| .history-group:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
| } | |
| .history-header { | |
| background-color: #e9ecef; | |
| padding: 12px 15px; | |
| font-weight: 600; | |
| font-size: 16px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .record-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| } | |
| th, td { | |
| border: 1px solid #dee2e6; | |
| padding: 12px; | |
| text-align: left; | |
| } | |
| th { | |
| background-color: #f8f9fa; | |
| font-weight: 600; | |
| color: #495057; | |
| font-size: 15px; | |
| } | |
| tr:nth-child(even) { | |
| background-color: #fbfcfc; | |
| } | |
| tr:hover { | |
| background-color: #f2f5f7; | |
| } | |
| .filter-section { | |
| margin-bottom: 20px; | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 10px; | |
| padding: 15px; | |
| background-color: #f8f9fa; | |
| border-radius: 8px; | |
| border: 1px solid #dee2e6; | |
| } | |
| #no-records { | |
| text-align: center; | |
| padding: 40px 20px; | |
| color: #6c757d; | |
| font-style: italic; | |
| border: 2px dashed #dee2e6; | |
| border-radius: 8px; | |
| margin-top: 20px; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| z-index: 2000; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .modal-content { | |
| background-color: white; | |
| padding: 30px; | |
| border-radius: 10px; | |
| width: 90%; | |
| max-width: 600px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| } | |
| .close-modal { | |
| position: absolute; | |
| top: 10px; | |
| right: 10px; | |
| font-size: 24px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| color: #6c757d; | |
| background: none; | |
| border: none; | |
| } | |
| .close-modal:hover { | |
| color: #000; | |
| } | |
| .input-group { | |
| margin-bottom: 15px; | |
| } | |
| .export-import-buttons { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 20px; | |
| } | |
| .edit-icon { | |
| width: 14px; | |
| height: 14px; | |
| fill: white; | |
| } | |
| .delete-icon { | |
| width: 14px; | |
| height: 14px; | |
| fill: white; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1 class="text-3xl font-bold mb-2">Pre-Spud Checklist</h1> | |
| <p class="text-xl">Workover & Well Intervention Operations</p> | |
| </header> | |
| <div class="tabs"> | |
| <button class="tab-button active" data-tab="input">Input Rekaman Baru</button> | |
| <button class="tab-button" data-tab="manage">Kelola Checklist</button> | |
| <button class="tab-button" data-tab="history">Riwayat Rekaman</button> | |
| <button class="tab-button" data-tab="export-import">Ekspor/Impor Data</button> | |
| </div> | |
| <div id="input" class="tab-content active"> | |
| <h2 class="text-2xl font-bold mb-4">Input Rekaman Baru</h2> | |
| <form id="pre-spud-form"> | |
| <div class="input-group"> | |
| <label for="pre-spud-date">Tanggal Pre-Spud:</label> | |
| <input type="date" id="pre-spud-date" required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="operation-type">Jenis Operasi:</label> | |
| <select id="operation-type" required> | |
| <option value="">Pilih jenis operasi</option> | |
| <option value="Workover">Workover</option> | |
| <option value="Well Intervention">Well Intervention</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label for="program-number">Nomor Program:</label> | |
| <div class="suggestion-wrapper relative"> | |
| <input type="text" id="program-number" required autocomplete="off"> | |
| <ul class="suggestion-list" id="program-suggestions"></ul> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label for="rig-name">Nama Rig:</label> | |
| <div class="suggestion-wrapper relative"> | |
| <input type="text" id="rig-name" required autocomplete="off"> | |
| <ul class="suggestion-list" id="rig-suggestions"></ul> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label for="location-from">Lokasi Saat Ini:</label> | |
| <div class="suggestion-wrapper relative"> | |
| <input type="text" id="location-from" required autocomplete="off"> | |
| <ul class="suggestion-list" id="from-suggestions"></ul> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label for="location-to">Lokasi Tujuan:</label> | |
| <div class="suggestion-wrapper relative"> | |
| <input type="text" id="location-to" required autocomplete="off"> | |
| <ul class="suggestion-list" id="to-suggestions"></ul> | |
| </div> | |
| </div> | |
| <div class="input-group"> | |
| <label>Checklist Persyaratan:</label> | |
| <div id="dynamic-checklist" class="checklist-container"></div> | |
| </div> | |
| <div class="input-group"> | |
| <label for="notes">Catatan Tambahan (opsional):</label> | |
| <textarea id="notes"></textarea> | |
| </div> | |
| <div class="action-buttons"> | |
| <button type="submit" id="submit-btn">Submit & Rekam Checklist</button> | |
| </div> | |
| </form> | |
| </div> | |
| <div id="manage" class="tab-content"> | |
| <h2 class="text-2xl font-bold mb-4">Kelola Item Checklist</h2> | |
| <p class="text-gray-700 mb-4">Tambah, edit, atau hapus item checklist sesuai kebutuhan operasi.</p> | |
| <div id="manage-checklist" class="checklist-container"></div> | |
| <button id="add-new-item" class="mt-4">Tambah Item Baru</button> | |
| </div> | |
| <div id="history" class="tab-content"> | |
| <h2 class="text-2xl font-bold mb-4">Riwayat Rekaman Checklist</h2> | |
| <div class="filter-section"> | |
| <input type="date" id="filter-date-from" placeholder="Dari Tanggal"> | |
| <input type="date" id="filter-date-to" placeholder="Sampai Tanggal"> | |
| <select id="filter-operation"> | |
| <option value="">Semua Jenis Operasi</option> | |
| <option value="Workover">Workover</option> | |
| <option value="Well Intervention">Well Intervention</option> | |
| </select> | |
| <input type="text" id="filter-location" placeholder="Cari Lokasi / Program / Rig"> | |
| <button id="apply-filter">Terapkan Filter</button> | |
| <button id="clear-filter">Hapus Filter</button> | |
| </div> | |
| <div id="history-container"></div> | |
| <div id="no-records">Belum ada rekaman.</div> | |
| </div> | |
| <div id="export-import" class="tab-content"> | |
| <h2 class="text-2xl font-bold mb-4">Ekspor/Impor Data</h2> | |
| <p class="text-gray-700 mb-6">Gunakan fitur ini untuk menyimpan cadangan data atau memindahkan data antar perangkat.</p> | |
| <div class="export-import-buttons mb-8"> | |
| <button id="export-data" class="bg-success hover:bg-green-500">Ekspor Data ke XLS</button> | |
| <label for="import-file" class="bg-warning hover:bg-yellow-500 cursor-pointer inline-block py-2 px-4 rounded"> | |
| Impor Data dari File | |
| </label> | |
| <input type="file" id="import-file" accept=".xls,.xlsx,.json" class="hidden"> | |
| </div> | |
| <div class="bg-blue-50 p-6 rounded-lg border border-blue-200"> | |
| <h3 class="text-lg font-semibold mb-3 text-blue-800">Petunjuk Impor/Ekspor</h3> | |
| <ul class="list-disc pl-5 text-blue-700 space-y-2"> | |
| <li>Gunakan tombol "Ekspor Data ke XLS" untuk menyimpan cadangan semua data checklist</li> | |
| <li>Gunakan tombol "Impor Data dari File" untuk memulihkan data dari file ekspor sebelumnya</li> | |
| <li>File impor harus berformat XLS, XLSX, atau JSON</li> | |
| <li>Impor data akan menggantikan semua data yang ada saat ini</li> | |
| <li>Dianjurkan untuk membuat cadangan sebelum melakukan impor</li> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- Edit Record Modal --> | |
| <div id="edit-modal" class="modal"> | |
| <div class="modal-content"> | |
| <button class="close-modal">×</button> | |
| <h2 class="text-2xl font-bold mb-6">Edit Rekaman Checklist</h2> | |
| <form id="edit-form"> | |
| <input type="hidden" id="edit-record-id"> | |
| <div class="input-group"> | |
| <label for="edit-pre-spud-date">Tanggal Pre-Spud:</label> | |
| <input type="date" id="edit-pre-spud-date" required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="edit-operation-type">Jenis Operasi:</label> | |
| <select id="edit-operation-type" required> | |
| <option value="">Pilih jenis operasi</option> | |
| <option value="Workover">Workover</option> | |
| <option value="Well Intervention">Well Intervention</option> | |
| </select> | |
| </div> | |
| <div class="input-group"> | |
| <label for="edit-program-number">Nomor Program:</label> | |
| <input type="text" id="edit-program-number" required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="edit-rig-name">Nama Rig:</label> | |
| <input type="text" id="edit-rig-name" required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="edit-location-from">Lokasi Saat Ini:</label> | |
| <input type="text" id="edit-location-from" required> | |
| </div> | |
| <div class="input-group"> | |
| <label for="edit-location-to">Lokasi Tujuan:</label> | |
| <input type="text" id="edit-location-to" required> | |
| </div> | |
| <div class="input-group"> | |
| <label>Checklist Persyaratan:</label> | |
| <div id="edit-dynamic-checklist" class="checklist-container"></div> | |
| </div> | |
| <div class="input-group"> | |
| <label for="edit-notes">Catatan Tambahan (opsional):</label> | |
| <textarea id="edit-notes"></textarea> | |
| </div> | |
| <div class="action-buttons"> | |
| <button type="submit">Simpan Perubahan</button> | |
| <button type="button" id="cancel-edit" class="bg-secondary">Batal</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Confirmation Modal --> | |
| <div id="confirm-modal" class="modal"> | |
| <div class="modal-content" style="max-width: 400px;"> | |
| <button class="close-modal">×</button> | |
| <h2 class="text-2xl font-bold mb-4">Konfirmasi</h2> | |
| <p id="confirm-message" class="mb-6"></p> | |
| <div class="action-buttons"> | |
| <button id="confirm-yes">Ya</button> | |
| <button id="confirm-no" class="bg-secondary">Tidak</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Tab functionality | |
| const tabs = document.querySelectorAll('.tab-button'); | |
| const contents = document.querySelectorAll('.tab-content'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| tabs.forEach(t => t.classList.remove('active')); | |
| contents.forEach(c => c.classList.remove('active')); | |
| tab.classList.add('active'); | |
| document.getElementById(tab.dataset.tab).classList.add('active'); | |
| if (tab.dataset.tab === 'history') loadRecords(); | |
| if (tab.dataset.tab === 'input' || tab.dataset.tab === 'manage') loadChecklistItems(); | |
| }); | |
| }); | |
| // Data storage keys | |
| const checklistKey = 'checklistItems'; | |
| const recordsKey = 'preSpudRecords'; | |
| const suggestionsKey = 'fieldSuggestions'; // {programs: [], rigs: [], fromLocations: [], toLocations: []} | |
| // Default checklist items | |
| const defaultChecklist = [ | |
| "Inspeksi peralatan telah selesai", | |
| "Protokol keselamatan telah diverifikasi", | |
| "Pelatihan personel telah dikonfirmasi", | |
| "Persetujuan regulasi telah diperoleh", | |
| "Logistik dan transportasi telah diatur" | |
| ]; | |
| // Checklist management functions | |
| function getChecklistItems() { | |
| let items = JSON.parse(localStorage.getItem(checklistKey)); | |
| if (!items || items.length === 0) { | |
| items = defaultChecklist; | |
| localStorage.setItem(checklistKey, JSON.stringify(items)); | |
| } | |
| return items; | |
| } | |
| function saveChecklistItems(items) { | |
| localStorage.setItem(checklistKey, JSON.stringify(items)); | |
| } | |
| function loadChecklistItems(forInput = true) { | |
| const items = getChecklistItems(); | |
| const container = forInput ? document.getElementById('dynamic-checklist') : document.getElementById('manage-checklist'); | |
| container.innerHTML = ''; | |
| items.forEach((item, index) => { | |
| const div = document.createElement('div'); | |
| div.className = 'checklist-item'; | |
| if (forInput) { | |
| div.innerHTML = `<input type="checkbox" class="checklist-item-cb" id="cb-${index}"><label for="cb-${index}">${item}</label>`; | |
| } else { | |
| div.innerHTML = `<input type="text" value="${item}" data-index="${index}"><button class="remove" data-index="${index}">Hapus</button>`; | |
| } | |
| container.appendChild(div); | |
| }); | |
| if (!forInput) { | |
| // Add event listeners for manage tab | |
| document.querySelectorAll('#manage-checklist .remove').forEach(btn => { | |
| btn.addEventListener('click', e => { | |
| const idx = parseInt(e.target.dataset.index); | |
| const newItems = items.filter((_, i) => i !== idx); | |
| saveChecklistItems(newItems); | |
| loadChecklistItems(false); | |
| }); | |
| }); | |
| document.querySelectorAll('#manage-checklist input[type="text"]').forEach(input => { | |
| input.addEventListener('change', e => { | |
| const idx = parseInt(e.target.dataset.index); | |
| items[idx] = e.target.value.trim(); | |
| saveChecklistItems(items); | |
| }); | |
| }); | |
| } | |
| } | |
| document.getElementById('add-new-item').addEventListener('click', () => { | |
| const items = getChecklistItems(); | |
| items.push("Item checklist baru"); | |
| saveChecklistItems(items); | |
| loadChecklistItems(false); | |
| }); | |
| // Suggestions management | |
| function getSuggestions() { | |
| return JSON.parse(localStorage.getItem(suggestionsKey)) || { | |
| programs: [], | |
| rigs: [], | |
| fromLocations: [], | |
| toLocations: [] | |
| }; | |
| } | |
| function saveSuggestions(suggestions) { | |
| localStorage.setItem(suggestionsKey, JSON.stringify(suggestions)); | |
| } | |
| function addSuggestion(field, value) { | |
| if (!value.trim()) return; | |
| const suggestions = getSuggestions(); | |
| const list = suggestions[field]; | |
| // Remove if exists | |
| const index = list.indexOf(value.trim()); | |
| if (index > -1) list.splice(index, 1); | |
| // Add to beginning | |
| list.unshift(value.trim()); | |
| // Limit history to 20 items | |
| if (list.length > 20) list.pop(); | |
| saveSuggestions(suggestions); | |
| } | |
| function setupSuggestions(inputId, suggestionListId, field) { | |
| const input = document.getElementById(inputId); | |
| const list = document.getElementById(suggestionListId); | |
| input.addEventListener('input', () => { | |
| const val = input.value.toLowerCase(); | |
| const suggestions = getSuggestions()[field]; | |
| list.innerHTML = ''; | |
| if (val) { | |
| const filteredSuggestions = suggestions.filter(item => | |
| item.toLowerCase().includes(val) | |
| ).slice(0, 8); | |
| if (filteredSuggestions.length > 0) { | |
| filteredSuggestions.forEach(item => { | |
| const li = document.createElement('li'); | |
| li.textContent = item; | |
| li.addEventListener('click', () => { | |
| input.value = item; | |
| list.innerHTML = ''; | |
| }); | |
| list.appendChild(li); | |
| }); | |
| } | |
| } | |
| }); | |
| input.addEventListener('blur', () => setTimeout(() => { | |
| if (list && document.body.contains(list)) { | |
| list.innerHTML = ''; | |
| } | |
| }, 200)); | |
| } | |
| // Setup suggestions for all fields | |
| setupSuggestions('program-number', 'program-suggestions', 'programs'); | |
| setupSuggestions('rig-name', 'rig-suggestions', 'rigs'); | |
| setupSuggestions('location-from', 'from-suggestions', 'fromLocations'); | |
| setupSuggestions('location-to', 'to-suggestions', 'toLocations'); | |
| // Form submission | |
| const form = document.getElementById('pre-spud-form'); | |
| form.addEventListener('submit', e => { | |
| e.preventDefault(); | |
| const record = { | |
| preSpudDate: document.getElementById('pre-spud-date').value, | |
| recordDate: new Date().toLocaleString('id-ID'), | |
| operationType: document.getElementById('operation-type').value, | |
| programNumber: document.getElementById('program-number').value.trim(), | |
| rigName: document.getElementById('rig-name').value.trim(), | |
| fromLocation: document.getElementById('location-from').value.trim(), | |
| toLocation: document.getElementById('location-to').value.trim(), | |
| notes: document.getElementById('notes').value.trim(), | |
| checklist: [] | |
| }; | |
| // Get checklist state | |
| const checklistItems = getChecklistItems(); | |
| const checkboxes = document.querySelectorAll('.checklist-item-cb'); | |
| checklistItems.forEach((item, index) => { | |
| record.checklist.push({ | |
| text: item, | |
| checked: checkboxes[index]?.checked || false | |
| }); | |
| }); | |
| // Save suggestions | |
| addSuggestion('programs', record.programNumber); | |
| addSuggestion('rigs', record.rigName); | |
| addSuggestion('fromLocations', record.fromLocation); | |
| addSuggestion('toLocations', record.toLocation); | |
| // Save record | |
| const records = JSON.parse(localStorage.getItem(recordsKey)) || []; | |
| records.push(record); | |
| localStorage.setItem(recordsKey, JSON.stringify(records)); | |
| form.reset(); | |
| alert('Checklist berhasil direkam!'); | |
| // Reload history if on history tab | |
| if (document.querySelector('.tab-button.active').dataset.tab === 'history') { | |
| loadRecords(); | |
| } | |
| }); | |
| // Edit Modal Functionality | |
| const editModal = document.getElementById('edit-modal'); | |
| const closeModalButtons = document.querySelectorAll('.close-modal'); | |
| const cancelEdit = document.getElementById('cancel-edit'); | |
| closeModalButtons.forEach(button => { | |
| button.addEventListener('click', () => { | |
| editModal.style.display = 'none'; | |
| confirmModal.style.display = 'none'; | |
| }); | |
| }); | |
| // Close modals when clicking outside of them | |
| window.addEventListener('click', (e) => { | |
| if (e.target === editModal) { | |
| editModal.style.display = 'none'; | |
| } | |
| if (e.target === confirmModal) { | |
| confirmModal.style.display = 'none'; | |
| } | |
| }); | |
| cancelEdit.addEventListener('click', () => { | |
| editModal.style.display = 'none'; | |
| }); | |
| // Load checklist items for edit form | |
| function loadEditChecklistItems(checklistStatus = null) { | |
| const items = getChecklistItems(); | |
| const container = document.getElementById('edit-dynamic-checklist'); | |
| container.innerHTML = ''; | |
| items.forEach((item, index) => { | |
| const div = document.createElement('div'); | |
| div.className = 'checklist-item'; | |
| // Check if we have status data, otherwise default to unchecked | |
| const isChecked = checklistStatus && checklistStatus[index] ? checklistStatus[index].checked : false; | |
| const checkedAttr = isChecked ? 'checked' : ''; | |
| div.innerHTML = ` | |
| <input type="checkbox" class="edit-checklist-item-cb" id="edit-cb-${index}" ${checkedAttr}> | |
| <label for="edit-cb-${index}">${item}</label> | |
| `; | |
| container.appendChild(div); | |
| }); | |
| } | |
| // Edit form submission | |
| document.getElementById('edit-form').addEventListener('submit', e => { | |
| e.preventDefault(); | |
| const recordId = document.getElementById('edit-record-id').value; | |
| const records = JSON.parse(localStorage.getItem(recordsKey)) || []; | |
| const recordIndex = records.findIndex((_, index) => index.toString() === recordId); | |
| if (recordIndex === -1) return; | |
| // Update the record | |
| records[recordIndex] = { | |
| ...records[recordIndex], | |
| preSpudDate: document.getElementById('edit-pre-spud-date').value, | |
| operationType: document.getElementById('edit-operation-type').value, | |
| programNumber: document.getElementById('edit-program-number').value.trim(), | |
| rigName: document.getElementById('edit-rig-name').value.trim(), | |
| fromLocation: document.getElementById('edit-location-from').value.trim(), | |
| toLocation: document.getElementById('edit-location-to').value.trim(), | |
| notes: document.getElementById('edit-notes').value.trim(), | |
| checklist: [] | |
| }; | |
| // Update checklist status | |
| const checklistItems = getChecklistItems(); | |
| const checkboxes = document.querySelectorAll('.edit-checklist-item-cb'); | |
| checklistItems.forEach((item, index) => { | |
| records[recordIndex].checklist.push({ | |
| text: item, | |
| checked: checkboxes[index]?.checked || false | |
| }); | |
| }); | |
| // Save updated records | |
| localStorage.setItem(recordsKey, JSON.stringify(records)); | |
| editModal.style.display = 'none'; | |
| // Refresh history | |
| if (document.querySelector('.tab-button.active').dataset.tab === 'history') { | |
| loadRecords(); | |
| } | |
| alert('Rekaman berhasil diperbarui!'); | |
| }); | |
| // History display and management | |
| const confirmModal = document.getElementById('confirm-modal'); | |
| let pendingAction = null; | |
| function loadRecords(filter = {}) { | |
| const records = JSON.parse(localStorage.getItem(recordsKey)) || []; | |
| const container = document.getElementById('history-container'); | |
| const noRecords = document.getElementById('no-records'); | |
| container.innerHTML = ''; | |
| let filtered = records; | |
| // Apply filters | |
| if (filter.dateFrom) filtered = filtered.filter(r => r.preSpudDate >= filter.dateFrom); | |
| if (filter.dateTo) filtered = filtered.filter(r => r.preSpudDate <= filter.dateTo); | |
| if (filter.operation) filtered = filtered.filter(r => r.operationType === filter.operation); | |
| if (filter.search) { | |
| const s = filter.search.toLowerCase(); | |
| filtered = filtered.filter(r => | |
| r.programNumber.toLowerCase().includes(s) || | |
| r.rigName.toLowerCase().includes(s) || | |
| r.fromLocation.toLowerCase().includes(s) || | |
| r.toLocation.toLowerCase().includes(s) | |
| ); | |
| } | |
| if (filtered.length === 0) { | |
| noRecords.style.display = 'block'; | |
| return; | |
| } | |
| noRecords.style.display = 'none'; | |
| // Sort by preSpudDate descending | |
| filtered.sort((a, b) => b.preSpudDate.localeCompare(a.preSpudDate)); | |
| filtered.forEach((record, index) => { | |
| const group = document.createElement('div'); | |
| group.className = 'history-group'; | |
| // Calculate checklist status | |
| const totalItems = record.checklist?.length || getChecklistItems().length; | |
| const checkedItems = record.checklist?.filter(item => item.checked).length || 0; | |
| const checklistStatus = `${checkedItems}/${totalItems}`; | |
| const checklistPercent = totalItems > 0 ? Math.round((checkedItems / totalItems) * 100) : 0; | |
| const checklistColor = checklistPercent === 100 ? 'text-green-600' : checklistPercent === 0 ? 'text-red-600' : 'text-yellow-600'; | |
| group.innerHTML = ` | |
| <div class="history-header"> | |
| <span> | |
| <strong>[${record.preSpudDate}]</strong> ${record.operationType} - | |
| Program: ${record.programNumber} - | |
| Rig: ${record.rigName} | |
| <br> | |
| <small>(Dari: ${record.fromLocation} → Ke: ${record.toLocation})</small> | |
| </span> | |
| <div class="record-actions"> | |
| <button class="edit-record" data-id="${index}" title="Edit rekaman"> | |
| <svg class="edit-icon" viewBox="0 0 24 24"> | |
| <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/> | |
| </svg> | |
| </button> | |
| <button class="delete-record" data-id="${index}" title="Hapus rekaman"> | |
| <svg class="delete-icon" viewBox="0 0 24 24"> | |
| <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Waktu Rekam</th> | |
| <th>Status Checklist</th> | |
| <th>Catatan</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td>${record.recordDate}</td> | |
| <td> | |
| <div class="flex items-center"> | |
| <div class="w-full bg-gray-300 rounded-full h-2.5 mr-2"> | |
| <div class="bg-blue-600 h-2.5 rounded-full" style="width: ${checklistPercent}%"></div> | |
| </div> | |
| <span class="text-sm font-medium ${checklistColor}">${checklistStatus} selesai</span> | |
| </div> | |
| </td> | |
| <td>${record.notes || '-'}</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| `; | |
| container.appendChild(group); | |
| }); | |
| // Add event listeners for edit and delete buttons | |
| document.querySelectorAll('.edit-record').forEach(btn => { | |
| btn.addEventListener('click', e => { | |
| const recordId = e.target.closest('.edit-record').dataset.id; | |
| openEditModal(recordId); | |
| }); | |
| }); | |
| document.querySelectorAll('.delete-record').forEach(btn => { | |
| btn.addEventListener('click', e => { | |
| const recordId = e.target.closest('.delete-record').dataset.id; | |
| confirmDelete(recordId); | |
| }); | |
| }); | |
| } | |
| function openEditModal(recordId) { | |
| const records = JSON.parse(localStorage.getItem(recordsKey)) || []; | |
| const record = records[recordId]; | |
| if (!record) return; | |
| // Fill the form | |
| document.getElementById('edit-record-id').value = recordId; | |
| document.getElementById('edit-pre-spud-date').value = record.preSpudDate; | |
| document.getElementById('edit-operation-type').value = record.operationType; | |
| document.getElementById('edit-program-number').value = record.programNumber; | |
| document.getElementById('edit-rig-name').value = record.rigName; | |
| document.getElementById('edit-location-from').value = record.fromLocation; | |
| document.getElementById('edit-location-to').value = record.toLocation; | |
| document.getElementById('edit-notes').value = record.notes || ''; | |
| // Load checklist items with current status | |
| loadEditChecklistItems(record.checklist); | |
| // Show modal | |
| editModal.style.display = 'flex'; | |
| } | |
| function confirmDelete(recordId) { | |
| pendingAction = { | |
| type: 'delete', | |
| recordId: recordId | |
| }; | |
| document.getElementById('confirm-message').textContent = 'Apakah Anda yakin ingin menghapus rekaman ini? Tindakan ini tidak dapat dibatalkan.'; | |
| confirmModal.style.display = 'flex'; | |
| } | |
| document.getElementById('confirm-yes').addEventListener('click', () => { | |
| if (pendingAction.type === 'delete') { | |
| const records = JSON.parse(localStorage.getItem(recordsKey)) || []; | |
| records.splice(pendingAction.recordId, 1); | |
| localStorage.setItem(recordsKey, JSON.stringify(records)); | |
| // Refresh history | |
| loadRecords(); | |
| alert('Rekaman berhasil dihapus!'); | |
| } | |
| confirmModal.style.display = 'none'; | |
| pendingAction = null; | |
| }); | |
| document.getElementById('confirm-no').addEventListener('click', () => { | |
| confirmModal.style.display = 'none'; | |
| pendingAction = null; | |
| }); | |
| // Filter functionality | |
| document.getElementById('apply-filter').addEventListener('click', () => { | |
| const filter = { | |
| dateFrom: document.getElementById('filter-date-from').value, | |
| dateTo: document.getElementById('filter-date-to').value, | |
| operation: document.getElementById('filter-operation').value, | |
| search: document.getElementById('filter-location').value.trim() | |
| }; | |
| loadRecords(filter); | |
| }); | |
| document.getElementById('clear-filter').addEventListener('click', () => { | |
| document.getElementById('filter-date-from').value = ''; | |
| document.getElementById('filter-date-to').value = ''; | |
| document.getElementById('filter-operation').value = ''; | |
| document.getElementById('filter-location').value = ''; | |
| loadRecords(); | |
| }); | |
| // Export/Import functionality | |
| document.getElementById('export-data').addEventListener('click', () => { | |
| // Create workbook | |
| const wb = XLSX.utils.book_new(); | |
| // Create worksheet from records | |
| const records = JSON.parse(localStorage.getItem(recordsKey)) || []; | |
| if (records.length === 0) { | |
| alert('Tidak ada data untuk diekspor.'); | |
| return; | |
| } | |
| // Transform records to flat data structure for spreadsheet | |
| const exportData = records.map(record => { | |
| const checklistStatus = record.checklist?.map(item => | |
| `${item.text}: ${item.checked ? 'Completed' : 'Pending'}` | |
| ).join('\n') || ''; | |
| return { | |
| 'Tanggal Pre-Spud': record.preSpudDate, | |
| 'Waktu Rekam': record.recordDate, | |
| 'Jenis Operasi': record.operationType, | |
| 'Nomor Program': record.programNumber, | |
| 'Nama Rig': record.rigName, | |
| 'Lokasi Saat Ini': record.fromLocation, | |
| 'Lokasi Tujuan': record.toLocation, | |
| 'Status Checklist': checklistStatus, | |
| 'Catatan': record.notes || '' | |
| }; | |
| }); | |
| const ws = XLSX.utils.json_to_sheet(exportData); | |
| // Add formatting | |
| ws['!cols'] = [ | |
| {wch: 12}, // Tanggal Pre-Spud | |
| {wch: 15}, // Waktu Rekam | |
| {wch: 15}, // Jenis Operasi | |
| {wch: 15}, // Nomor Program | |
| {wch: 15}, // Nama Rig | |
| {wch: 20}, // Lokasi Saat Ini | |
| {wch: 20}, // Lokasi Tujuan | |
| {wch: 40}, // Status Checklist | |
| {wch: 30} // Catatan | |
| ]; | |
| // Add style to header | |
| const headerRange = XLSX.utils.decode_range(ws['!ref']); | |
| for (let C = headerRange.s.c; C <= headerRange.e.c; ++C) { | |
| const headerCell = XLSX.utils.encode_cell({c: C, r: 0}); | |
| if (!ws[headerCell]) ws[headerCell] = {t: 's'}; | |
| ws[headerCell].s = { | |
| font: {bold: true, color: {rgb: "FFFFFF"}}, | |
| fill: {fgColor: {rgb: "4F81BD"}}, | |
| alignment: {horizontal: "center"} | |
| }; | |
| } | |
| // Add worksheet to workbook | |
| XLSX.utils.book_append_sheet(wb, ws, "PreSpudRecords"); | |
| // Create checklist items sheet | |
| const checklistItems = getChecklistItems(); | |
| const checklistData = checklistItems.map(item => ({'Item Checklist': item})); | |
| const checklistWs = XLSX.utils.json_to_sheet(checklistData); | |
| // Add checklist worksheet to workbook | |
| XLSX.utils.book_append_sheet(wb, checklistWs, "ChecklistItems"); | |
| // Export file | |
| XLSX.writeFile(wb, `pre-spud-checklist-${new Date().toISOString().slice(0, 10)}.xlsx`); | |
| }); | |
| document.getElementById('import-file').addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| try { | |
| const data = new Uint8Array(e.target.result); | |
| const workbook = XLSX.read(data, {type: 'array'}); | |
| // Check if required sheets exist | |
| if (!workbook.Sheets['PreSpudRecords']) { | |
| alert('File tidak valid. Sheet "PreSpudRecords" tidak ditemukan.'); | |
| return; | |
| } | |
| // Parse the records sheet | |
| const records = XLSX.utils.sheet_to_json(workbook.Sheets['PreSpudRecords']); | |
| if (records.length === 0) { | |
| alert('Tidak ada data ditemukan dalam file.'); | |
| return; | |
| } | |
| // Transform data back to our format | |
| const importedRecords = records.map(record => { | |
| // Parse checklist status | |
| const checklistText = getChecklistItems(); | |
| const checklistStatus = record['Status Checklist'] || ''; | |
| const checklist = checklistText.map(itemText => { | |
| // Check if the item is in the status text as completed | |
| const isCompleted = checklistStatus.includes(`${itemText}: Completed`); | |
| return { | |
| text: itemText, | |
| checked: isCompleted | |
| }; | |
| }); | |
| return { | |
| preSpudDate: record['Tanggal Pre-Spud'], | |
| recordDate: record['Waktu Rekam'] || new Date().toLocaleString('id-ID'), | |
| operationType: record['Jenis Operasi'], | |
| programNumber: record['Nomor Program'], | |
| rigName: record['Nama Rig'], | |
| fromLocation: record['Lokasi Saat Ini'], | |
| toLocation: record['Lokasi Tujuan'], | |
| notes: record['Catatan'], | |
| checklist: checklist | |
| }; | |
| }); | |
| // If there's a ChecklistItems sheet, import that too | |
| if (workbook.Sheets['ChecklistItems']) { | |
| const checklistItemsSheet = XLSX.utils.sheet_to_json(workbook.Sheets['ChecklistItems']); | |
| const importedChecklistItems = checklistItemsSheet.map(item => item['Item Checklist']); | |
| if (importedChecklistItems.length > 0) { | |
| saveChecklistItems(importedChecklistItems); | |
| } | |
| } | |
| // Confirm with user before replacing data | |
| if (confirm(`Impor data selesai. Apakah Anda ingin menggantikan semua data yang ada dengan ${importedRecords.length} rekaman baru?`)) { | |
| localStorage.setItem(recordsKey, JSON.stringify(importedRecords)); | |
| // Refresh all views | |
| loadChecklistItems(true); | |
| loadChecklistItems(false); | |
| if (document.querySelector('.tab-button.active').dataset.tab === 'history') { | |
| loadRecords(); | |
| } | |
| alert(`Data berhasil diimpor! ${importedRecords.length} rekaman telah dimuat.`); | |
| } | |
| // Reset file input | |
| e.target.value = ''; | |
| } catch (error) { | |
| console.error('Error importing data:', error); | |
| alert('Terjadi kesalahan saat mengimpor file. Pastikan format file benar.'); | |
| e.target.value = ''; | |
| } | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| }); | |
| // Initial loads | |
| loadChecklistItems(true); | |
| loadChecklistItems(false); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-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/prespud-v3" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |