alterzick's picture
Add 2 files
2839df3 verified
<!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">
<!-- Toast Notification -->
<div id="toast" class="toast">Data saved successfully!</div>
<!-- Sidebar -->
<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>
<!-- Mobile Menu Button -->
<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>
<!-- Main Content -->
<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">
<!-- Dashboard Page -->
<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">
<!-- Filled dynamically -->
</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>
<!-- Input Page -->
<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">
<!-- Material tags will be added here -->
</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">
<!-- Process tags will be added here -->
</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>
<!-- Records Page -->
<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">
<!-- Filled by JavaScript -->
</tbody>
</table>
</div>
</div>
</section>
<!-- Charts Page -->
<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>
<!-- Import Page -->
<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>
<!-- Settings Page -->
<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">
<!-- Materials will be listed here -->
</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">
<!-- Processes will be listed here -->
</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>
<!-- Add Job Type Modal -->
<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>
<!-- Edit Record Modal -->
<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">
<!-- Filled dynamically -->
</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">
<!-- Material tags will be added here -->
</div>
<div class="flex">
<select id="edit-material-select" class="p-2 border border-gray-300 rounded-l-lg flex-grow">
<!-- Filled dynamically -->
</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">
<!-- Process tags will be added here -->
</div>
<div class="flex">
<select id="edit-process-select" class="p-2 border border-gray-300 rounded-l-lg flex-grow">
<!-- Filled dynamically -->
</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>
// Initial state
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"
];
// Materials and processes - loaded from localStorage or defaults
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"
];
// Format currency
function formatCurrency(value) {
const currency = localStorage.getItem('currency') || 'IDR';
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(value);
}
// Format date
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];
}
}
// Update current date
document.getElementById('current-date').textContent = new Date().toLocaleDateString();
// Navigation
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);
});
});
// Mobile menu toggle
document.getElementById('mobile-menu-toggle').addEventListener('click', () => {
document.querySelector('.sidebar').classList.toggle('collapsed');
document.querySelector('.main-content').classList.toggle('expanded');
});
// Update Job Type Dropdown
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>`;
});
// Update edit modal dropdown
if (editSelect) {
editSelect.innerHTML = '';
jobTypes.forEach(type => {
editSelect.innerHTML += `<option value="${type}">${type}</option>`;
});
}
}
// Update Materials and Processes dropdowns
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>`;
});
}
}
// Show add job modal
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';
});
// Add material tag
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);
}
// Add process 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);
}
// Add material button
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) {
// Check if material tag already exists and remove it
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';
}
});
// Add process button
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) {
// Check if process tag already exists and remove it
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';
}
});
// Remove material tag
document.getElementById('material-tags').addEventListener('click', (e) => {
if (e.target.classList.contains('material-remove')) {
e.target.parentElement.remove();
}
});
// Remove process tag
document.getElementById('process-tags').addEventListener('click', (e) => {
if (e.target.classList.contains('process-remove')) {
e.target.parentElement.remove();
}
});
// Form submission
document.getElementById('saving-form').addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
// Use custom job type if provided
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;
}
// Get materials
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 });
});
// Get processes
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(); // unique ID
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();
});
// Cancel input
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';
});
// Export to CSV
document.getElementById('export-btn').addEventListener('click', () => {
// Format materials and processes as strings
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();
});
// Import CSV
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 = {};
// Map headers to object properties
headers.forEach((h, idx) => {
obj[h.toLowerCase().replace(/ \([^)]*\)/g, '').replace(/ /g, '')] = values[idx] || '';
});
// Parse materials saved
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() });
});
}
// Parse processes saved
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() });
});
}
// Clean up the object
delete obj.materialssaved;
delete obj.operationssaved;
// Set the job type
obj.jobtype = obj.jobtype;
// Set unique ID and timestamp
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);
});
// Edit record
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;
// Clear existing tags
document.getElementById('edit-material-tags').innerHTML = '';
document.getElementById('edit-process-tags').innerHTML = '';
// Add material tags
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);
});
}
// Add process tags
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';
}
// Add material tag in edit mode
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);
}
// Add process tag in edit mode
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);
}
// Add material button in edit mode
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) {
// Check if material tag already exists and remove it
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';
}
});
// Add process button in edit mode
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) {
// Check if process tag already exists and remove it
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';
}
});
// Remove material tag in edit mode
document.getElementById('edit-material-tags').addEventListener('click', (e) => {
if (e.target.classList.contains('edit-material-remove')) {
e.target.parentElement.remove();
}
});
// Remove process tag in edit mode
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;
// Update fields
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;
// Update materials
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 });
});
// Update processes
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!');
});
// Delete record
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!');
}
}
// Reset all data
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.');
}
});
// Search
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);
});
// Update page content based on page
function updatePageContent(page) {
if (page === 'dashboard') {
updateDashboard();
updateCharts();
} else if (page === 'records') {
updateRecords();
} else if (page === 'charts') {
updateCharts();
} else if (page === 'settings') {
updateSettings();
}
}
// Settings page updates
function updateSettings() {
// Update currency select
const currentCurrency = localStorage.getItem('currency') || 'IDR';
document.getElementById('currency-select').value = currentCurrency;
// Update date format select
const dateFormat = localStorage.getItem('dateFormat') || 'DD/MM/YYYY';
document.getElementById('date-format-select').value = dateFormat;
// Update material list
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);
});
// Add event listeners for removing materials
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!');
});
});
// Update processes list
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);
});
// Add event listeners for removing processes
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!');
});
});
}
// Add material from settings
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 = '';
});
// Add process from settings
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 = '';
});
// Currency and date format changes
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();
});
// Dashboard Stats
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);
// Count total items saved
let totalItems = 0;
savingsData.forEach(item => {
if (item.materialsaved) {
item.materialsaved.forEach(material => {
totalItems += parseInt(material.quantity) || 0;
});
}
});
// Count total operations
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;
// Recent activities
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 => {
// Format materials string
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);
});
}
// Records table
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) => {
// Format materials text
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('');
}
// Format processes text
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);
});
}
// Charts
let monthlyChart, jobtypeChart, materialTimeChart, topJobsChart;
function updateCharts() {
// Monthly chart
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 }
}
}
});
// Job type chart
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
}
});
// Material vs Time Chart
// We need to calculate total items per record
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
}
}
}
});
// Top 5 Operations by Cost
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
}
});
}
// Toast
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast show';
setTimeout(() => {
toast.className = toast.className.replace('show', '');
}, 3000);
}
// Initialize
updateJobTypeDropdown();
updateMaterialsDropdown();
updateProcessesDropdown();
updateSettings();
updateDashboard();
updateRecords();
updateCharts();
// Set default page
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>