|
|
<!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> |