Initial commit: Clean Mold Cost Calculator application

- Flask-based mold cost calculation application
- PostgreSQL database with SQLAlchemy ORM
- Docker/Podman containerization
- Comprehensive documentation in docs/
- Clean, organized codebase without obsolete files
- Production-ready deployment configuration
- Enhanced pytest configuration with coverage reporting
- Master/Development branch structure
This commit is contained in:
Gan, Jimmy
2025-06-24 02:30:09 +08:00
commit 6c0d5a4e63
108 changed files with 15911 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
/* Admin Panel Global Styles */
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--info-color: #17a2b8;
--warning-color: #ffc107;
--danger-color: #dc3545;
--light-color: #f8f9fa;
--dark-color: #343a40;
}
/* Layout */
.admin-container {
padding: 1.5rem;
}
.admin-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1.5rem;
}
.admin-card-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid rgba(0,0,0,0.1);
background-color: var(--light-color);
border-radius: 8px 8px 0 0;
}
.admin-card-body {
padding: 1.5rem;
}
/* Tables */
.admin-table {
width: 100%;
margin-bottom: 1rem;
background-color: transparent;
}
.admin-table th,
.admin-table td {
padding: 0.75rem;
vertical-align: middle;
border-top: 1px solid #dee2e6;
}
.admin-table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
background-color: var(--light-color);
font-weight: 600;
}
/* Forms */
.admin-form-group {
margin-bottom: 1rem;
}
.admin-form-control {
display: block;
width: 100%;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
/* Buttons */
.admin-btn {
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
user-select: none;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
}
.admin-btn-primary {
color: #fff;
background-color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.admin-btn-secondary {
color: #fff;
background-color: var(--secondary-color);
border: 1px solid var(--secondary-color);
}
.admin-btn-info {
color: #fff;
background-color: var(--info-color);
border: 1px solid var(--info-color);
}
.admin-btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0.2rem;
}
/* Stats Cards */
.admin-stats-card {
padding: 1.5rem;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1rem;
}
.admin-stats-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
}
.admin-stats-number {
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.admin-stats-label {
color: var(--secondary-color);
font-size: 0.875rem;
}
/* Filters */
.admin-filters {
margin-bottom: 1.5rem;
padding: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.admin-filter-form {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
align-items: end;
}
/* Pagination */
.admin-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 1.5rem;
}
.admin-pagination-link {
padding: 0.375rem 0.75rem;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
color: var(--primary-color);
text-decoration: none;
}
.admin-pagination-link:hover {
background-color: var(--light-color);
}
.admin-pagination-info {
color: var(--secondary-color);
padding: 0.375rem 0.75rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.admin-filter-form {
grid-template-columns: 1fr;
}
.admin-table {
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
+2078
View File
File diff suppressed because it is too large Load Diff
+7
View File
File diff suppressed because one or more lines are too long
+91
View File
@@ -0,0 +1,91 @@
/* Calculator Page Styles */
.container-main {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.calculator-card {
background-color: #ffffff;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.section-header {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
border-left: 4px solid #0d6efd;
}
.section-content {
background-color: #ffffff;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 2rem;
}
.form-label {
color: #495057;
font-weight: 500;
}
.form-select, .form-control {
border: 1px solid #ced4da;
}
.form-select:focus, .form-control:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
/* Table Styles */
.table {
margin-bottom: 0;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}
.table td {
vertical-align: middle;
}
/* Button Styles */
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
font-weight: 500;
}
.btn-primary:hover {
background-color: #0b5ed7;
border-color: #0a58ca;
}
/* Form Text */
.form-text {
color: #6c757d;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.container-main {
margin: 1rem auto;
}
.section-content {
padding: 1rem;
}
.col-md-3 {
margin-bottom: 1rem;
}
}
+144
View File
@@ -0,0 +1,144 @@
:root {
--primary-color: #2563eb;
--secondary-color: #3b82f6;
--success-color: #22c55e;
--warning-color: #f59e0b;
--error-color: #ef4444;
--text-primary: #1e293b;
--text-secondary: #64748b;
--bg-light: #f8fafc;
}
/* Base Styles */
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
color: var(--text-primary);
background-color: var(--bg-light);
line-height: 1.6;
}
.container-main {
max-width: 1280px;
margin: 2rem auto;
padding: 0 1rem;
}
/* Responsive Navigation */
.navbar-brand {
font-weight: 600;
letter-spacing: -0.025em;
}
.nav-link {
transition: all 0.2s ease;
padding: 0.5rem 1rem !important;
border-radius: 0.375rem;
}
.nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Calculator Form */
.calculator-card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 2rem;
margin: 2rem 0;
}
.input-group-3d {
position: relative;
}
.input-group-3d input,
.input-group-3d select {
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 0.875rem 1rem;
transition: all 0.2s ease;
width: 100%;
}
.input-group-3d input:focus,
.input-group-3d select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
/* Result Panel */
.result-card {
border-left: 4px solid var(--success-color);
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
margin-top: 2rem;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Mobile Optimization */
@media (max-width: 768px) {
.calculator-card {
margin: 1rem -0.5rem;
border-radius: 0;
}
.navbar-collapse {
margin-top: 1rem;
padding: 1rem;
background: var(--primary-color);
border-radius: 0.5rem;
}
}
建议配合以下CSS样式添加到base模板中
.input-group-text {
transition: opacity 0.3s ease;
}
.progress-bar {
transition: width 0.5s ease, background-color 0.3s ease;
}
/* Cost Breakdown Visual Enhancement */
.cost-breakdown {
background: #fff;
border-radius: 1rem;
box-shadow: 0 2px 12px 0 rgba(37,99,235,0.08);
padding: 2rem 1.5rem 1.5rem 1.5rem;
margin-bottom: 2rem;
}
.cost-breakdown .cost-item {
border: 1.5px solid #e0e7ef;
box-shadow: 0 1px 4px 0 rgba(37,99,235,0.04);
transition: box-shadow 0.2s, border-color 0.2s;
margin-bottom: 0.5rem;
}
.cost-breakdown .cost-item:hover {
border-color: var(--primary-color);
box-shadow: 0 4px 16px 0 rgba(37,99,235,0.10);
background: #f1f5fd;
}
.cost-breakdown .h4.text-primary {
font-weight: 700;
letter-spacing: 0.5px;
}
.cost-breakdown .small.text-muted {
font-size: 0.98rem;
color: var(--text-secondary);
}
.cost-breakdown .total-cost {
background: linear-gradient(90deg, #e0e7ef 0%, #f8fafc 100%);
border-width: 2.5px;
box-shadow: 0 2px 8px 0 rgba(37,99,235,0.10);
}
.cost-breakdown .total-cost span {
letter-spacing: 1.5px;
}
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="16" cy="16" r="16" fill="#2C3E50"/>
<!-- Calculator body -->
<rect x="8" y="6" width="16" height="20" rx="2" fill="#3498DB"/>
<!-- Calculator screen -->
<rect x="10" y="8" width="12" height="4" rx="1" fill="#ECF0F1"/>
<!-- Calculator buttons -->
<rect x="10" y="14" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="15" y="14" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="20" y="14" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="10" y="19" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="15" y="19" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="20" y="19" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="10" y="24" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="15" y="24" width="3" height="3" rx="1" fill="#ECF0F1"/>
<rect x="20" y="24" width="3" height="3" rx="1" fill="#ECF0F1"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
+2607
View File
File diff suppressed because one or more lines are too long
+638
View File
@@ -0,0 +1,638 @@
// static/js/mold_calculator.js
// Check if script is already initialized
if (window.moldCalculatorInitialized) {
console.log('Mold calculator already initialized, skipping...');
} else {
window.moldCalculatorInitialized = true;
// Wait for DOM to be fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeCalculator);
} else {
initializeCalculator();
}
}
function initializeCalculator() {
console.log('DOM Content Loaded - Initializing mold calculator');
// Get form elements
const form = document.getElementById('calcForm');
const mainSelect = document.getElementById('mold_main_type');
const subSelect = document.getElementById('mold_sub_type');
const specificTypeSelect = document.getElementById('mold_specific_type');
const stageSelect = document.getElementById('stage');
const typeSelect = document.getElementById('type');
const toolingIdInput = document.getElementById('tooling_id');
const submitButton = document.getElementById('calculateBtn');
console.log('Main select element:', mainSelect);
console.log('Sub select element:', subSelect);
console.log('Specific type select element:', specificTypeSelect);
console.log('Stage select element:', stageSelect);
console.log('Type select element:', typeSelect);
console.log('Tooling ID input:', toolingIdInput);
console.log('Submit button:', submitButton);
// Check if submit button exists
if (!submitButton) {
console.error('Submit button not found! Looking for element with ID "calculateBtn"');
// Try alternative selectors
const alternativeButton = form.querySelector('button[type="submit"]');
if (alternativeButton) {
console.log('Found submit button using alternative selector:', alternativeButton);
} else {
console.error('No submit button found with any selector!');
return; // Exit initialization if we can't find the submit button
}
}
// Define mappings
const moldTypeMapping = {
'Rubber': ['Flat Outsole', 'Cupsole', 'Rubber Sole'],
'CMEVA': ['2 Plates Mold', '3 Plates Mold', '2 Plates with in-mold channel', 'Breathable + in-mold channel'],
'IMEVA': ['2 Plates Mold', '3 Plates Mold'],
'Sandal': ['Sandal with Upper Bandage', '2 Plate without Upper Bandage'],
'Co-shot mold': ['Co-shot mold', 'Foaming mold'],
'PU': ['2 Plates Mold', '3 Plates Mold']
};
const specificTypeOptions = {
'Flat Outsole': ['Flat', 'Flat with Top/Heel Tip'],
'Cupsole': ['Top PL', 'Visible PL'],
'Rubber Sole': ['Top PL', 'Short PL (Heel/Forefoot)', 'Visible PL', 'Wave PL']
};
const stageTypeMapping = {
'CR0': ['Sample'],
'CR1': ['Sample'],
'CR2': ['Sample'],
'SMS': ['A Set'],
'commercialization': ['A Set'],
'production': ['Additional']
};
// Function to update sub-type options
function updateSubTypeOptions() {
console.log('Populating sub-types');
const selectedType = mainSelect.value;
console.log('Selected type:', selectedType);
// Clear current options
subSelect.innerHTML = '<option value="">Select Sub Type</option>';
if (selectedType && moldTypeMapping[selectedType]) {
console.log('Found mapping for type:', selectedType);
const options = moldTypeMapping[selectedType];
console.log('Adding options:', options);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
subSelect.appendChild(opt);
});
}
// Reset specific type
specificTypeSelect.innerHTML = '<option value="">Select Specific Type</option>';
specificTypeSelect.disabled = true;
}
// Function to update specific type options
function updateSpecificTypeOptions() {
const selectedSubType = subSelect.value;
console.log('Selected sub-type:', selectedSubType);
// Clear current options
specificTypeSelect.innerHTML = '<option value="">Select Specific Type</option>';
if (selectedSubType && specificTypeOptions[selectedSubType]) {
console.log('Found specific type options for:', selectedSubType);
const options = specificTypeOptions[selectedSubType];
console.log('Adding specific type options:', options);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
specificTypeSelect.appendChild(opt);
});
specificTypeSelect.disabled = false;
} else {
specificTypeSelect.disabled = true;
}
}
// Function to update type options based on stage
function updateTypeOptions() {
const selectedStage = stageSelect.value;
console.log('Selected stage:', selectedStage);
// Clear current options
typeSelect.innerHTML = '<option value="">Select Type</option>';
if (selectedStage && stageTypeMapping[selectedStage]) {
const options = stageTypeMapping[selectedStage];
console.log('Adding type options:', options);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
typeSelect.appendChild(opt);
});
// Enable the type select
typeSelect.disabled = false;
} else {
// Disable the type select if no stage is selected
typeSelect.disabled = true;
}
}
// Add event listeners
mainSelect.addEventListener('change', function() {
console.log('Main type changed:', this.value);
updateSubTypeOptions();
});
subSelect.addEventListener('change', function() {
console.log('Sub type changed:', this.value);
updateSpecificTypeOptions();
});
stageSelect.addEventListener('change', function() {
console.log('Stage changed:', this.value);
updateTypeOptions();
});
// Add tooling ID validation
if (toolingIdInput) {
console.log('Tooling ID input found, adding validation');
toolingIdInput.addEventListener('input', function(e) {
console.log('Tooling ID input event triggered, value:', this.value);
// Remove any non-numeric characters
let value = this.value.replace(/\D/g, '');
// Limit to 5 digits
if (value.length > 5) {
value = value.substring(0, 5);
}
this.value = value;
console.log('Tooling ID value after validation:', this.value);
});
toolingIdInput.addEventListener('paste', function(e) {
console.log('Tooling ID paste event triggered');
e.preventDefault();
const pastedText = (e.clipboardData || window.clipboardData).getData('text');
const numericOnly = pastedText.replace(/\D/g, '').substring(0, 5);
this.value = numericOnly;
console.log('Tooling ID value after paste validation:', this.value);
});
} else {
console.error('Tooling ID input element not found!');
// Try alternative selectors
const alternativeSelectors = [
'input[name="tooling_id"]',
'input[placeholder*="tooling"]',
'input[placeholder*="Tooling"]'
];
for (let selector of alternativeSelectors) {
const element = document.querySelector(selector);
if (element) {
console.log('Found tooling ID input using selector:', selector, element);
break;
}
}
}
// Initialize options
updateSubTypeOptions();
updateTypeOptions();
// Function to reset form
function resetForm(form) {
console.log('Resetting form...');
// Reset all select elements to their first option
const selects = form.querySelectorAll('select');
selects.forEach(select => {
select.selectedIndex = 0;
select.dispatchEvent(new Event('change'));
});
// Reset all input elements
const inputs = form.querySelectorAll('input');
inputs.forEach(input => {
input.value = '';
});
// Reset all checkboxes and radio buttons
const checkboxes = form.querySelectorAll('input[type="checkbox"], input[type="radio"]');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
// Disable dependent dropdowns
if (subSelect) subSelect.disabled = true;
if (specificTypeSelect) specificTypeSelect.disabled = true;
if (typeSelect) typeSelect.disabled = true;
// Show reset notification
const notification = document.createElement('div');
notification.className = 'alert alert-info alert-dismissible fade show mt-3';
notification.innerHTML = `
<i class="fas fa-info-circle me-2"></i>
Form has been reset. You can now create a new quotation.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
form.insertBefore(notification, form.firstChild);
console.log('Form reset complete');
}
// Handle form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
console.log('Form submitted');
// Get submit button robustly
let submitButton = document.getElementById('calculateBtn') || form.querySelector('button[type="submit"]');
if (!submitButton) {
console.error('Submit button not found during form submission!');
alert('Error: Submit button not found. Please refresh the page and try again.');
return;
}
// Check if submission is already in progress
if (submitButton.disabled || form.dataset.submitting === 'true') {
console.log('Form submission already in progress or cooldown active, ignoring duplicate click');
return;
}
// Set submission state
form.dataset.submitting = 'true';
// Disable submit button and show loading state
submitButton.disabled = true;
const originalButtonText = submitButton.innerHTML;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Calculating...';
submitButton.classList.add('btn-secondary');
submitButton.classList.remove('btn-primary');
submitButton.title = 'Please wait while calculating...';
try {
// Log all form fields
console.log('Form fields:');
for (let [key, value] of new FormData(form).entries()) {
console.log(`${key}: ${value}`);
}
// Validate required fields
const mainType = mainSelect.value;
const subType = subSelect.value;
const specificType = specificTypeSelect.value;
const stage = stageSelect.value;
const type = typeSelect.value;
const toolingId = toolingIdInput ? toolingIdInput.value : '';
console.log('Required fields:', {
mainType,
subType,
specificType,
stage,
type,
toolingId
});
if (!mainType) {
alert('Please select a main type');
return;
}
if (!subType) {
alert('Please select a sub type');
return;
}
if (!stage) {
alert('Please select a stage');
return;
}
if (!type) {
alert('Please select a type');
return;
}
// Validate tooling ID
if (!toolingId) {
alert('Please enter a Tooling ID');
return;
}
if (!/^\d{5}$/.test(toolingId)) {
alert('Tooling ID must be exactly 5 digits');
return;
}
// Check if specific type is required
const validSpecificTypes = specificTypeOptions[subType] || [];
if (validSpecificTypes.length > 0 && !specificType) {
alert('Please select a specific type');
return;
}
// Create form data
const formData = new FormData(form);
// Add CSRF token to form data
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
formData.append('csrf_token', csrfToken);
// Remove specific type if it's not needed
if (validSpecificTypes.length === 0) {
formData.delete('mold_specific_type');
}
// Log the final form data being sent
console.log('Final form data being sent:');
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
const response = await fetch('/calculate', {
method: 'POST',
body: formData
});
console.log('Response status:', response.status);
const responseText = await response.text();
console.log('Response text:', responseText);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, body: ${responseText}`);
}
const data = JSON.parse(responseText);
console.log('Calculation result:', data);
// Get the result element
const resultElement = document.getElementById('result');
// Update the result display
updateResultsDisplay(data);
// Save the quotation to the database
try {
console.log('Saving quotation to database...');
const saveResponse = await fetch('/save-quotation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data)
});
const saveResult = await saveResponse.json();
console.log('Save result:', saveResult);
if (saveResult.success) {
console.log('Quotation saved successfully with ID:', saveResult.quotation_id);
// Show success message
const successMessage = document.createElement('div');
successMessage.className = 'alert alert-success alert-dismissible fade show';
successMessage.innerHTML = `
<i class="fas fa-check-circle me-2"></i>
Quotation saved successfully! ID: ${saveResult.quotation_id}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
resultElement.insertBefore(successMessage, resultElement.firstChild);
// Reset form after successful save
resetForm(form);
// Refresh quote history
setTimeout(() => {
refreshQuoteHistory();
}, 1000);
} else if (saveResult.duplicate) {
console.log('Duplicate submission detected:', saveResult.message);
const duplicateMessage = document.createElement('div');
duplicateMessage.className = 'alert alert-warning alert-dismissible fade show';
duplicateMessage.innerHTML = `
<i class="fas fa-exclamation-triangle me-2"></i>
${saveResult.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
resultElement.insertBefore(duplicateMessage, resultElement.firstChild);
// Reset form even on duplicate to prevent repeated attempts
resetForm(form);
}
} catch (saveError) {
console.error('Error saving quotation:', saveError);
const errorMessage = document.createElement('div');
errorMessage.className = 'alert alert-danger alert-dismissible fade show';
errorMessage.innerHTML = `
<i class="fas fa-exclamation-circle me-2"></i>
Error saving quotation. Please try again later.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
resultElement.insertBefore(errorMessage, resultElement.firstChild);
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred while calculating. Please check the console for details.');
} finally {
// Re-enable submit button and restore original state after a delay
setTimeout(() => {
let submitButton = document.getElementById('calculateBtn') || form.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
submitButton.classList.remove('btn-secondary');
submitButton.classList.add('btn-primary');
submitButton.title = 'Calculate mold cost';
}
// Reset submission state
form.dataset.submitting = 'false';
}, 2000); // 2 second cooldown before allowing new submission
}
});
}
// Function to update results display
function updateResultsDisplay(data) {
const resultElement = document.getElementById('result');
if (!resultElement) {
console.error('Result element not found');
return;
}
// Format the results to match the provided screenshot
const formattedResults = `
<div class="card bg-white shadow-sm p-4 mb-4" style="border-radius: 1rem; border: 1px solid #e3e6ea;">
<div class="mb-4 pb-2 border-bottom d-flex align-items-center" style="font-size:1.4rem;font-weight:600;color:#1976d2;">
<i class="fas fa-clipboard-list me-2"></i>Calculation Results
</div>
<!-- Factory Information -->
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-industry me-2"></i>Factory Information
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="card p-3 h-100 bg-light border-0">
<div class="fw-bold">Mold Shop</div>
<div>${data.mold_shop_name || '-'}</div>
<div class="text-muted small">${data.mold_shop_country || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100 bg-light border-0">
<div class="fw-bold">T1 Factory</div>
<div>${data.t1_shoe_factory_name || '-'}</div>
<div class="text-muted small">${data.t1_shoe_factory_country || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100 bg-light border-0">
<div class="fw-bold">T2 Factory</div>
<div>${data.t2_component_factory_name || '-'}</div>
<div class="text-muted small">${data.t2_component_factory_country || '-'}</div>
</div>
</div>
</div>
</div>
<!-- Mold Information -->
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-cube me-2"></i>Mold Information
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="card p-3 h-100">
<div class="fw-bold">Model</div>
<div>${data.model_name || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100">
<div class="fw-bold">Tooling</div>
<div>${data.tooling_id || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100">
<div class="fw-bold">Season & Stage</div>
<div>${(data.season || '-') + '/' + (data.stage || '-')}</div>
</div>
</div>
</div>
</div>
<!-- Mold Specifications -->
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-wrench me-2"></i>Mold Specifications
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="card p-3 h-100">
<div class="fw-bold">Mold Type</div>
<div>${data.mold_main_type || '-'}</div>
<div class="text-muted small">${data.mold_sub_type || ''}</div>
</div>
</div>
<div class="col-md-6">
<div class="card p-3 h-100">
<div class="fw-bold mb-2">Complexity Factors</div>
<div class="row">
<div class="col-6 small">High Sidewall</div>
<div class="col-6 small text-end">${data.complexity_high_sidewall === 'yes' ? '<b>Yes</b>' : '-'}</div>
<div class="col-6 small">Sliders</div>
<div class="col-6 small text-end">${data.sliders_count ? `<b>${data.sliders_count} slider${data.sliders_count > 1 ? 's' : ''}</b>` : '-'}</div>
<div class="col-6 small">Cavities</div>
<div class="col-6 small text-end">${data.cavity_count ? `<b>${data.cavity_count} pair</b>` : '-'}</div>
<div class="col-6 small">Digital Texture</div>
<div class="col-6 small text-end">${data.digital_texture_type ? `<b>${data.digital_texture_type}</b>` : '-'}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Cost Breakdown & Approvals -->
<div class="row g-3">
<div class="col-md-8">
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-table me-2"></i>Cost Breakdown
</div>
<div class="card p-0">
<table class="table mb-0">
<tbody>
<tr><td>Base Price</td><td class="text-end">$${Number(data.base_price).toFixed(2)}</td></tr>
<tr><td>Complexity Adjustment</td><td class="text-end">$${Number(data.complexity_cost).toFixed(2)}</td></tr>
<tr><td>Digital Texture Cost</td><td class="text-end">$${Number(data.texture_cost).toFixed(2)}</td></tr>
<tr class="table-primary fw-bold"><td>Total Cost</td><td class="text-end">$${Number(data.total_cost).toFixed(2)}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-check-circle me-2"></i>Approvals
</div>
<div class="card p-3">
<div class="mb-3 text-muted small"><i class="far fa-calendar-alt me-1"></i>Issue Date: <b>${data.issue_date || '-'}</b></div>
<div class="mb-2">Mold Shop<br><b>${data.approved_by_mold_shop || '-'}</b></div>
<div class="mb-2">T1 Tooling<br><b>${data.approved_by_t1_tooling || '-'}</b></div>
<div>LO Tooling<br><b>${data.approved_by_lo_tooling || '-'}</b></div>
</div>
</div>
</div>
</div>
</div>
`;
resultElement.innerHTML = formattedResults;
}
// Helper function to format currency
function formatCurrency(value) {
if (value === null || value === undefined) {
return 'N/A';
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(value);
}
// Refresh quote history
async function refreshQuoteHistory() {
try {
const response = await fetch('/quote-history');
if (response.ok) {
const data = await response.json();
// Update quote history display
const historyContainer = document.getElementById('quote-history');
if (historyContainer) {
historyContainer.innerHTML = data.html;
}
}
} catch (error) {
console.error('Error refreshing quote history:', error);
}
}