| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>Drilling & Workover Saving Tracker</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet"/> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); |
| body { |
| font-family: 'Poppins', sans-serif; |
| } |
| .sidebar { |
| transition: 0.3s ease-in-out; |
| } |
| .sidebar.collapsed { |
| transform: translateX(-100%); |
| } |
| .main-content { |
| transition: 0.3s ease-in-out; |
| margin-left: 16rem; |
| } |
| .main-content.expanded { |
| margin-left: 0; |
| } |
| .modal { |
| display: none; |
| position: fixed; |
| z-index: 50; |
| left: 0; |
| top: 0; |
| width: 100%; |
| height: 100%; |
| overflow: auto; |
| background-color: rgba(0,0,0,0.5); |
| } |
| .toast { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| z-index: 100; |
| min-width: 250px; |
| background-color: #333; |
| color: #fff; |
| text-align: center; |
| border-radius: 4px; |
| padding: 16px; |
| font-size: 16px; |
| visibility: hidden; |
| opacity: 0; |
| transition: opacity 0.6s; |
| } |
| .toast.show { |
| visibility: visible; |
| opacity: 1; |
| } |
| .custom-input { |
| border: 1px solid #d1d5db; |
| border-radius: 0.5rem; |
| } |
| .tag { |
| display: inline-block; |
| padding: 0.25rem 0.75rem; |
| border-radius: 9999px; |
| font-size: 0.875rem; |
| margin: 0.25rem; |
| background-color: #e5e7eb; |
| position: relative; |
| } |
| .tag .remove { |
| margin-left: 0.5rem; |
| cursor: pointer; |
| color: #9ca3af; |
| } |
| .tag .remove:hover { |
| color: #ef4444; |
| } |
| .process-tag { |
| background-color: #dbeafe; |
| color: #1e40af; |
| } |
| .material-tag { |
| background-color: #ecfccb; |
| color: #65a30d; |
| } |
| .process-tag .remove, .material-tag .remove { |
| color: #3b82f6; |
| } |
| .process-tag .remove:hover, .material-tag .remove:hover { |
| color: #ef4444; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50"> |
|
|
| |
| <div id="toast" class="toast">Data saved successfully!</div> |
|
|
| |
| <div class="sidebar fixed top-0 left-0 h-full w-64 bg-gradient-to-b from-blue-800 to-blue-600 text-white shadow-lg z-40"> |
| <div class="p-5"> |
| <h1 class="text-2xl font-bold">WellSave</h1> |
| <p class="text-sm opacity-90">Drilling & Workover Tracker</p> |
| </div> |
| <nav class="mt-8"> |
| <a href="#" class="nav-link block py-3 px-6 bg-blue-700" data-page="dashboard"> |
| <i class="fas fa-tachometer-alt mr-3"></i> Dashboard |
| </a> |
| <a href="#" class="nav-link block py-3 px-6 hover:bg-blue-700" data-page="input"> |
| <i class="fas fa-plus-circle mr-3"></i> Input New Saving |
| </a> |
| <a href="#" class="nav-link block py-3 px-6 hover:bg-blue-700" data-page="records"> |
| <i class="fas fa-list-alt mr-3"></i> Records |
| </a> |
| <a href="#" class="nav-link block py-3 px-6 hover:bg-blue-700" data-page="charts"> |
| <i class="fas fa-chart-bar mr-3"></i> Analytics |
| </a> |
| <a href="#" class="nav-link block py-3 px-6 hover:bg-blue-700" data-page="import"> |
| <i class="fas fa-file-import mr-3"></i> Import Data |
| </a> |
| <a href="#" class="nav-link block py-3 px-6 hover:bg-blue-700" data-page="settings"> |
| <i class="fas fa-cog mr-3"></i> Settings |
| </a> |
| </nav> |
| </div> |
|
|
| |
| <button id="mobile-menu-toggle" class="md:hidden fixed top-4 left-4 z-50 p-2 bg-blue-700 text-white rounded-lg"> |
| <i class="fas fa-bars"></i> |
| </button> |
|
|
| |
| <div class="main-content relative min-h-screen ml-64 bg-gray-50 transition-all duration-300"> |
| <header class="bg-white shadow-sm p-4 flex justify-between items-center"> |
| <h2 class="text-xl font-semibold text-gray-700">Drilling & Workover Saving Tracker</h2> |
| <div class="text-sm text-gray-500"> |
| <i class="far fa-calendar-alt mr-1"></i> <span id="current-date"></span> |
| </div> |
| </header> |
|
|
| <main class="p-6"> |
| |
| <section id="dashboard" class="page-content"> |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> |
| <div class="bg-white p-5 rounded-xl shadow-md border-l-4 border-green-500 transform hover:scale-105 transition-transform"> |
| <h3 class="text-gray-500 text-sm font-medium">Total Savings (Cost)</h3> |
| <p class="text-3xl font-bold text-green-600 mt-2" id="total-cost">Rp 0</p> |
| </div> |
| <div class="bg-white p-5 rounded-xl shadow-md border-l-4 border-blue-500 transform hover:scale-105 transition-transform"> |
| <h3 class="text-gray-500 text-sm font-medium">Total Time Saved</h3> |
| <p class="text-3xl font-bold text-blue-600 mt-2" id="total-time">0 hrs</p> |
| </div> |
| <div class="bg-white p-5 rounded-xl shadow-md border-l-4 border-purple-500 transform hover:scale-105 transition-transform"> |
| <h3 class="text-gray-500 text-sm font-medium">Items Saved</h3> |
| <p class="text-3xl font-bold text-purple-600 mt-2" id="total-material">0 items</p> |
| </div> |
| <div class="bg-white p-5 rounded-xl shadow-md border-l-4 border-orange-500 transform hover:scale-105 transition-transform"> |
| <h3 class="text-gray-500 text-sm font-medium">Operations Saved</h3> |
| <p class="text-3xl font-bold text-orange-600 mt-2" id="total-processes">0</p> |
| </div> |
| </div> |
|
|
| <div class="bg-white p-6 rounded-xl shadow-md mb-8"> |
| <h3 class="text-lg font-semibold mb-4">Recent Activities</h3> |
| <div id="recent-activities" class="space-y-3"> |
| |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
| <div class="bg-white p-6 rounded-xl shadow-md"> |
| <h3 class="text-lg font-semibold mb-4">Monthly Savings Trend</h3> |
| <canvas id="monthly-chart" height="250"></canvas> |
| </div> |
| <div class="bg-white p-6 rounded-xl shadow-md"> |
| <h3 class="text-lg font-semibold mb-4">Savings by Operation Type</h3> |
| <canvas id="jobtype-chart" height="250"></canvas> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <section id="input" class="page-content hidden"> |
| <div class="bg-white p-8 rounded-xl shadow-md max-w-4xl mx-auto"> |
| <h2 class="text-2xl font-bold mb-6 text-gray-700">Input New Saving</h2> |
| <form id="saving-form"> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Operation Type</label> |
| <div class="flex"> |
| <select id="job-type" name="jobType" class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"> |
| <option value="">Select operation type</option> |
| <option value="Drilling - Tripping">Drilling - Tripping</option> |
| <option value="Drilling - BHA Runs">Drilling - BHA Runs</option> |
| <option value="Drilling - Bit Runs">Drilling - Bit Runs</option> |
| <option value="Drilling - Cementing">Drilling - Cementing</option> |
| <option value="Workover - Tubing Change">Workover - Tubing Change</option> |
| <option value="Workover - Pump Repair">Workover - Pump Repair</option> |
| <option value="Workover - Zone Isolation">Workover - Zone Isolation</option> |
| <option value="Workover - Stimulations">Workover - Stimulations</option> |
| </select> |
| <button type="button" id="add-job-btn" class="ml-2 bg-gray-200 px-3 rounded-lg hover:bg-gray-300"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Additional Operation Type (if not listed)</label> |
| <input type="text" id="custom-job-type" placeholder="Enter new operation type" class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"/> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Materials Saved (items)</label> |
| <div class="border border-gray-300 rounded-lg p-3"> |
| <div id="material-tags" class="flex flex-wrap mb-2"> |
| |
| </div> |
| <div class="flex"> |
| <select id="material-select" class="p-2 border border-gray-300 rounded-l-lg flex-grow"> |
| <option value="">Select material</option> |
| <option value="Drill Bits">Drill Bits</option> |
| <option value="Motor Assemblies">Motor Assemblies</option> |
| <option value="Mud Motors">Mud Motors</option> |
| <option value="Measurement While Drilling (MWD)">MWD Tools</option> |
| <option value="Logging While Drilling (LWD)">LWD Tools</option> |
| <option value="Casing/Line Pipe">Casing/Line Pipe</option> |
| <option value="Tubing">Tubing</option> |
| <option value="Packers"> Packers</option> |
| <option value="Safety Valves">Safety Valves</option> |
| <option value="Pump Jacks">Pump Jacks</option> |
| <option value="Gas Lift Valves">Gas Lift Valves</option> |
| <option value="Chokes">Chokes</option> |
| </select> |
| <input type="number" id="material-quantity" placeholder="Qty" min="1" value="1" class="p-2 border border-gray-300 w-24"/> |
| <button type="button" id="add-material-btn" class="bg-blue-600 text-white px-4 rounded-r-lg hover:bg-blue-700"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| <small class="text-gray-500">Add materials saved (select item and quantity)</small> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Operations/Processes Saved</label> |
| <div class="border border-gray-300 rounded-lg p-3"> |
| <div id="process-tags" class="flex flex-wrap mb-2"> |
| |
| </div> |
| <div class="flex"> |
| <select id="process-select" class="p-2 border border-gray-300 rounded-l-lg flex-grow"> |
| <option value="">Select process</option> |
| <option value="Connection Time">Connection Time</option> |
| <option value="Tripping Time">Tripping Time</option> |
| <option value="Circulation Time">Circulation Time</option> |
| <option value="Rig Move">Rig Move</option> |
| <option value="Blowout Preventer (BOP) Testing">BOP Testing</option> |
| <option value="Casing Running">Casing Running</option> |
| <option value="Cement Evaluation">Cement Evaluation</option> |
| <option value="Well Control">Well Control</option> |
| <option value="Tubing Running">Tubing Running</option> |
| <option value="Perforation">Perforation</option> |
| <option value="Coiled Tubing">Coiled Tubing</option> |
| <option value="Fishing">Fishing</option> |
| <option value="Plug and Abandon">Plug and Abandon</option> |
| </select> |
| <input type="number" id="process-quantity" placeholder="Qty" min="1" value="1" class="p-2 border border-gray-300 w-24"/> |
| <button type="button" id="add-process-btn" class="bg-blue-600 text-white px-4 rounded-r-lg hover:bg-blue-700"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| <small class="text-gray-500">Add operations saved (select process and quantity)</small> |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Time Saved (hours)</label> |
| <input type="number" name="timeSaved" step="0.1" required class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"/> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Cost Saved (Rp)</label> |
| <input type="number" name="costSaved" required class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"/> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Date</label> |
| <input type="date" name="date" required class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"/> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Time</label> |
| <input type="time" name="time" required class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"/> |
| </div> |
| </div> |
| <div class="mb-6"> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Notes / Description</label> |
| <textarea name="notes" rows="3" class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"></textarea> |
| </div> |
| <div class="flex justify-end space-x-4"> |
| <button type="button" id="cancel-input" class="px-6 py-3 bg-gray-400 text-white rounded-lg hover:bg-gray-500 transition">Cancel</button> |
| <button type="submit" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"> |
| <i class="fas fa-save mr-2"></i> Save Record |
| </button> |
| </div> |
| </form> |
| </div> |
| </section> |
|
|
| |
| <section id="records" class="page-content hidden"> |
| <div class="bg-white p-8 rounded-xl shadow-md"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-bold text-gray-700">Saving Records</h2> |
| <div class="flex space-x-3"> |
| <input type="text" id="search-input" placeholder="Search records..." class="p-2 border border-gray-300 rounded-lg text-sm w-64"/> |
| <button id="export-btn" class="px-4 py-2 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700"> |
| <i class="fas fa-file-excel mr-1"></i> Export |
| </button> |
| </div> |
| </div> |
| <div class="overflow-x-auto"> |
| <table class="min-w-full bg-white border rounded-lg overflow-hidden"> |
| <thead class="bg-gray-100 text-gray-600 uppercase text-sm leading-normal"> |
| <tr> |
| <th class="py-3 px-6 text-left">No</th> |
| <th class="py-3 px-6 text-left">Operation Type</th> |
| <th class="py-3 px-6 text-left">Materials Saved</th> |
| <th class="py-3 px-6 text-left">Time (hrs)</th> |
| <th class="py-3 px-6 text-left">Operations</th> |
| <th class="py-3 px-6 text-left">Cost (Rp)</th> |
| <th class="py-3 px-6 text-left">Date & Time</th> |
| <th class="py-3 px-6 text-center">Actions</th> |
| </tr> |
| </thead> |
| <tbody id="records-body" class="text-gray-600 text-sm"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <section id="charts" class="page-content hidden"> |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
| <div class="bg-white p-6 rounded-xl shadow-md"> |
| <h3 class="text-lg font-semibold mb-4">Material Items vs Time Saved</h3> |
| <canvas id="matrial-time-chart" height="250"></canvas> |
| </div> |
| <div class="bg-white p-6 rounded-xl shadow-md"> |
| <h3 class="text-lg font-semibold mb-4">Top 5 Operations (by Cost)</h3> |
| <canvas id="top-jobs-chart" height="250"></canvas> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <section id="import" class="page-content hidden"> |
| <div class="bg-white p-8 rounded-xl shadow-md max-w-2xl mx-auto"> |
| <h2 class="text-2xl font-bold mb-6 text-gray-700">Import Data</h2> |
| <div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center"> |
| <i class="fas fa-cloud-upload-alt text-6xl text-gray-400 mb-4"></i> |
| <p class="text-gray-600 mb-4">Drag & drop your CSV file here or click to browse</p> |
| <input type="file" id="import-file" accept=".csv" class="hidden"/> |
| <button id="browse-btn" class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> |
| Browse Files |
| </button> |
| </div> |
| <div class="mt-6 text-sm text-gray-500"> |
| <p><strong>Required Columns:</strong> jobType, materialsSaved, timeSaved, processesSaved, costSaved, date, time (ISO format), notes</p> |
| <p><strong>Materials/Processes Format:</strong> Use semicolon to separate items, e.g., "Drill Bits:2;MWD Tools:1"</p> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <section id="settings" class="page-content hidden"> |
| <div class="bg-white p-8 rounded-xl shadow-md max-w-2xl mx-auto"> |
| <h2 class="text-2xl font-bold mb-6 text-gray-700">Settings</h2> |
| <div class="space-y-6"> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-2">Currency Format</label> |
| <select id="currency-select" class="p-3 border border-gray-300 rounded-lg w-full"> |
| <option value="IDR">Indonesian Rupiah (Rp)</option> |
| <option value="USD">US Dollar ($)</option> |
| <option value="EUR">Euro (€)</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-2">Date Format</label> |
| <select id="date-format-select" class="p-3 border border-gray-300 rounded-lg w-full"> |
| <option value="DD/MM/YYYY">DD/MM/YYYY</option> |
| <option value="MM/DD/YYYY">MM/DD/YYYY</option> |
| <option value="YYYY-MM-DD">YYYY-MM-DD</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-2">Custom Materials</label> |
| <div class="border border-gray-300 rounded-lg p-3 mb-2 max-h-40 overflow-y-auto"> |
| <div id="custom-materials-list"> |
| |
| </div> |
| <div class="flex mt-2"> |
| <input type="text" id="new-material-name" placeholder="New material name" class="p-2 border border-gray-300 rounded-l-lg flex-grow"/> |
| <button id="add-material-setting" class="bg-blue-600 text-white px-3 rounded-r-lg hover:bg-blue-700"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-2">Custom Processes</label> |
| <div class="border border-gray-300 rounded-lg p-3 mb-2 max-h-40 overflow-y-auto"> |
| <div id="custom-processes-list"> |
| |
| </div> |
| <div class="flex mt-2"> |
| <input type="text" id="new-process-name" placeholder="New process name" class="p-2 border border-gray-300 rounded-l-lg flex-grow"/> |
| <button id="add-process-setting" class="bg-blue-600 text-white px-3 rounded-r-lg hover:bg-blue-700"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| <div class="flex space-x-4"> |
| <button id="reset-data-btn" class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm"> |
| <i class="fas fa-trash mr-2"></i> Reset All Data |
| </button> |
| </div> |
| </div> |
| </div> |
| </section> |
| </main> |
| </div> |
|
|
| |
| <div id="add-job-modal" class="modal"> |
| <div class="bg-white p-6 rounded-lg shadow-lg max-w-md mx-auto mt-20"> |
| <h3 class="text-lg font-semibold mb-4">Add New Operation Type</h3> |
| <input type="text" id="new-job-input" placeholder="Enter operation type name" class="w-full p-3 border border-gray-300 rounded-lg mb-4"/> |
| <div class="flex justify-end space-x-3"> |
| <button id="cancel-job-btn" class="px-4 py-2 bg-gray-400 text-white rounded-lg">Cancel</button> |
| <button id="save-job-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg">Save</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="edit-modal" class="modal"> |
| <div class="bg-white p-6 rounded-lg shadow-lg max-w-4xl mx-auto mt-10"> |
| <h3 class="text-xl font-bold mb-6">Edit Record</h3> |
| <form id="edit-form"> |
| <input type="hidden" id="edit-id"/> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Operation Type</label> |
| <select id="edit-job-type" class="block w-full p-3 border border-gray-300 rounded-lg focus:ring focus:ring-blue-200"> |
| |
| </select> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Materials Saved</label> |
| <div class="border border-gray-300 rounded-lg p-3"> |
| <div id="edit-material-tags" class="flex flex-wrap mb-2"> |
| |
| </div> |
| <div class="flex"> |
| <select id="edit-material-select" class="p-2 border border-gray-300 rounded-l-lg flex-grow"> |
| |
| </select> |
| <input type="number" id="edit-material-quantity" placeholder="Qty" min="1" value="1" class="p-2 border border-gray-300 w-24"/> |
| <button type="button" id="edit-add-material-btn" class="bg-blue-600 text-white px-4 rounded-r-lg hover:bg-blue-700"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Operations/Processes Saved</label> |
| <div class="border border-gray-300 rounded-lg p-3"> |
| <div id="edit-process-tags" class="flex flex-wrap mb-2"> |
| |
| </div> |
| <div class="flex"> |
| <select id="edit-process-select" class="p-2 border border-gray-300 rounded-l-lg flex-grow"> |
| |
| </select> |
| <input type="number" id="edit-process-quantity" placeholder="Qty" min="1" value="1" class="p-2 border border-gray-300 w-24"/> |
| <button type="button" id="edit-add-process-btn" class="bg-blue-600 text-white px-4 rounded-r-lg hover:bg-blue-700"> |
| <i class="fas fa-plus"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Time Saved (hours)</label> |
| <input type="number" id="edit-time" step="0.1" class="block w-full p-3 border border-gray-300 rounded-lg"/> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Cost Saved</label> |
| <input type="number" id="edit-cost" class="block w-full p-3 border border-gray-300 rounded-lg"/> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Date</label> |
| <input type="date" id="edit-date" class="block w-full p-3 border border-gray-300 rounded-lg"/> |
| </div> |
| |
| <div> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Time</label> |
| <input type="time" id="edit-time-input" class="block w-full p-3 border border-gray-300 rounded-lg"/> |
| </div> |
| </div> |
| <div class="mb-6"> |
| <label class="block text-sm font-medium text-gray-600 mb-1">Notes</label> |
| <textarea id="edit-notes" rows="3" class="block w-full p-3 border border-gray-300 rounded-lg"></textarea> |
| </div> |
| <div class="flex justify-end space-x-4"> |
| <button type="button" id="cancel-edit" class="px-6 py-3 bg-gray-400 text-white rounded-lg hover:bg-gray-500">Cancel</button> |
| <button type="submit" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Update</button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let savingsData = JSON.parse(localStorage.getItem('savingsData')) || []; |
| let jobTypes = JSON.parse(localStorage.getItem('jobTypes')) || [ |
| "Drilling - Tripping", "Drilling - BHA Runs", "Drilling - Bit Runs", "Drilling - Cementing", |
| "Workover - Tubing Change", "Workover - Pump Repair", "Workover - Zone Isolation", "Workover - Stimulations" |
| ]; |
| |
| |
| let materialsList = JSON.parse(localStorage.getItem('materialsList')) || [ |
| "Drill Bits", "Motor Assemblies", "Mud Motors", "Measurement While Drilling (MWD)", |
| "Logging While Drilling (LWD)", "Casing/Line Pipe", "Tubing", "Packers", |
| "Safety Valves", "Pump Jacks", "Gas Lift Valves", "Chokes" |
| ]; |
| |
| let processesList = JSON.parse(localStorage.getItem('processesList')) || [ |
| "Connection Time", "Tripping Time", "Circulation Time", "Rig Move", |
| "Blowout Preventer (BOP) Testing", "Casing Running", "Cement Evaluation", |
| "Well Control", "Tubing Running", "Perforation", "Coiled Tubing", "Fishing", "Plug and Abandon" |
| ]; |
| |
| |
| function formatCurrency(value) { |
| const currency = localStorage.getItem('currency') || 'IDR'; |
| return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(value); |
| } |
| |
| |
| function formatDate(dateStr) { |
| const format = localStorage.getItem('dateFormat') || 'DD/MM/YYYY'; |
| const date = new Date(dateStr); |
| if (format === 'DD/MM/YYYY') { |
| return date.toLocaleDateString('id-ID'); |
| } else if (format === 'MM/DD/YYYY') { |
| return date.toLocaleDateString('en-US'); |
| } else { |
| return date.toISOString().split('T')[0]; |
| } |
| } |
| |
| |
| document.getElementById('current-date').textContent = new Date().toLocaleDateString(); |
| |
| |
| document.querySelectorAll('.nav-link').forEach(link => { |
| link.addEventListener('click', (e) => { |
| e.preventDefault(); |
| const page = e.target.closest('a').getAttribute('data-page'); |
| document.querySelectorAll('.page-content').forEach(section => { |
| section.classList.add('hidden'); |
| }); |
| document.getElementById(page).classList.remove('hidden'); |
| document.querySelectorAll('.nav-link').forEach(link => { |
| link.classList.remove('bg-blue-700'); |
| }); |
| e.target.closest('a').classList.add('bg-blue-700'); |
| updatePageContent(page); |
| }); |
| }); |
| |
| |
| document.getElementById('mobile-menu-toggle').addEventListener('click', () => { |
| document.querySelector('.sidebar').classList.toggle('collapsed'); |
| document.querySelector('.main-content').classList.toggle('expanded'); |
| }); |
| |
| |
| function updateJobTypeDropdown() { |
| const select = document.getElementById('job-type'); |
| const editSelect = document.getElementById('edit-job-type'); |
| select.innerHTML = '<option value="">Select operation type</option>'; |
| jobTypes.forEach(type => { |
| select.innerHTML += `<option value="${type}">${type}</option>`; |
| }); |
| |
| |
| if (editSelect) { |
| editSelect.innerHTML = ''; |
| jobTypes.forEach(type => { |
| editSelect.innerHTML += `<option value="${type}">${type}</option>`; |
| }); |
| } |
| } |
| |
| |
| function updateMaterialsDropdown() { |
| const select = document.getElementById('material-select'); |
| const editSelect = document.getElementById('edit-material-select'); |
| select.innerHTML = '<option value="">Select material</option>'; |
| materialsList.forEach(material => { |
| select.innerHTML += `<option value="${material}">${material}</option>`; |
| }); |
| |
| if (editSelect) { |
| editSelect.innerHTML = '<option value="">Select material</option>'; |
| materialsList.forEach(material => { |
| editSelect.innerHTML += `<option value="${material}">${material}</option>`; |
| }); |
| } |
| } |
| |
| function updateProcessesDropdown() { |
| const select = document.getElementById('process-select'); |
| const editSelect = document.getElementById('edit-process-select'); |
| select.innerHTML = '<option value="">Select process</option>'; |
| processesList.forEach(process => { |
| select.innerHTML += `<option value="${process}">${process}</option>`; |
| }); |
| |
| if (editSelect) { |
| editSelect.innerHTML = '<option value="">Select process</option>'; |
| processesList.forEach(process => { |
| editSelect.innerHTML += `<option value="${process}">${process}</option>`; |
| }); |
| } |
| } |
| |
| |
| document.getElementById('add-job-btn').addEventListener('click', () => { |
| document.getElementById('add-job-modal').style.display = 'block'; |
| }); |
| |
| document.getElementById('cancel-job-btn').addEventListener('click', () => { |
| document.getElementById('add-job-modal').style.display = 'none'; |
| }); |
| |
| document.getElementById('save-job-btn').addEventListener('click', () => { |
| const newJob = document.getElementById('new-job-input').value.trim(); |
| if (newJob && !jobTypes.includes(newJob)) { |
| jobTypes.push(newJob); |
| jobTypes.sort(); |
| localStorage.setItem('jobTypes', JSON.stringify(jobTypes)); |
| updateJobTypeDropdown(); |
| showToast('New operation type added!'); |
| } |
| document.getElementById('new-job-input').value = ''; |
| document.getElementById('add-job-modal').style.display = 'none'; |
| }); |
| |
| |
| function addMaterialTag(material, qty) { |
| const container = document.getElementById('material-tags'); |
| const tag = document.createElement('span'); |
| tag.className = 'tag material-tag'; |
| tag.innerHTML = `${material}: ${qty} <span class="remove material-remove" data-material="${material}">×</span>`; |
| container.appendChild(tag); |
| } |
| |
| |
| function addProcessTag(process, qty) { |
| const container = document.getElementById('process-tags'); |
| const tag = document.createElement('span'); |
| tag.className = 'tag process-tag'; |
| tag.innerHTML = `${process}: ${qty} <span class="remove process-remove" data-process="${process}">×</span>`; |
| container.appendChild(tag); |
| } |
| |
| |
| document.getElementById('add-material-btn').addEventListener('click', () => { |
| const material = document.getElementById('material-select').value; |
| const qty = document.getElementById('material-quantity').value; |
| |
| if (material && qty > 0) { |
| |
| const existing = document.querySelector(`.tag[data-material="${material}"]`); |
| if (existing) existing.remove(); |
| |
| addMaterialTag(material, qty); |
| document.getElementById('material-select').value = ''; |
| document.getElementById('material-quantity').value = '1'; |
| } |
| }); |
| |
| |
| document.getElementById('add-process-btn').addEventListener('click', () => { |
| const process = document.getElementById('process-select').value; |
| const qty = document.getElementById('process-quantity').value; |
| |
| if (process && qty > 0) { |
| |
| const existing = document.querySelector(`.tag[data-process="${process}"]`); |
| if (existing) existing.remove(); |
| |
| addProcessTag(process, qty); |
| document.getElementById('process-select').value = ''; |
| document.getElementById('process-quantity').value = '1'; |
| } |
| }); |
| |
| |
| document.getElementById('material-tags').addEventListener('click', (e) => { |
| if (e.target.classList.contains('material-remove')) { |
| e.target.parentElement.remove(); |
| } |
| }); |
| |
| |
| document.getElementById('process-tags').addEventListener('click', (e) => { |
| if (e.target.classList.contains('process-remove')) { |
| e.target.parentElement.remove(); |
| } |
| }); |
| |
| |
| document.getElementById('saving-form').addEventListener('submit', (e) => { |
| e.preventDefault(); |
| const formData = new FormData(e.target); |
| const data = Object.fromEntries(formData); |
| |
| |
| if (document.getElementById('custom-job-type').value.trim()) { |
| const customJob = document.getElementById('custom-job-type').value.trim(); |
| if (!jobTypes.includes(customJob)) { |
| jobTypes.push(customJob); |
| jobTypes.sort(); |
| localStorage.setItem('jobTypes', JSON.stringify(jobTypes)); |
| updateJobTypeDropdown(); |
| } |
| data.jobType = customJob; |
| } |
| |
| |
| const materialTags = document.querySelectorAll('#material-tags .tag'); |
| data.materialsSaved = []; |
| materialTags.forEach(tag => { |
| const material = tag.querySelector('.material-remove').getAttribute('data-material'); |
| const qtyText = tag.textContent.trim().split(': ')[1]; |
| const qty = qtyText.split(' ')[0]; |
| data.materialsSaved.push({ name: material, quantity: qty }); |
| }); |
| |
| |
| const processTags = document.querySelectorAll('#process-tags .tag'); |
| data.processesSaved = []; |
| processTags.forEach(tag => { |
| const process = tag.querySelector('.process-remove').getAttribute('data-process'); |
| const qtyText = tag.textContent.trim().split(': ')[1]; |
| const qty = qtyText.split(' ')[0]; |
| data.processesSaved.push({ name: process, quantity: qty }); |
| }); |
| |
| data.timestamp = new Date().toISOString(); |
| data.id = Date.now(); |
| savingsData.push(data); |
| localStorage.setItem('savingsData', JSON.stringify(savingsData)); |
| |
| e.target.reset(); |
| document.getElementById('custom-job-type').value = ''; |
| document.getElementById('material-tags').innerHTML = ''; |
| document.getElementById('process-tags').innerHTML = ''; |
| document.getElementById('material-quantity').value = '1'; |
| document.getElementById('process-quantity').value = '1'; |
| showToast("Data saved successfully!"); |
| updateDashboard(); |
| updateRecords(); |
| updateCharts(); |
| }); |
| |
| |
| document.getElementById('cancel-input').addEventListener('click', () => { |
| document.getElementById('saving-form').reset(); |
| document.getElementById('custom-job-type').value = ''; |
| document.getElementById('material-tags').innerHTML = ''; |
| document.getElementById('process-tags').innerHTML = ''; |
| document.getElementById('material-quantity').value = '1'; |
| document.getElementById('process-quantity').value = '1'; |
| }); |
| |
| |
| document.getElementById('export-btn').addEventListener('click', () => { |
| |
| const formatItems = (items) => { |
| return items.map(item => `${item.name}:${item.quantity}`).join(';'); |
| }; |
| |
| const csv = [ |
| ['Job Type', 'Materials Saved', 'Time Saved (hrs)', 'Operations Saved', 'Cost Saved', 'Date', 'Time', 'Notes'] |
| ].concat(savingsData.map(item => [ |
| item.jobType, |
| formatItems(item.materialsSaved || []), |
| item.timeSaved, |
| formatItems(item.processesSaved || []), |
| item.costSaved, |
| item.date, |
| item.time, |
| item.notes || '' |
| ])).map(row => row.join(',')).join('\n'); |
| |
| const blob = new Blob([csv], { type: 'text/csv' }); |
| const url = window.URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'saving_records.csv'; |
| a.click(); |
| }); |
| |
| |
| document.getElementById('browse-btn').addEventListener('click', () => { |
| document.getElementById('import-file').click(); |
| }); |
| |
| document.getElementById('import-file').addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = (event) => { |
| const content = event.target.result; |
| const lines = content.split('\n'); |
| const headers = lines[0].split(',').map(h => h.trim()); |
| |
| const requiredHeaders = ['Job Type', 'Materials Saved', 'Time Saved (hrs)', 'Operations Saved', 'Cost Saved', 'Date', 'Time']; |
| if (!requiredHeaders.every(h => headers.includes(h))) { |
| alert('CSV file must contain the required columns.'); |
| return; |
| } |
| |
| for (let i = 1; i < lines.length; i++) { |
| if (lines[i].trim() === '') continue; |
| const values = lines[i].split(',').map(v => v.trim()); |
| const obj = {}; |
| |
| |
| headers.forEach((h, idx) => { |
| obj[h.toLowerCase().replace(/ \([^)]*\)/g, '').replace(/ /g, '')] = values[idx] || ''; |
| }); |
| |
| |
| obj.materialsaved = []; |
| if (obj.materialssaved && obj.materialssaved.includes(':')) { |
| obj.materialssaved.split(';').forEach(item => { |
| const [name, qty] = item.split(':'); |
| obj.materialsaved.push({ name: name.trim(), quantity: qty.trim() }); |
| }); |
| } |
| |
| |
| obj.processessaved = []; |
| if (obj.operationssaved && obj.operationssaved.includes(':')) { |
| obj.operationssaved.split(';').forEach(item => { |
| const [name, qty] = item.split(':'); |
| obj.processessaved.push({ name: name.trim(), quantity: qty.trim() }); |
| }); |
| } |
| |
| |
| delete obj.materialssaved; |
| delete obj.operationssaved; |
| |
| |
| obj.jobtype = obj.jobtype; |
| |
| |
| obj.id = Date.now() + i; |
| obj.timestamp = new Date().toISOString(); |
| |
| savingsData.push(obj); |
| } |
| |
| localStorage.setItem('savingsData', JSON.stringify(savingsData)); |
| showToast('Data imported successfully!'); |
| updateDashboard(); |
| updateRecords(); |
| updateCharts(); |
| }; |
| reader.readAsText(file); |
| }); |
| |
| |
| function editRecord(id) { |
| const record = savingsData.find(r => r.id == id); |
| if (!record) return; |
| |
| document.getElementById('edit-id').value = record.id; |
| document.getElementById('edit-job-type').value = record.jobType; |
| |
| |
| document.getElementById('edit-material-tags').innerHTML = ''; |
| document.getElementById('edit-process-tags').innerHTML = ''; |
| |
| |
| if (record.materialsaved && record.materialsaved.length > 0) { |
| record.materialsaved.forEach(item => { |
| const tag = document.createElement('span'); |
| tag.className = 'tag material-tag'; |
| tag.innerHTML = `${item.name}: ${item.quantity} <span class="remove edit-material-remove" data-material="${item.name}">×</span>`; |
| document.getElementById('edit-material-tags').appendChild(tag); |
| }); |
| } |
| |
| |
| if (record.processessaved && record.processessaved.length > 0) { |
| record.processessaved.forEach(item => { |
| const tag = document.createElement('span'); |
| tag.className = 'tag process-tag'; |
| tag.innerHTML = `${item.name}: ${item.quantity} <span class="remove edit-process-remove" data-process="${item.name}">×</span>`; |
| document.getElementById('edit-process-tags').appendChild(tag); |
| }); |
| } |
| |
| document.getElementById('edit-time').value = record.timeSaved; |
| document.getElementById('edit-cost').value = record.costSaved; |
| document.getElementById('edit-date').value = record.date; |
| document.getElementById('edit-time-input').value = record.time; |
| document.getElementById('edit-notes').value = record.notes || ''; |
| |
| document.getElementById('edit-modal').style.display = 'block'; |
| } |
| |
| |
| function addEditMaterialTag(material, qty) { |
| const container = document.getElementById('edit-material-tags'); |
| const tag = document.createElement('span'); |
| tag.className = 'tag material-tag'; |
| tag.innerHTML = `${material}: ${qty} <span class="remove edit-material-remove" data-material="${material}">×</span>`; |
| container.appendChild(tag); |
| } |
| |
| |
| function addEditProcessTag(process, qty) { |
| const container = document.getElementById('edit-process-tags'); |
| const tag = document.createElement('span'); |
| tag.className = 'tag process-tag'; |
| tag.innerHTML = `${process}: ${qty} <span class="remove edit-process-remove" data-process="${process}">×</span>`; |
| container.appendChild(tag); |
| } |
| |
| |
| document.getElementById('edit-add-material-btn').addEventListener('click', () => { |
| const material = document.getElementById('edit-material-select').value; |
| const qty = document.getElementById('edit-material-quantity').value; |
| |
| if (material && qty > 0) { |
| |
| const existing = Array.from(document.querySelectorAll('.edit-material-remove')) |
| .find(el => el.getAttribute('data-material') === material); |
| if (existing) existing.parentElement.remove(); |
| |
| addEditMaterialTag(material, qty); |
| document.getElementById('edit-material-select').value = ''; |
| document.getElementById('edit-material-quantity').value = '1'; |
| } |
| }); |
| |
| |
| document.getElementById('edit-add-process-btn').addEventListener('click', () => { |
| const process = document.getElementById('edit-process-select').value; |
| const qty = document.getElementById('edit-process-quantity').value; |
| |
| if (process && qty > 0) { |
| |
| const existing = Array.from(document.querySelectorAll('.edit-process-remove')) |
| .find(el => el.getAttribute('data-process') === process); |
| if (existing) existing.parentElement.remove(); |
| |
| addEditProcessTag(process, qty); |
| document.getElementById('edit-process-select').value = ''; |
| document.getElementById('edit-process-quantity').value = '1'; |
| } |
| }); |
| |
| |
| document.getElementById('edit-material-tags').addEventListener('click', (e) => { |
| if (e.target.classList.contains('edit-material-remove')) { |
| e.target.parentElement.remove(); |
| } |
| }); |
| |
| |
| document.getElementById('edit-process-tags').addEventListener('click', (e) => { |
| if (e.target.classList.contains('edit-process-remove')) { |
| e.target.parentElement.remove(); |
| } |
| }); |
| |
| document.getElementById('cancel-edit').addEventListener('click', () => { |
| document.getElementById('edit-modal').style.display = 'none'; |
| }); |
| |
| document.getElementById('edit-form').addEventListener('submit', (e) => { |
| e.preventDefault(); |
| const id = document.getElementById('edit-id').value; |
| const record = savingsData.find(r => r.id == id); |
| if (!record) return; |
| |
| |
| record.jobType = document.getElementById('edit-job-type').value; |
| record.timeSaved = document.getElementById('edit-time').value; |
| record.costSaved = document.getElementById('edit-cost').value; |
| record.date = document.getElementById('edit-date').value; |
| record.time = document.getElementById('edit-time-input').value; |
| record.notes = document.getElementById('edit-notes').value; |
| |
| |
| record.materialsaved = []; |
| const materialTags = document.querySelectorAll('#edit-material-tags .tag'); |
| materialTags.forEach(tag => { |
| const material = tag.querySelector('.edit-material-remove').getAttribute('data-material'); |
| const qtyText = tag.textContent.trim().split(': ')[1]; |
| const qty = qtyText.split(' ')[0]; |
| record.materialsaved.push({ name: material, quantity: qty }); |
| }); |
| |
| |
| record.processessaved = []; |
| const processTags = document.querySelectorAll('#edit-process-tags .tag'); |
| processTags.forEach(tag => { |
| const process = tag.querySelector('.edit-process-remove').getAttribute('data-process'); |
| const qtyText = tag.textContent.trim().split(': ')[1]; |
| const qty = qtyText.split(' ')[0]; |
| record.processessaved.push({ name: process, quantity: qty }); |
| }); |
| |
| localStorage.setItem('savingsData', JSON.stringify(savingsData)); |
| document.getElementById('edit-modal').style.display = 'none'; |
| updateRecords(); |
| updateDashboard(); |
| updateCharts(); |
| showToast('Record updated!'); |
| }); |
| |
| |
| function deleteRecord(id) { |
| if (confirm('Are you sure you want to delete this record?')) { |
| savingsData = savingsData.filter(r => r.id != id); |
| localStorage.setItem('savingsData', JSON.stringify(savingsData)); |
| updateRecords(); |
| updateDashboard(); |
| updateCharts(); |
| showToast('Record deleted!'); |
| } |
| } |
| |
| |
| document.getElementById('reset-data-btn').addEventListener('click', () => { |
| if (confirm('Are you sure you want to delete ALL data? This cannot be undone.')) { |
| savingsData = []; |
| localStorage.removeItem('savingsData'); |
| updateRecords(); |
| updateDashboard(); |
| updateCharts(); |
| showToast('All data has been reset.'); |
| } |
| }); |
| |
| |
| document.getElementById('search-input').addEventListener('keyup', (e) => { |
| const term = e.target.value.toLowerCase(); |
| const filtered = savingsData.filter(item => |
| item.jobType.toLowerCase().includes(term) || |
| item.notes?.toLowerCase().includes(term) || |
| (item.materialsaved && item.materialsaved.some(m => m.name.toLowerCase().includes(term))) || |
| (item.processessaved && item.processessaved.some(p => p.name.toLowerCase().includes(term))) |
| ); |
| populateRecords(filtered); |
| }); |
| |
| |
| function updatePageContent(page) { |
| if (page === 'dashboard') { |
| updateDashboard(); |
| updateCharts(); |
| } else if (page === 'records') { |
| updateRecords(); |
| } else if (page === 'charts') { |
| updateCharts(); |
| } else if (page === 'settings') { |
| updateSettings(); |
| } |
| } |
| |
| |
| function updateSettings() { |
| |
| const currentCurrency = localStorage.getItem('currency') || 'IDR'; |
| document.getElementById('currency-select').value = currentCurrency; |
| |
| |
| const dateFormat = localStorage.getItem('dateFormat') || 'DD/MM/YYYY'; |
| document.getElementById('date-format-select').value = dateFormat; |
| |
| |
| const materialsListEl = document.getElementById('custom-materials-list'); |
| materialsListEl.innerHTML = ''; |
| materialsList.forEach(material => { |
| const div = document.createElement('div'); |
| div.className = 'flex justify-between items-center py-1 border-b border-gray-100'; |
| div.innerHTML = ` |
| <span>${material}</span> |
| <button class="text-red-500 text-sm remove-material-btn" data-material="${material}">Remove</button> |
| `; |
| materialsListEl.appendChild(div); |
| }); |
| |
| |
| document.querySelectorAll('.remove-material-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const material = e.target.getAttribute('data-material'); |
| materialsList = materialsList.filter(m => m !== material); |
| localStorage.setItem('materialsList', JSON.stringify(materialsList)); |
| updateMaterialsDropdown(); |
| updateSettings(); |
| showToast('Material removed!'); |
| }); |
| }); |
| |
| |
| const processesListEl = document.getElementById('custom-processes-list'); |
| processesListEl.innerHTML = ''; |
| processesList.forEach(process => { |
| const div = document.createElement('div'); |
| div.className = 'flex justify-between items-center py-1 border-b border-gray-100'; |
| div.innerHTML = ` |
| <span>${process}</span> |
| <button class="text-red-500 text-sm remove-process-btn" data-process="${process}">Remove</button> |
| `; |
| processesListEl.appendChild(div); |
| }); |
| |
| |
| document.querySelectorAll('.remove-process-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const process = e.target.getAttribute('data-process'); |
| processesList = processesList.filter(p => p !== process); |
| localStorage.setItem('processesList', JSON.stringify(processesList)); |
| updateProcessesDropdown(); |
| updateSettings(); |
| showToast('Process removed!'); |
| }); |
| }); |
| } |
| |
| |
| document.getElementById('add-material-setting').addEventListener('click', () => { |
| const newMaterial = document.getElementById('new-material-name').value.trim(); |
| if (newMaterial && !materialsList.includes(newMaterial)) { |
| materialsList.push(newMaterial); |
| materialsList.sort(); |
| localStorage.setItem('materialsList', JSON.stringify(materialsList)); |
| updateMaterialsDropdown(); |
| updateSettings(); |
| showToast('New material added!'); |
| } |
| document.getElementById('new-material-name').value = ''; |
| }); |
| |
| |
| document.getElementById('add-process-setting').addEventListener('click', () => { |
| const newProcess = document.getElementById('new-process-name').value.trim(); |
| if (newProcess && !processesList.includes(newProcess)) { |
| processesList.push(newProcess); |
| processesList.sort(); |
| localStorage.setItem('processesList', JSON.stringify(processesList)); |
| updateProcessesDropdown(); |
| updateSettings(); |
| showToast('New process added!'); |
| } |
| document.getElementById('new-process-name').value = ''; |
| }); |
| |
| |
| document.getElementById('currency-select').addEventListener('change', (e) => { |
| localStorage.setItem('currency', e.target.value); |
| updateDashboard(); |
| updateCharts(); |
| }); |
| |
| document.getElementById('date-format-select').addEventListener('change', (e) => { |
| localStorage.setItem('dateFormat', e.target.value); |
| updateRecords(); |
| updateDashboard(); |
| }); |
| |
| |
| function updateDashboard() { |
| const totalCost = savingsData.reduce((sum, item) => sum + parseFloat(item.costSaved || 0), 0); |
| const totalTime = savingsData.reduce((sum, item) => sum + parseFloat(item.timeSaved || 0), 0); |
| |
| |
| let totalItems = 0; |
| savingsData.forEach(item => { |
| if (item.materialsaved) { |
| item.materialsaved.forEach(material => { |
| totalItems += parseInt(material.quantity) || 0; |
| }); |
| } |
| }); |
| |
| |
| let totalOperations = 0; |
| savingsData.forEach(item => { |
| if (item.processessaved) { |
| item.processessaved.forEach(process => { |
| totalOperations += parseInt(process.quantity) || 0; |
| }); |
| } |
| }); |
| |
| document.getElementById('total-cost').textContent = formatCurrency(totalCost); |
| document.getElementById('total-time').textContent = `${totalTime.toFixed(1)} hrs`; |
| document.getElementById('total-material').textContent = `${totalItems} items`; |
| document.getElementById('total-processes').textContent = totalOperations; |
| |
| |
| const recent = savingsData |
| .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) |
| .slice(0, 5); |
| const recentContainer = document.getElementById('recent-activities'); |
| recentContainer.innerHTML = ''; |
| recent.forEach(item => { |
| |
| let materialsText = ''; |
| if (item.materialsaved && item.materialsaved.length > 0) { |
| materialsText = item.materialsaved.map(m => `${m.quantity} ${m.name}`).join(', '); |
| } |
| |
| const div = document.createElement('div'); |
| div.className = 'flex justify-between items-center p-3 bg-gray-50 rounded-lg'; |
| div.innerHTML = ` |
| <div> |
| <p class="font-medium">${item.jobType}</p> |
| <p class="text-sm text-gray-500">${formatDate(item.date)} at ${item.time}</p> |
| ${materialsText ? `<p class="text-sm text-gray-500">${materialsText}</p>` : ''} |
| </div> |
| <div class="text-right"> |
| <p class="font-medium text-green-600">${formatCurrency(item.costSaved)}</p> |
| <p class="text-sm text-gray-500">${totalItems} items saved</p> |
| </div> |
| `; |
| recentContainer.appendChild(div); |
| }); |
| } |
| |
| |
| function updateRecords() { |
| populateRecords(savingsData); |
| } |
| |
| function populateRecords(data) { |
| const body = document.getElementById('records-body'); |
| body.innerHTML = ''; |
| if (data.length === 0) { |
| body.innerHTML = `<tr><td colspan="8" class="py-4 text-center text-gray-500">No records found</td></tr>`; |
| return; |
| } |
| |
| data.slice().reverse().forEach((item, index) => { |
| |
| let materialsText = ''; |
| if (item.materialsaved && item.materialsaved.length > 0) { |
| materialsText = item.materialsaved.map(m => |
| `<span class="inline-block bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full mr-1 mb-1">${m.quantity}×${m.name}</span>` |
| ).join(''); |
| } |
| |
| |
| let processesText = ''; |
| if (item.processessaved && item.processessaved.length > 0) { |
| processesText = item.processessaved.map(p => |
| `<span class="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full mr-1 mb-1">${p.quantity}×${p.name}</span>` |
| ).join(''); |
| } |
| |
| const tr = document.createElement('tr'); |
| tr.className = 'border-t border-gray-200 hover:bg-gray-50'; |
| tr.innerHTML = ` |
| <td class="py-3 px-6">${data.length - index}</td> |
| <td class="py-3 px-6">${item.jobType}</td> |
| <td class="py-3 px-6">${materialsText}</td> |
| <td class="py-3 px-6">${parseFloat(item.timeSaved).toFixed(1)}</td> |
| <td class="py-3 px-6">${processesText}</td> |
| <td class="py-3 px-6">${formatCurrency(item.costSaved)}</td> |
| <td class="py-3 px-6">${formatDate(item.date)} ${item.time}</td> |
| <td class="py-3 px-6 text-center"> |
| <button onclick="editRecord(${item.id})" class="text-blue-600 hover:text-blue-800 mx-1"> |
| <i class="fas fa-edit"></i> |
| </button> |
| <button onclick="deleteRecord(${item.id})" class="text-red-600 hover:text-red-800 mx-1"> |
| <i class="fas fa-trash"></i> |
| </button> |
| </td> |
| `; |
| body.appendChild(tr); |
| }); |
| } |
| |
| |
| let monthlyChart, jobtypeChart, materialTimeChart, topJobsChart; |
| function updateCharts() { |
| |
| const months = Array(12).fill(0); |
| const now = new Date(); |
| const currentYear = now.getFullYear(); |
| savingsData.forEach(item => { |
| const date = new Date(item.date); |
| if (date.getFullYear() === currentYear) { |
| months[date.getMonth()] += parseFloat(item.costSaved); |
| } |
| }); |
| |
| const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; |
| const monthlyCtx = document.getElementById('monthly-chart').getContext('2d'); |
| if (monthlyChart) monthlyChart.destroy(); |
| monthlyChart = new Chart(monthlyCtx, { |
| type: 'line', |
| data: { |
| labels: monthNames, |
| datasets: [{ |
| label: 'Cost Saved (monthly)', |
| data: months, |
| backgroundColor: 'rgba(54, 162, 235, 0.2)', |
| borderColor: 'rgba(54, 162, 235, 1)', |
| tension: 0.3, |
| fill: true |
| }] |
| }, |
| options: { |
| responsive: true, |
| plugins: { |
| legend: { display: false } |
| } |
| } |
| }); |
| |
| |
| const jobCounts = {}; |
| savingsData.forEach(item => { |
| jobCounts[item.jobType] = (jobCounts[item.jobType] || 0) + parseFloat(item.costSaved); |
| }); |
| const jobtypeCtx = document.getElementById('jobtype-chart').getContext('2d'); |
| if (jobtypeChart) jobtypeChart.destroy(); |
| jobtypeChart = new Chart(jobtypeCtx, { |
| type: 'pie', |
| data: { |
| labels: Object.keys(jobCounts), |
| datasets: [{ |
| data: Object.values(jobCounts), |
| backgroundColor: [ |
| '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#C9CBCF', '#46BFBD' |
| ] |
| }] |
| }, |
| options: { |
| responsive: true |
| } |
| }); |
| |
| |
| |
| const chartData = savingsData.map(item => { |
| let totalItems = 0; |
| if (item.materialsaved) { |
| item.materialsaved.forEach(material => { |
| totalItems += parseInt(material.quantity) || 0; |
| }); |
| } |
| return { |
| x: totalItems, |
| y: parseFloat(item.timeSaved), |
| r: Math.sqrt(parseFloat(item.costSaved)) / 10, |
| jobType: item.jobType |
| }; |
| }); |
| |
| const materialTimeCtx = document.getElementById('matrial-time-chart').getContext('2d'); |
| if (materialTimeChart) materialTimeChart.destroy(); |
| materialTimeChart = new Chart(materialTimeCtx, { |
| type: 'scatter', |
| data: { |
| datasets: [{ |
| label: 'Items vs Time Saved', |
| data: chartData, |
| backgroundColor: 'rgba(75, 192, 192, 0.6)' |
| }] |
| }, |
| options: { |
| responsive: true, |
| scales: { |
| x: { |
| title: { display: true, text: 'Items Saved' }, |
| beginAtZero: true |
| }, |
| y: { |
| title: { display: true, text: 'Time Saved (hours)' }, |
| beginAtZero: true |
| } |
| } |
| } |
| }); |
| |
| |
| const sortedJobs = Object.entries(jobCounts) |
| .sort((a,b) => b[1] - a[1]) |
| .slice(0, 5); |
| const topJobsCtx = document.getElementById('top-jobs-chart').getContext('2d'); |
| if (topJobsChart) topJobsChart.destroy(); |
| topJobsChart = new Chart(topJobsCtx, { |
| type: 'bar', |
| data: { |
| labels: sortedJobs.map(j => j[0]), |
| datasets: [{ |
| label: 'Total Cost Saved', |
| data: sortedJobs.map(j => j[1]), |
| backgroundColor: 'rgba(153, 102, 255, 0.6)' |
| }] |
| }, |
| options: { |
| indexAxis: 'y', |
| responsive: true |
| } |
| }); |
| } |
| |
| |
| function showToast(message) { |
| const toast = document.getElementById('toast'); |
| toast.textContent = message; |
| toast.className = 'toast show'; |
| setTimeout(() => { |
| toast.className = toast.className.replace('show', ''); |
| }, 3000); |
| } |
| |
| |
| updateJobTypeDropdown(); |
| updateMaterialsDropdown(); |
| updateProcessesDropdown(); |
| updateSettings(); |
| updateDashboard(); |
| updateRecords(); |
| updateCharts(); |
| |
| |
| document.getElementById('dashboard').classList.remove('hidden'); |
| </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/saving-operation-tracker-v1" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |