13 Commits

Author SHA1 Message Date
Gan, Jimmy 49d983ba2a 🔧 FIX: Restore missing calculator files from src/ to app/ directory 2025-07-12 11:48:08 +08:00
Gan, Jimmy bc079ead05 RESTORE: Admin quotation detail template improvements
 Fixed admin quotation detail template structure:
   - Corrected block structure for proper inheritance
   - Updated to use Bootstrap Icons (bi-*) for consistency
   - Improved header layout with proper title and back button
   - Enhanced visual consistency across admin pages

CHANGES:
- Switch from FontAwesome to Bootstrap Icons in quotation templates
- Better template block organization
- Consistent header styling with other admin pages
- Improved navigation flow

SAFE: Only template improvements, no functionality changes
2025-07-11 13:36:33 +08:00
Gan, Jimmy ea5b4e5f18 🔐 RESTORE: Admin password reset functionality
 Added admin password reset capability with:
   - Generate secure temporary passwords (12 chars with symbols)
   - Create password reset links with 15-min expiry
   - Beautiful modal UI with copy-to-clipboard functionality
   - Toggle password visibility for security
   - Reset buttons in both users list and user detail pages
   - Proper error handling and loading states
   - Responsive design for mobile/desktop

FEATURES:
- Admins can reset any user's password instantly
- Users get both temporary password and reset link options
- Secure password generation with proper entropy
- Audit logging of all password reset actions
- Professional UI with Bootstrap modals and tooltips

SAFE: Only affects admin functionality, calculator unchanged
2025-07-11 13:36:05 +08:00
Gan, Jimmy fefe9b10c0 🎯 RESTORE: Enhanced admin user management functionality
 Added user search functionality to admin users page
 Created comprehensive user detail template with:
   - Complete user information display
   - User action buttons (activate/deactivate/delete)
   - Quotation history for each user
   - Improved tooltips and responsive design
 Enhanced admin users table with better action buttons
 Added proper error handling and confirmation dialogs

SAFE: Only affects admin pages, calculator functionality unchanged
2025-07-11 13:34:56 +08:00
Gan, Jimmy 2027c443d3 Rollback to much earlier stable commit (22249e30) + CDK version fix 2025-07-10 15:42:12 +08:00
Shang, Xiaomeng 22249e300a SECRET_KEY 2025-06-27 17:15:06 +02:00
Shang, Xiaomeng c4c6588ac5 UPDATE db 2025-06-27 15:10:04 +02:00
Shang, Xiaomeng 55bffe71f4 CDK 2025-06-27 14:52:21 +02:00
Gopinathan, Anup Raj d7a9e2a35b Pull request #4: Fix environment variables
Merge in DCF/mold_cost_calculator from Fix-environment-variables to master

* commit '5f4973ed116d4a503bd7a69d0712f616924d3196':
  add secret key
  Adding secret key to the env
2025-06-26 08:58:43 +00:00
Anup Raj Gopinathan 5f4973ed11 add secret key 2025-06-26 10:43:46 +02:00
Anup Raj Gopinathan f19bd324f2 Adding secret key to the env 2025-06-26 10:34:55 +02:00
Gopinathan, Anup Raj 0082e9d761 Pull request #3: Increase Memory
Merge in DCF/mold_cost_calculator from IncreaseMemory to master

* commit '3f878a5fbe48f16ebddc97656a565ba069132b74':
  Increase Memory
2025-06-26 04:25:09 +00:00
Anup Raj Gopinathan 3f878a5fbe Increase Memory 2025-06-26 06:24:02 +02:00
13 changed files with 1904 additions and 22 deletions
Vendored
+15 -4
View File
@@ -21,10 +21,17 @@ def mccDatabasePassword = [
'prd': 'MCC_PRD_DATABASE_PASSWORD', 'prd': 'MCC_PRD_DATABASE_PASSWORD',
] ]
def mccSecretKey = [
'poc': 'MCC_SECRET_KEY',
'dev': 'MCC_SECRET_KEY',
'uat': 'MCC_SECRET_KEY',
'prd': 'MCC_SECRET_KEY',
]
def mccDatabase = [ def mccDatabase = [
'poc': 'devmccmccdb-devmccmccdbwriterfe3d6e74-nzeetdanw6sx.c8uwkt51amck.eu-west-1.rds.amazonaws.com/mccdb', 'poc': 'devmccmccdb-devmccmccdbwriterfe3d6e74-nzeetdanw6sx.c8uwkt51amck.eu-west-1.rds.amazonaws.com/mccdb',
'dev': 'dev-mld-mccdb.cluster-c8uwkt51amck.eu-west-1.rds.amazonaws.com/mccdb', 'dev': 'dev-mld-mccdb.cluster-c8uwkt51amck.eu-west-1.rds.amazonaws.com/mccdb',
'uat': 'devmccmccdb-devmccmccdbwriterfe3d6e74-nzeetdanw6sx.c8uwkt51amck.eu-west-1.rds.amazonaws.com/mccdb', 'uat': 'uat-mld-mccdb.cluster-cuol4qg6qhxt.eu-west-1.rds.amazonaws.com/mccdb',
'prd': 'devmccmccdb-devmccmccdbwriterfe3d6e74-nzeetdanw6sx.c8uwkt51amck.eu-west-1.rds.amazonaws.com/mccdb', 'prd': 'devmccmccdb-devmccmccdbwriterfe3d6e74-nzeetdanw6sx.c8uwkt51amck.eu-west-1.rds.amazonaws.com/mccdb',
] ]
@@ -48,8 +55,8 @@ pipeline {
POSTGRES_PASSWORD_ENV = "${mccDatabasePassword[DEPLOY_ENV]}" POSTGRES_PASSWORD_ENV = "${mccDatabasePassword[DEPLOY_ENV]}"
FLASK_APP = 'run.py' FLASK_APP = 'run.py'
FLASK_ENV = 'production' FLASK_ENV = 'production'
DATABASE_URL = "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${mccDatabase[DEPLOY_ENV]}"
HOST_ENV = 'AWS' HOST_ENV = 'AWS'
SECRET_KEY_ENV = "${mccSecretKey[DEPLOY_ENV]}"
} }
agent { agent {
@@ -77,12 +84,16 @@ pipeline {
dir('infra') { dir('infra') {
withCredentials([ withCredentials([
string(credentialsId: "${POSTGRES_PASSWORD_ENV}", variable: 'POSTGRES_PASSWORD'), string(credentialsId: "${POSTGRES_PASSWORD_ENV}", variable: 'POSTGRES_PASSWORD'),
string(credentialsId: "${SECRET_KEY_ENV}", variable: 'SECRET_KEY'),
aws(credentialsId: credentialsIDs[env.DEPLOY_ENV], accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY') aws(credentialsId: credentialsIDs[env.DEPLOY_ENV], accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY')
]) { ]) {
script {
env.DATABASE_URL = "postgresql://${env.POSTGRES_USER}:${env.POSTGRES_PASSWORD}@${mccDatabase[env.DEPLOY_ENV]}"
}
withNvm { withNvm {
sh 'npm install' sh 'npm install'
sh "npx aws-cdk@2.1019.1 bootstrap aws://${env.CDK_DEPLOY_ACCOUNT}/${env.CDK_DEPLOY_REGION} --context config=${env.DEPLOY_ENV} --toolkit-stack-name ${env.DEPLOY_ENV}${env.STACK_NAME} --change-set-name changeset-${env.DEPLOY_ENV}-${currentBuild.number} --qualifier ${env.CDK_QUALIFIER}" sh "npx aws-cdk@2.1020.2 bootstrap aws://${env.CDK_DEPLOY_ACCOUNT}/${env.CDK_DEPLOY_REGION} --context config=${env.DEPLOY_ENV} --toolkit-stack-name ${env.DEPLOY_ENV}${env.STACK_NAME} --change-set-name changeset-${env.DEPLOY_ENV}-${currentBuild.number} --qualifier ${env.CDK_QUALIFIER}"
sh "npx aws-cdk@2.1019.1 deploy --all --context config=${env.DEPLOY_ENV} --toolkit-stack-name ${env.DEPLOY_ENV}${env.STACK_NAME} --change-set-name ${env.STACK_NAME}-changeset-${env.DEPLOY_ENV}-${currentBuild.number} --require-approval never" sh "npx aws-cdk@2.1020.2 deploy --all --context config=${env.DEPLOY_ENV} --toolkit-stack-name ${env.DEPLOY_ENV}${env.STACK_NAME} --change-set-name ${env.STACK_NAME}-changeset-${env.DEPLOY_ENV}-${currentBuild.number} --require-approval never"
} }
} }
} }
+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;
}
}
+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);
}
}
+215
View File
@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% block head %}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Mold Cost Calculator</h2>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-primary">
<i class="fas fa-cog me-2"></i>Admin Portal
</a>
{% endif %}
</div>
<form id="calcForm" class="row g-3" method="POST" action="/calculate">
{{ form.csrf_token }}
<!-- Factory Information -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Factory Information</h6>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.mold_shop_name.label(class="form-label") }}
{{ form.mold_shop_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.mold_shop_country.label(class="form-label") }}
{{ form.mold_shop_country(class="form-control") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.t1_shoe_factory_name.label(class="form-label") }}
{{ form.t1_shoe_factory_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.t1_shoe_factory_country.label(class="form-label") }}
{{ form.t1_shoe_factory_country(class="form-control") }}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
{{ form.t2_component_factory_name.label(class="form-label") }}
{{ form.t2_component_factory_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.t2_component_factory_country.label(class="form-label") }}
{{ form.t2_component_factory_country(class="form-control") }}
</div>
</div>
</div>
</div>
</div>
<!-- Mold Information -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Mold Information</h6>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.model_name.label(class="form-label") }}
{{ form.model_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.tooling_id.label(class="form-label") }}
{{ form.tooling_id(class="form-control") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="form-group">
{{ form.season.label(class="form-label") }}
{{ form.season(class="form-control", id="season") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.stage.label(class="form-label") }}
{{ form.stage(class="form-control", id="stage") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.type.label(class="form-label") }}
{{ form.type(class="form-control", id="type") }}
</div>
</div>
</div>
</div>
</div>
<!-- Mold Specifications -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Mold Specifications</h6>
<div class="row mb-3">
<div class="col-md-4">
<div class="form-group">
{{ form.mold_main_type.label(class="form-label") }}
{{ form.mold_main_type(class="form-control", id="mold_main_type") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.mold_sub_type.label(class="form-label") }}
{{ form.mold_sub_type(class="form-control", id="mold_sub_type") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.mold_specific_type.label(class="form-label") }}
{{ form.mold_specific_type(class="form-control", id="mold_specific_type") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.sliders_count.label(class="form-label") }}
{{ form.sliders_count(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.cavity_count.label(class="form-label") }}
{{ form.cavity_count(class="form-control") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.digital_texture_type.label(class="form-label") }}
{{ form.digital_texture_type(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.complexity_high_sidewall.label(class="form-label") }}
{{ form.complexity_high_sidewall(class="form-control") }}
</div>
</div>
</div>
</div>
</div>
<!-- Dates & Approvals -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Dates & Approvals</h6>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.issue_date.label(class="form-label") }}
{{ form.issue_date(class="form-control", type="date") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="form-group">
{{ form.approved_by_mold_shop.label(class="form-label") }}
{{ form.approved_by_mold_shop(class="form-control") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.approved_by_t1_tooling.label(class="form-label") }}
{{ form.approved_by_t1_tooling(class="form-control") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.approved_by_lo_tooling.label(class="form-label") }}
{{ form.approved_by_lo_tooling(class="form-control") }}
</div>
</div>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="text-center">
<button type="submit" id="calculateBtn" class="btn btn-primary btn-lg">Calculate</button>
</div>
</form>
<!-- Results Section -->
<div id="result" class="mt-4"></div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script id="mold-calculator-script" src="{{ url_for('static', filename='js/mold_calculator.js', v='1.4') }}"></script>
{% endblock %}
+56
View File
@@ -0,0 +1,56 @@
from ..utils.cost_factor_loader import get_cost_factors
def calculate_base_price(mold_main_type, mold_sub_type, mold_specific_type, sliders_count, cavity_count, digital_texture_type, complexity_high_sidewall=False):
"""
Calculate the base price for a mold based on various factors.
Args:
mold_main_type (str): The main type of the mold
mold_sub_type (str): The sub type of the mold
mold_specific_type (str): The specific type of the mold
sliders_count (int): Number of sliders
cavity_count (int): Number of cavities
digital_texture_type (str): Type of digital texture
complexity_high_sidewall (bool): Whether the mold has high sidewall complexity
Returns:
dict: A dictionary containing all cost components
"""
# Get cost factors from database (or fallback to defaults)
cost_factors, tax_rates, default_net_base = get_cost_factors()
# Start with the default net base
base_price = default_net_base
# Apply mold type factor
if mold_specific_type in cost_factors['mold']:
base_price *= cost_factors['mold'][mold_specific_type]
# Calculate complexity cost
complexity_cost = 0
# Add slider complexity
if sliders_count > 0:
complexity_cost += base_price * (sliders_count * cost_factors['complexity']['slider'])
# Add cavity complexity
if cavity_count > 1:
complexity_cost += base_price * ((cavity_count - 1) * cost_factors['complexity']['cavity'])
# Add high sidewall complexity
if complexity_high_sidewall:
complexity_cost += base_price * cost_factors['complexity']['high_sidewall']
# Calculate texture cost
texture_cost = 0
if digital_texture_type in cost_factors['texture']:
texture_cost = base_price * cost_factors['texture'][digital_texture_type]
# Calculate total cost
total_cost = base_price + complexity_cost + texture_cost
return {
'base_price': round(base_price, 2),
'complexity_cost': round(complexity_cost, 2),
'texture_cost': round(texture_cost, 2),
'total_cost': round(total_cost, 2)
}
+139
View File
@@ -0,0 +1,139 @@
from ..models import CostFactor
from .. import db
import logging
logger = logging.getLogger(__name__)
# Single source of truth for default cost factors
DEFAULT_COST_FACTORS = [
# Mold Type Factors
{'category': 'mold', 'name': 'Flat', 'factor_value': 1.0, 'description': 'Standard flat outsole'},
{'category': 'mold', 'name': 'Flat with Top/Heel Tip', 'factor_value': 1.25, 'description': 'Flat with additional tip features'},
{'category': 'mold', 'name': 'Top PL', 'factor_value': 1.05, 'description': 'Top part line mold'},
{'category': 'mold', 'name': 'Visible PL', 'factor_value': 1.28, 'description': 'Visible part line mold'},
{'category': 'mold', 'name': 'Short PL (Heel/Forefoot)', 'factor_value': 1.18, 'description': 'Short part line for specific areas'},
{'category': 'mold', 'name': 'Wave PL', 'factor_value': 1.32, 'description': 'Wave pattern part line'},
{'category': 'mold', 'name': 'Multi-color PL', 'factor_value': 1.35, 'description': 'Multi-color part line mold'},
{'category': 'mold', 'name': '2 Plates Mold', 'factor_value': 1.0, 'description': 'Standard 2-plate configuration'},
{'category': 'mold', 'name': '3 Plates Mold', 'factor_value': 1.35, 'description': 'Complex 3-plate configuration'},
{'category': 'mold', 'name': '2 Plates with in-mold channel', 'factor_value': 1.25, 'description': '2-plate with channel features'},
{'category': 'mold', 'name': 'Breathable + in-mold channel', 'factor_value': 1.45, 'description': 'Breathable with channel features'},
{'category': 'mold', 'name': 'Sandal with Upper Bandage', 'factor_value': 1.15, 'description': 'Sandal with bandage features'},
{'category': 'mold', 'name': '2 Plate without Upper Bandage', 'factor_value': 1.05, 'description': 'Standard 2-plate sandal'},
{'category': 'mold', 'name': 'Co-shot mold', 'factor_value': 1.25, 'description': 'Multi-material injection'},
{'category': 'mold', 'name': 'Foaming mold', 'factor_value': 1.4, 'description': 'Foam injection process'},
# Complexity Factors
{'category': 'complexity', 'name': 'high_sidewall', 'factor_value': 0.18, 'calculation_formula': 'Base Price × 0.18', 'description': 'Additional cost for high sidewall molds'},
{'category': 'complexity', 'name': 'slider', 'factor_value': 0.1, 'calculation_formula': 'Base Price × (Number of Sliders × 0.10)', 'description': 'Cost per slider added to mold'},
{'category': 'complexity', 'name': 'cavity', 'factor_value': 0.12, 'calculation_formula': 'Base Price × ((Number of Cavities - 1) × 0.12)', 'description': 'Cost for additional cavities beyond 1'},
# Texture Factors
{'category': 'texture', 'name': 'adidas_digital', 'factor_value': 0.0, 'percentage': '0%', 'description': 'No additional cost for standard adidas texture'},
{'category': 'texture', 'name': 'customized texture', 'factor_value': 0.15, 'percentage': '15%', 'description': '15% of base price for custom texture'},
{'category': 'texture', 'name': 'laser', 'factor_value': 0.2, 'percentage': '20%', 'description': '20% of base price for laser texture'},
# Tax Rates
{'category': 'tax', 'name': 'China', 'factor_value': 0.06, 'percentage': '6%', 'description': '6% VAT for China'},
{'category': 'tax', 'name': 'Vietnam', 'factor_value': 0.10, 'percentage': '10%', 'description': '10% VAT for Vietnam'},
{'category': 'tax', 'name': 'Indonesia', 'factor_value': 0.07, 'percentage': '7%', 'description': '7% VAT for Indonesia'},
# Base Price
{'category': 'base', 'name': 'DEFAULT_NET_BASE', 'factor_value': 12.5, 'description': 'Default base price for calculations'}
]
def load_cost_factors_from_db():
"""
Load cost factors from database and return them in the same format as constants.
Falls back to hardcoded values if database is not available.
"""
try:
# Get all active cost factors
factors = CostFactor.query.filter_by(is_active=True).all()
if not factors:
logger.warning("No cost factors found in database, using hardcoded defaults")
return get_default_cost_factors()
# Build the cost factors structure
cost_factors = {
'mold': {},
'complexity': {},
'texture': {}
}
tax_rates = {}
default_net_base = 12.5 # Default value
for factor in factors:
if factor.category == 'mold':
cost_factors['mold'][factor.name] = factor.factor_value
elif factor.category == 'complexity':
cost_factors['complexity'][factor.name] = factor.factor_value
elif factor.category == 'texture':
cost_factors['texture'][factor.name] = factor.factor_value
elif factor.category == 'tax':
tax_rates[factor.name] = factor.factor_value
elif factor.category == 'base':
default_net_base = factor.factor_value
logger.info(f"Loaded {len(factors)} cost factors from database")
return cost_factors, tax_rates, default_net_base
except Exception as e:
logger.error(f"Error loading cost factors from database: {str(e)}")
logger.info("Falling back to hardcoded cost factors")
return get_default_cost_factors()
def get_default_cost_factors():
"""Return hardcoded cost factors as fallback - derived from single source of truth"""
cost_factors = {
'mold': {},
'complexity': {},
'texture': {}
}
tax_rates = {}
default_net_base = 12.5
# Build the structure from the single source of truth
for factor_data in DEFAULT_COST_FACTORS:
category = factor_data['category']
name = factor_data['name']
value = factor_data['factor_value']
if category == 'mold':
cost_factors['mold'][name] = value
elif category == 'complexity':
cost_factors['complexity'][name] = value
elif category == 'texture':
cost_factors['texture'][name] = value
elif category == 'tax':
tax_rates[name] = value
elif category == 'base':
default_net_base = value
return cost_factors, tax_rates, default_net_base
def get_cost_factors():
"""Get current cost factors (from database or defaults)"""
return load_cost_factors_from_db()
def initialize_default_cost_factors():
"""Initialize the database with default cost factors if none exist"""
try:
if CostFactor.query.count() == 0:
logger.info("Initializing database with default cost factors")
# Use the single source of truth
for factor_data in DEFAULT_COST_FACTORS:
factor = CostFactor(**factor_data)
db.session.add(factor)
db.session.commit()
logger.info(f"Initialized {len(DEFAULT_COST_FACTORS)} default cost factors")
except Exception as e:
logger.error(f"Error initializing default cost factors: {str(e)}")
db.session.rollback()
raise
+3 -1
View File
@@ -61,7 +61,7 @@ export class MccAPIInfraStack extends Stack {
timeout: Duration.seconds(30), timeout: Duration.seconds(30),
entry: join(__dirname, '../../src'), entry: join(__dirname, '../../src'),
index: 'run.py', index: 'run.py',
memorySize: 256, memorySize: 1024,
vpc, vpc,
logRetention: RetentionDays.ONE_WEEK, logRetention: RetentionDays.ONE_WEEK,
environment: { environment: {
@@ -72,6 +72,7 @@ export class MccAPIInfraStack extends Stack {
FLASK_ENV: process.env.FLASK_ENV!, FLASK_ENV: process.env.FLASK_ENV!,
DATABASE_URL: process.env.DATABASE_URL!, DATABASE_URL: process.env.DATABASE_URL!,
HOST_ENV: process.env.HOST_ENV!, HOST_ENV: process.env.HOST_ENV!,
SECRET_KEY: process.env.SECRET_KEY!,
}, },
}); });
@@ -93,6 +94,7 @@ export class MccAPIInfraStack extends Stack {
FLASK_ENV: process.env.FLASK_ENV!, FLASK_ENV: process.env.FLASK_ENV!,
DATABASE_URL: process.env.DATABASE_URL!, DATABASE_URL: process.env.DATABASE_URL!,
HOST_ENV: process.env.HOST_ENV!, HOST_ENV: process.env.HOST_ENV!,
SECRET_KEY: process.env.SECRET_KEY!,
}, },
}); });
+14
View File
@@ -105,6 +105,20 @@
"TenantID": "3bfeb222-e42c-4535-aace-ea6f7751369b", "TenantID": "3bfeb222-e42c-4535-aace-ea6f7751369b",
"ClientID": "bc56643f-7002-4df9-ada5-1d79f55da6e3" "ClientID": "bc56643f-7002-4df9-ada5-1d79f55da6e3"
} }
},
"uat": {
"Account": "534745294648",
"Region": "eu-west-1",
"ApplicationName": "Mold Cost Calculator",
"ApplicationShortName": "mld",
"Environment": "uat",
"Version": "1.0.0",
"Parameters": {
"VPC": "vpc-0a0f5a76b45d40780",
"TenantID": "3bfeb222-e42c-4535-aace-ea6f7751369b",
"ClientID": "bc56643f-7002-4df9-ada5-1d79f55da6e3"
}
} }
} }
} }
+62 -2
View File
@@ -57,9 +57,23 @@ def dashboard():
def users(): def users():
try: try:
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
search = request.args.get('search', '').strip()
per_page = 20 per_page = 20
users = User.query.order_by(User.created_at.desc()).paginate( # Build query with search functionality
query = User.query
if search:
# Search by username or email (case-insensitive)
search_filter = f"%{search.lower()}%"
query = query.filter(
db.or_(
User.username.ilike(search_filter),
User.email.ilike(search_filter)
)
)
users = query.order_by(User.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False page=page, per_page=per_page, error_out=False
) )
@@ -229,7 +243,7 @@ def delete_quotation():
if not request.is_json: if not request.is_json:
return jsonify({'error': 'Invalid request format'}), 400 return jsonify({'error': 'Invalid request format'}), 400
data = request.get_json() data = request.get_json()
quotation_id = data.get('quotation_id') quotation_id = data.get('quotation_id')
if not quotation_id: if not quotation_id:
@@ -464,6 +478,52 @@ def reset_cost_factors():
flash('An error occurred while resetting cost factors.', 'error') flash('An error occurred while resetting cost factors.', 'error')
return redirect(url_for('admin.cost_factors')) return redirect(url_for('admin.cost_factors'))
@bp.route('/user/<int:user_id>/reset-password', methods=['POST'])
@login_required
@limiter.limit("10 per minute")
def admin_reset_password(user_id):
"""Admin function to reset a user's password"""
try:
user = User.query.get_or_404(user_id)
# Generate a temporary password
import secrets
import string
# Generate a secure temporary password (12 characters with mix of letters, numbers, symbols)
temp_password = ''.join(secrets.choice(string.ascii_letters + string.digits + '!@#$%^&*') for _ in range(12))
# Set the new password
user.set_password(temp_password)
# Generate a reset token for the user (optional - they can use this to set their own password)
reset_token = user.generate_reset_token()
db.session.commit()
# Log the password reset
current_app.logger.info(f'Admin {current_user.email} reset password for user {user.email}')
# Create reset URL
reset_url = url_for('auth.reset_password', token=reset_token, _external=True)
return jsonify({
'status': 'success',
'message': 'Password reset successfully',
'temporary_password': temp_password,
'reset_url': reset_url,
'username': user.username,
'email': user.email
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Admin password reset error: {str(e)}')
return jsonify({
'status': 'error',
'message': 'An error occurred while resetting the password'
}), 500
@bp.route('/debug-endpoints') @bp.route('/debug-endpoints')
def debug_endpoints(): def debug_endpoints():
from flask import current_app from flask import current_app
+2 -2
View File
@@ -121,10 +121,10 @@
{% if show_actions %} {% if show_actions %}
<div class="detail-actions"> <div class="detail-actions">
<a href="{{ url_for('admin.quotations') }}" class="btn btn-secondary"> <a href="{{ url_for('admin.quotations') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Quotations <i class="bi bi-arrow-left"></i> Back to Quotations
</a> </a>
<button class="btn btn-primary" onclick="window.print()"> <button class="btn btn-primary" onclick="window.print()">
<i class="fas fa-print"></i> Print Quotation <i class="bi bi-printer"></i> Print Quotation
</button> </button>
</div> </div>
{% endif %} {% endif %}
+11 -9
View File
@@ -1,16 +1,18 @@
{% extends "admin/base.html" %} {% extends "admin/base.html" %}
{% block admin_title %}Quotation Details{% endblock %} {% block title %}Quotation Details - Admin Panel{% endblock %}
{% block admin_header %}Quotation Details{% endblock %} {% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="bi bi-file-earmark-text me-2 text-primary"></i>
Quotation Details #{{ quotation.id }}
</h2>
<a href="{{ url_for('admin.quotations') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i>Back to Quotations
</a>
</div>
{% block admin_actions %}
<a href="{{ url_for('admin.quotations') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Quotations
</a>
{% endblock %}
{% block admin_content %}
{% from "_quotation_details.html" import render_quotation_details %} {% from "_quotation_details.html" import render_quotation_details %}
{{ render_quotation_details(quotation) }} {{ render_quotation_details(quotation) }}
{% endblock %} {% endblock %}
+441 -1
View File
@@ -1 +1,441 @@
{% extends "admin/base.html" %}
{% block title %}User Details - {{ user.username }} - Admin Panel{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-user me-2 text-primary"></i>
User Details: {{ user.username }}
</h2>
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to Users
</a>
</div>
<!-- User Information Card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-transparent border-0">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle me-2 text-primary"></i>
User Information
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label fw-bold">User ID:</label>
<span class="badge bg-secondary">#{{ user.id }}</span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Username:</label>
<span class="text-muted">{{ user.username }}</span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Email:</label>
<span class="text-muted">{{ user.email }}</span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Company:</label>
<span class="text-muted">{{ user.company if user.company else 'Not specified' }}</span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label fw-bold">Status:</label>
<span class="badge bg-{{ 'success' if user.is_active else 'danger' }}">
<i class="fas fa-{{ 'check-circle' if user.is_active else 'times-circle' }} me-1"></i>
{{ 'Active' if user.is_active else 'Inactive' }}
</span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Role:</label>
<span class="badge bg-{{ 'primary' if user.is_admin else 'secondary' }}">
<i class="fas fa-{{ 'crown' if user.is_admin else 'user' }} me-1"></i>
{{ 'Admin' if user.is_admin else 'User' }}
</span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Last Login:</label>
<span class="text-muted">
{{ user.last_login.strftime('%B %d, %Y at %H:%M') if user.last_login else 'Never' }}
</span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Account Created:</label>
<span class="text-muted">{{ user.created_at.strftime('%B %d, %Y at %H:%M') }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- User Actions Card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-transparent border-0">
<h5 class="card-title mb-0">
<i class="fas fa-cogs me-2 text-primary"></i>
User Actions
</h5>
</div>
<div class="card-body">
<div class="btn-group" role="group">
<button type="button"
class="btn btn-outline-secondary reset-password-btn"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}"
data-email="{{ user.email }}"
title="Reset User Password">
<i class="fas fa-key me-2"></i>Reset Password
</button>
<button type="button"
class="btn btn-{{ 'warning' if user.is_active else 'success' }} toggle-user-btn"
data-user-id="{{ user.id }}"
data-is-active="{{ user.is_active|tojson }}"
title="{{ 'Deactivate' if user.is_active else 'Activate' }} User">
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }} me-2"></i>
{{ 'Deactivate' if user.is_active else 'Activate' }} User
</button>
{% if user.id != current_user.id %}
<button type="button"
class="btn btn-outline-danger delete-user-btn"
data-user-id="{{ user.id }}"
title="Delete User">
<i class="fas fa-trash me-2"></i>Delete User
</button>
{% endif %}
</div>
</div>
</div>
<!-- Quotations History Card -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent border-0">
<h5 class="card-title mb-0">
<i class="fas fa-file-invoice me-2 text-primary"></i>
Quotation History ({{ quotations|length }} total)
</h5>
</div>
<div class="card-body p-0">
{% if quotations %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">ID</th>
<th class="border-0">Project Name</th>
<th class="border-0">Mold Type</th>
<th class="border-0">Country</th>
<th class="border-0">Total Cost</th>
<th class="border-0">Created</th>
<th class="border-0 text-center">Actions</th>
</tr>
</thead>
<tbody>
{% for quotation in quotations %}
<tr>
<td>
<span class="badge bg-info">#{{ quotation.id }}</span>
</td>
<td>
<span class="fw-medium">{{ quotation.project_name or 'Unnamed Project' }}</span>
</td>
<td>
<span class="text-muted">{{ quotation.mold_main_type or 'Not specified' }}</span>
</td>
<td>
<span class="text-muted">{{ quotation.mold_shop_country or 'Not specified' }}</span>
</td>
<td>
<span class="fw-bold text-success">
${{ "{:,.2f}".format(quotation.total_cost) if quotation.total_cost else 'N/A' }}
</span>
</td>
<td>
<small class="text-muted">
{{ quotation.created_at.strftime('%m/%d/%Y %H:%M') }}
</small>
</td>
<td class="text-center">
<a href="{{ url_for('admin.quotation_detail', id=quotation.id) }}"
class="btn btn-outline-info btn-sm"
title="View Quotation Details">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-file-invoice fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No quotations found</h5>
<p class="text-muted">This user hasn't created any quotations yet.</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Toggle user status
document.querySelectorAll('.toggle-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.dataset.userId;
const isActive = this.dataset.isActive === 'true';
const action = isActive ? 'deactivate' : 'activate';
if (!confirm(`Are you sure you want to ${action} this user?`)) {
return;
}
fetch(`/admin/user/${userId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while updating user status');
});
});
});
// Delete user
document.querySelectorAll('.delete-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.dataset.userId;
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
return;
}
fetch('/admin/delete_user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({ user_id: parseInt(userId) })
})
.then(response => response.json())
.then(data => {
if (data.message) {
alert('User deleted successfully');
window.location.href = '/admin/users';
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the user');
});
});
});
// Reset password functionality
document.querySelectorAll('.reset-password-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.dataset.userId;
const username = this.dataset.username;
const email = this.dataset.email;
if (!confirm(`Are you sure you want to reset the password for user "${username}"?\n\nThis will generate a new temporary password and invalidate their current password.`)) {
return;
}
// Show loading state
const originalHtml = this.innerHTML;
this.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Resetting Password...';
this.disabled = true;
fetch(`/admin/user/${userId}/reset-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showPasswordResetModal(data);
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while resetting the password');
})
.finally(() => {
// Restore button state
this.innerHTML = originalHtml;
this.disabled = false;
});
});
});
});
function showPasswordResetModal(data) {
const modalHtml = `
<div class="modal fade" id="passwordResetModal" tabindex="-1" aria-labelledby="passwordResetModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="passwordResetModalLabel">
<i class="fas fa-check-circle me-2"></i>Password Reset Successful
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-success">
<i class="fas fa-info-circle me-2"></i>
Password has been reset for user <strong>${data.username}</strong> (${data.email})
</div>
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-key me-2"></i>Temporary Password</h6>
</div>
<div class="card-body">
<div class="input-group">
<input type="password" class="form-control" id="tempPassword" value="${data.temporary_password}" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility()">
<i class="fas fa-eye" id="passwordToggleIcon"></i>
</button>
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('tempPassword')">
<i class="fas fa-copy"></i> Copy
</button>
</div>
<small class="text-muted">The user can log in with this temporary password and should change it immediately.</small>
</div>
</div>
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-link me-2"></i>Password Reset Link</h6>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" id="resetUrl" value="${data.reset_url}" readonly>
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('resetUrl')">
<i class="fas fa-copy"></i> Copy
</button>
</div>
<small class="text-muted">Send this link to the user so they can set their own password. Link expires in 15 minutes.</small>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
`;
// Remove existing modal if present
const existingModal = document.getElementById('passwordResetModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Show modal
const modal = new bootstrap.Modal(document.getElementById('passwordResetModal'));
modal.show();
// Clean up modal after it's hidden
document.getElementById('passwordResetModal').addEventListener('hidden.bs.modal', function () {
this.remove();
});
}
function togglePasswordVisibility() {
const passwordField = document.getElementById('tempPassword');
const toggleIcon = document.getElementById('passwordToggleIcon');
if (passwordField.type === 'password') {
passwordField.type = 'text';
toggleIcon.className = 'fas fa-eye-slash';
} else {
passwordField.type = 'password';
toggleIcon.className = 'fas fa-eye';
}
}
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
element.select();
element.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(element.value).then(function() {
// Show temporary success feedback
const button = event.target.closest('button');
const originalHtml = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i> Copied!';
button.classList.add('btn-success');
button.classList.remove('btn-outline-primary');
setTimeout(() => {
button.innerHTML = originalHtml;
button.classList.remove('btn-success');
button.classList.add('btn-outline-primary');
}, 2000);
}).catch(function(err) {
console.error('Could not copy text: ', err);
alert('Could not copy to clipboard');
});
}
</script>
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.form-label {
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.card-title {
font-size: 1.1rem;
}
.btn-group .btn {
margin-right: 0.5rem;
}
.table th {
font-weight: 600;
color: #495057;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
vertical-align: middle;
}
</style>
{% endblock %}
+217 -3
View File
@@ -85,21 +85,37 @@
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}" <a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="btn btn-outline-info btn-sm" class="btn btn-outline-info btn-sm"
title="View Details"> title="View User Details"
data-bs-toggle="tooltip">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
<span class="d-none d-lg-inline ms-1">View</span>
</a> </a>
<button type="button"
class="btn btn-outline-secondary btn-sm reset-password-btn"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}"
data-email="{{ user.email }}"
title="Reset User Password"
data-bs-toggle="tooltip">
<i class="fas fa-key"></i>
<span class="d-none d-lg-inline ms-1">Reset</span>
</button>
<button type="button" <button type="button"
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm" class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')" onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')"
title="{{ 'Deactivate' if user.is_active else 'Activate' }} User"> title="{{ 'Deactivate User' if user.is_active else 'Activate User' }}"
data-bs-toggle="tooltip">
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i> <i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i>
<span class="d-none d-lg-inline ms-1">{{ 'Deactivate' if user.is_active else 'Activate' }}</span>
</button> </button>
{% if user.id != current_user.id %} {% if user.id != current_user.id %}
<button type="button" <button type="button"
class="btn btn-outline-danger btn-sm" class="btn btn-outline-danger btn-sm"
onclick="deleteUser({{ user.id }})" onclick="deleteUser({{ user.id }})"
title="Delete User"> title="Delete User Permanently"
data-bs-toggle="tooltip">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
<span class="d-none d-lg-inline ms-1">Delete</span>
</button> </button>
{% endif %} {% endif %}
</div> </div>
@@ -164,6 +180,171 @@
{% block scripts %} {% block scripts %}
{{ super() }} {{ super() }}
<script> <script>
// Initialize tooltips
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
// Reset password functionality
document.querySelectorAll('.reset-password-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.dataset.userId;
const username = this.dataset.username;
const email = this.dataset.email;
if (!confirm(`Are you sure you want to reset the password for user "${username}"?\n\nThis will generate a new temporary password and invalidate their current password.`)) {
return;
}
// Show loading state
const originalHtml = this.innerHTML;
this.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span class="d-none d-lg-inline ms-1">Resetting...</span>';
this.disabled = true;
fetch(`/admin/user/${userId}/reset-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
showPasswordResetModal(data);
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while resetting the password');
})
.finally(() => {
// Restore button state
this.innerHTML = originalHtml;
this.disabled = false;
});
});
});
});
function showPasswordResetModal(data) {
const modalHtml = `
<div class="modal fade" id="passwordResetModal" tabindex="-1" aria-labelledby="passwordResetModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="passwordResetModalLabel">
<i class="fas fa-check-circle me-2"></i>Password Reset Successful
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-success">
<i class="fas fa-info-circle me-2"></i>
Password has been reset for user <strong>${data.username}</strong> (${data.email})
</div>
<div class="card mb-3">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-key me-2"></i>Temporary Password</h6>
</div>
<div class="card-body">
<div class="input-group">
<input type="password" class="form-control" id="tempPassword" value="${data.temporary_password}" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="togglePasswordVisibility()">
<i class="fas fa-eye" id="passwordToggleIcon"></i>
</button>
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('tempPassword')">
<i class="fas fa-copy"></i> Copy
</button>
</div>
<small class="text-muted">The user can log in with this temporary password and should change it immediately.</small>
</div>
</div>
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="fas fa-link me-2"></i>Password Reset Link</h6>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" id="resetUrl" value="${data.reset_url}" readonly>
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('resetUrl')">
<i class="fas fa-copy"></i> Copy
</button>
</div>
<small class="text-muted">Send this link to the user so they can set their own password. Link expires in 15 minutes.</small>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
`;
// Remove existing modal if present
const existingModal = document.getElementById('passwordResetModal');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Show modal
const modal = new bootstrap.Modal(document.getElementById('passwordResetModal'));
modal.show();
// Clean up modal after it's hidden
document.getElementById('passwordResetModal').addEventListener('hidden.bs.modal', function () {
this.remove();
});
}
function togglePasswordVisibility() {
const passwordField = document.getElementById('tempPassword');
const toggleIcon = document.getElementById('passwordToggleIcon');
if (passwordField.type === 'password') {
passwordField.type = 'text';
toggleIcon.className = 'fas fa-eye-slash';
} else {
passwordField.type = 'password';
toggleIcon.className = 'fas fa-eye';
}
}
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
element.select();
element.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(element.value).then(function() {
// Show temporary success feedback
const button = event.target.closest('button');
const originalHtml = button.innerHTML;
button.innerHTML = '<i class="fas fa-check"></i> Copied!';
button.classList.add('btn-success');
button.classList.remove('btn-outline-primary');
setTimeout(() => {
button.innerHTML = originalHtml;
button.classList.remove('btn-success');
button.classList.add('btn-outline-primary');
}, 2000);
}).catch(function(err) {
console.error('Could not copy text: ', err);
alert('Could not copy to clipboard');
});
}
function toggleUserStatus(userId, status) { function toggleUserStatus(userId, status) {
const button = event.target.closest('button'); const button = event.target.closest('button');
const isActive = status === 'warning'; const isActive = status === 'warning';
@@ -249,5 +430,38 @@ function deleteUser(userId) {
.btn-group .btn:last-child { .btn-group .btn:last-child {
margin-right: 0; margin-right: 0;
} }
/* Action buttons responsiveness */
@media (max-width: 1199.98px) {
.btn-group .btn span {
display: none !important;
}
.btn-group .btn {
padding: 0.25rem 0.5rem;
min-width: 32px;
}
}
/* Reset password button styling */
.reset-password-btn:hover {
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
/* Modal improvements */
.modal-lg {
max-width: 800px;
}
.input-group .form-control {
font-family: 'Courier New', monospace;
font-size: 0.9rem;
}
.alert-success {
border-left: 4px solid #28a745;
}
</style> </style>
{% endblock %} {% endblock %}