Restructure the app folder

This commit is contained in:
Anup Raj Gopinathan
2025-06-24 15:03:16 +02:00
parent 9b84cee5be
commit 370dbc96bf
106 changed files with 0 additions and 0 deletions
-124
View File
@@ -1,124 +0,0 @@
from flask import Flask
import os
import logging
from datetime import datetime
from sqlalchemy.exc import SQLAlchemyError, OperationalError
from sqlalchemy import inspect
from time import sleep
from .config import get_config
from .logging_config import configure_logging
from dotenv import load_dotenv
from .extensions import db, login_manager, migrate, csrf, limiter, cache, mail
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_caching import Cache
# Load environment variables first
load_dotenv()
db = SQLAlchemy()
migrate = Migrate()
limiter = Limiter(key_func=get_remote_address)
cache = Cache()
def create_app(config_class=None):
app = Flask(__name__, static_folder='static')
@app.context_processor
def inject_now():
return {'now': datetime.utcnow()}
# Load configuration
if config_class is None:
config_class = get_config()
app.config.from_object(config_class)
# Initialize configuration
config_class.init_app(app)
# Ensure secret key is properly set
if not app.config.get('SECRET_KEY'):
raise ValueError("SECRET_KEY must be set in the configuration")
app.config['SECRET_KEY'] = app.config['SECRET_KEY'].encode('utf-8')
# Ensure instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# Configure logging
configure_logging(app)
# Initialize extensions with retry logic
max_retries = 3
retry_delay = 5
# Initialize extensions only if they haven't been initialized
if not hasattr(app, 'extensions') or 'sqlalchemy' not in app.extensions:
for attempt in range(max_retries):
try:
# Initialize SQLAlchemy only if not already initialized
if 'sqlalchemy' not in app.extensions:
db.init_app(app)
migrate.init_app(app, db)
# Initialize other extensions
if 'csrf' not in app.extensions:
csrf.init_app(app)
if 'limiter' not in app.extensions:
limiter.init_app(app)
if 'cache' not in app.extensions:
cache.init_app(app)
if 'mail' not in app.extensions:
mail.init_app(app)
if 'login_manager' not in app.extensions:
login_manager.init_app(app)
# Test database connection
with app.app_context():
db.engine.connect()
break
except OperationalError as e:
if attempt == max_retries - 1:
app.logger.error(f"Failed to connect to database after {max_retries} attempts: {str(e)}")
raise
app.logger.warning(f"Database connection attempt {attempt + 1} failed: {str(e)}")
sleep(retry_delay)
# Configure login manager
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'warning'
@login_manager.user_loader
def load_user(user_id):
from .models import User
return User.query.get(int(user_id))
# Register blueprints
from .routes import main, auth, admin
app.register_blueprint(main.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(admin.bp)
# Register error handlers
from .errors import register_error_handlers
register_error_handlers(app)
# Debug: log all registered endpoints at startup (warning level and flush)
app.logger.warning("Registered endpoints (debug):")
for rule in app.url_map.iter_rules():
app.logger.warning(f"{rule.endpoint} {rule.methods} {rule.rule}")
for handler in app.logger.handlers:
handler.flush()
# Debug: write all registered endpoints to a file
with open('logs/endpoints.log', 'w') as f:
for rule in app.url_map.iter_rules():
f.write(f"{rule.endpoint} {rule.methods} {rule.rule}\n")
return app
-227
View File
@@ -1,227 +0,0 @@
import os
from pathlib import Path
from datetime import timedelta
import logging
from logging.handlers import RotatingFileHandler
# Base directories
BASE_DIR = Path(__file__).parent.parent
INSTANCE_DIR = BASE_DIR / 'instance'
# Default values
DEFAULT_NET_BASE = 12.5
# Allowed emails for registration
ALLOWED_EMAILS = {
'jenny.tay@adidas.com',
'gema.yostisiano@adidas.com',
'dirgasapto.nugratama@adidas.com',
'test@example.com', # Original test email
'test1@example.com',
'test2@example.com',
'test3@example.com',
'test4@example.com',
'test5@example.com',
'test6@example.com',
'test7@example.com',
'test8@example.com',
'test9@example.com',
'test10@example.com'
}
# Tax rates by country
TAX_RATES = {
'China': 0.06,
'Vietnam': 0.10,
'Indonesia': 0.07
}
# Cost factors for calculations
COST_FACTORS = {
'mold': {
'Flat & Thin': 1.0,
'Flat & Thick': 1.25,
'Flat with Top/Heat Tip': 1.35,
'Top PL': 1.05,
'Short PL (Heat/Forefoot)': 1.18,
'Visible PL': 1.28,
'Wave PL': 1.32,
'2 Plates Mold': 1.0,
'3 Plates Mold': 1.35,
'Breathable + In-mold channel': 1.45,
'With Upper Bandage': 1.15,
'2 plate without Bandage': 1.05,
'Co-shot mold': 1.25,
'Foaming mold': 1.4
},
'complexity': {
'high_sidewall': 0.18,
'slider': 0.1,
'cavity': 0.12
},
'texture': {
'standard': 0.0,
'custom': 0.15,
'laser': 0.2
}
}
class Config:
"""Base configuration class."""
# Flask configuration
SECRET_KEY = os.getenv('SECRET_KEY') # Get SECRET_KEY directly from environment
FLASK_APP = 'app'
FLASK_ENV = 'development'
FLASK_DEBUG = True
# Security configuration
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
PERMANENT_SESSION_LIFETIME = timedelta(days=7)
REMEMBER_COOKIE_DURATION = timedelta(days=30)
REMEMBER_COOKIE_SECURE = True
REMEMBER_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_SAMESITE = 'Lax'
# Database configuration
SQLALCHEMY_DATABASE_URI = None # Will be set from environment variable
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_size': 10,
'pool_recycle': 3600,
'pool_pre_ping': True
}
# Email configuration
MAIL_SERVER = None # Will be set from environment variable
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = None # Will be set from environment variable
MAIL_PASSWORD = None # Will be set from environment variable
MAIL_DEFAULT_SENDER = None # Will be set from environment variable
# Admin account configuration
ADMIN_EMAIL = None # Will be set from environment variable
ADMIN_PASSWORD = None # Will be set from environment variable
# Logging configuration
LOG_LEVEL = 'INFO'
LOG_TO_STDOUT = True
LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
# Application config
ALLOWED_EMAILS = ALLOWED_EMAILS
TAX_RATES = TAX_RATES
DEFAULT_NET_BASE = DEFAULT_NET_BASE
WTF_CSRF_ENABLED = True
# Rate Limiting Configuration
RATELIMIT_DEFAULT = "200 per day;50 per hour;10 per minute"
RATELIMIT_STORAGE_URL = "memory://"
RATELIMIT_STRATEGY = "fixed-window"
RATELIMIT_HEADERS_ENABLED = True
RATELIMIT_HEADERS_RESET = True
RATELIMIT_HEADERS_RETRY_AFTER = True
# Cache Configuration
CACHE_TYPE = "SimpleCache"
CACHE_DEFAULT_TIMEOUT = 300
CACHE_THRESHOLD = 1000
@classmethod
def init_app(cls, app):
"""Initialize application with configuration."""
# Load configuration from environment variables
db_user = os.getenv('POSTGRES_USER')
db_password = os.getenv('POSTGRES_PASSWORD')
db_host = os.getenv('DATABASE_HOST')
db_name = os.getenv('POSTGRES_DB')
if db_user and db_password and db_host and db_name:
cls.SQLALCHEMY_DATABASE_URI = f"postgresql://{db_user}:{db_password}@{db_host}/{db_name}"
else:
cls.SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL') # Fallback for other setups
cls.MAIL_SERVER = os.getenv('MAIL_SERVER')
cls.MAIL_USERNAME = os.getenv('MAIL_USERNAME')
cls.MAIL_PASSWORD = os.getenv('MAIL_PASSWORD')
cls.MAIL_DEFAULT_SENDER = os.getenv('MAIL_DEFAULT_SENDER')
cls.ADMIN_EMAIL = os.getenv('ADMIN_EMAIL')
cls.ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
cls.LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
cls.LOG_TO_STDOUT = os.getenv('LOG_TO_STDOUT', 'True').lower() == 'true'
# Ensure SECRET_KEY is set
if not cls.SECRET_KEY:
raise ValueError("SECRET_KEY environment variable is not set")
# Set SQLALCHEMY_DATABASE_URI on the app config
if not cls.SQLALCHEMY_DATABASE_URI:
raise ValueError("Database connection URI is not set. Please check DATABASE_URL or its components.")
app.config['SQLALCHEMY_DATABASE_URI'] = cls.SQLALCHEMY_DATABASE_URI
# Configure logging
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/app.log',
maxBytes=10240,
backupCount=10)
file_handler.setFormatter(logging.Formatter(cls.LOG_FORMAT))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Application startup')
class DevelopmentConfig(Config):
DEBUG = True
TESTING = False
REMEMBER_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
SQLALCHEMY_ECHO = True
LOG_LEVEL = 'DEBUG'
WTF_CSRF_ENABLED = True
# Development-specific settings
RATELIMIT_DEFAULT = "1000 per day;100 per hour;20 per minute"
CACHE_TYPE = "SimpleCache"
class ProductionConfig(Config):
DEBUG = False
TESTING = False
REMEMBER_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SQLALCHEMY_ECHO = False
LOG_LEVEL = 'INFO'
WTF_CSRF_ENABLED = True
# Production-specific settings
CACHE_TYPE = "SimpleCache"
CACHE_DEFAULT_TIMEOUT = 300
class TestingConfig(Config):
TESTING = True
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
WTF_CSRF_ENABLED = False
LOG_LEVEL = 'DEBUG'
# Testing-specific settings
SECRET_KEY = 'testing-secret-key-not-used-in-production'
MAIL_SUPPRESS_SEND = True
RATELIMIT_ENABLED = False
RATELIMIT_DEFAULT = "1000 per day;100 per hour;20 per minute"
CACHE_TYPE = "SimpleCache"
SQLALCHEMY_ENGINE_OPTIONS = {}
def get_config():
"""Get the appropriate configuration class based on environment."""
config_name = os.getenv('FLASK_CONFIG', 'development')
config_map = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig
}
return config_map.get(config_name, DevelopmentConfig)
-86
View File
@@ -1,86 +0,0 @@
from flask import render_template, jsonify, request, send_from_directory, current_app
import logging
import os
from sqlalchemy.exc import SQLAlchemyError, OperationalError, IntegrityError
logger = logging.getLogger(__name__)
def register_error_handlers(app):
@app.errorhandler(400)
def handle_400_error(e):
logger.error(f'400 error: {str(e)}')
if request.path.startswith('/calculate'):
return jsonify({'error': 'Invalid request parameters'}), 400
return render_template('errors/400.html'), 400
@app.errorhandler(403)
def handle_403_error(e):
logger.error(f'403 error: {str(e)}')
if request.path.startswith('/calculate'):
return jsonify({'error': 'Access forbidden'}), 403
return render_template('errors/403.html'), 403
@app.errorhandler(404)
def handle_404_error(e):
logger.error(f'404 error: {str(e)}')
if request.path.startswith('/calculate'):
return jsonify({'error': 'Resource not found'}), 404
if request.path.startswith('/static/'):
app.logger.warning(f'Static file not found: {request.path}')
return '', 404
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def handle_500_error(e):
logger.error(f'500 error: {str(e)}')
app.logger.error(f'Server error: {str(e)}')
if request.path.startswith('/calculate'):
return jsonify({'error': 'Internal server error'}), 500
if request.path.startswith('/static/'):
app.logger.error(f'Error serving static file {request.path}: {str(e)}')
return '', 404
return render_template('errors/500.html'), 500
@app.errorhandler(OperationalError)
def handle_db_operational_error(error):
logger.error(f'Database operational error: {str(error)}', exc_info=True)
if request.path.startswith('/api/'):
return jsonify({
'error': 'Database connection error',
'message': 'Unable to connect to the database. Please try again later.'
}), 503
return render_template('errors/503.html',
message='Database connection error. Please try again later.'), 503
@app.errorhandler(IntegrityError)
def handle_db_integrity_error(error):
logger.error(f'Database integrity error: {str(error)}', exc_info=True)
if request.path.startswith('/api/'):
return jsonify({
'error': 'Database integrity error',
'message': 'The operation could not be completed due to data constraints.'
}), 409
return render_template('errors/409.html',
message='The operation could not be completed due to data constraints.'), 409
@app.errorhandler(SQLAlchemyError)
def handle_db_error(error):
logger.error(f'Database error: {str(error)}', exc_info=True)
if request.path.startswith('/api/'):
return jsonify({
'error': 'Database error',
'message': 'An unexpected database error occurred.'
}), 500
return render_template('errors/500.html',
message='An unexpected database error occurred.'), 500
@app.errorhandler(Exception)
def handle_unhandled_exception(e):
logger.error(f'Unhandled exception: {str(e)}', exc_info=True)
app.logger.error(f'Unhandled exception: {str(e)}', exc_info=True)
if request.path.startswith('/calculate'):
return jsonify({'error': 'Internal server error'}), 500
if request.path.startswith('/static/'):
app.logger.error(f'Error serving static file {request.path}: {str(e)}')
return '', 404
return render_template('errors/500.html'), 500
-16
View File
@@ -1,16 +0,0 @@
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_wtf.csrf import CSRFProtect
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_caching import Cache
from flask_mail import Mail
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
csrf = CSRFProtect()
limiter = Limiter(key_func=get_remote_address)
cache = Cache()
mail = Mail()
-19
View File
@@ -1,19 +0,0 @@
from .auth import LoginForm, RegistrationForm
from .quotation import QuotationForm
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo
from dotenv import load_dotenv
load_dotenv()
__all__ = ['LoginForm', 'RegistrationForm', 'QuotationForm']
class RequestPasswordResetForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset')
class ResetPasswordForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Reset Password')
-108
View File
@@ -1,108 +0,0 @@
from flask_wtf import FlaskForm
from wtforms import (
StringField,
PasswordField,
BooleanField,
SubmitField
)
from wtforms.validators import (
DataRequired,
Email,
Length,
ValidationError,
EqualTo
)
from sqlalchemy import func
from app.models import User
import re
class LoginForm(FlaskForm):
email = StringField('Email', validators=[
DataRequired(),
Email(),
Length(max=120)
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8)
])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Log In')
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=64)
])
email = StringField('Email', validators=[
DataRequired(),
Email(),
Length(max=120)
])
company = StringField('Company', validators=[
DataRequired(),
Length(max=100)
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8)
])
password_confirm = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password', message='Passwords must match')
])
terms = BooleanField('I agree to the terms and conditions', validators=[
DataRequired(message='You must agree to the terms and conditions')
])
submit = SubmitField('Register')
def validate_username(self, username):
if not re.match(r'^[a-zA-Z0-9_-]+$', username.data):
raise ValidationError('Username can only contain letters, numbers, underscores and hyphens')
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('Username already taken')
def validate_email(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('Email already registered')
def validate_password(self, password):
if not re.search(r'[A-Z]', password.data):
raise ValidationError('Password must contain at least one uppercase letter')
if not re.search(r'[a-z]', password.data):
raise ValidationError('Password must contain at least one lowercase letter')
if not re.search(r'[0-9]', password.data):
raise ValidationError('Password must contain at least one number')
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password.data):
raise ValidationError('Password must contain at least one special character')
class RequestPasswordResetForm(FlaskForm):
email = StringField('Email', validators=[
DataRequired(),
Email(),
Length(max=120)
])
submit = SubmitField('Request Password Reset')
class ResetPasswordForm(FlaskForm):
password = PasswordField('New Password', validators=[
DataRequired(),
Length(min=8)
])
password_confirm = PasswordField('Confirm New Password', validators=[
DataRequired(),
EqualTo('password', message='Passwords must match')
])
submit = SubmitField('Reset Password')
def validate_password(self, password):
if not re.search(r'[A-Z]', password.data):
raise ValidationError('Password must contain at least one uppercase letter')
if not re.search(r'[a-z]', password.data):
raise ValidationError('Password must contain at least one lowercase letter')
if not re.search(r'[0-9]', password.data):
raise ValidationError('Password must contain at least one number')
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password.data):
raise ValidationError('Password must contain at least one special character')
-193
View File
@@ -1,193 +0,0 @@
from flask_wtf import FlaskForm
from wtforms import (
StringField,
SelectField,
DateField,
SubmitField
)
from wtforms.validators import (
DataRequired,
Length,
ValidationError
)
def validate_tooling_id_format(form, field):
"""Custom validator to ensure tooling_id is exactly 5 digits"""
if not field.data.isdigit() or len(field.data) != 5:
raise ValidationError('Tooling ID must be exactly 5 digits')
class QuotationForm(FlaskForm):
# Factory Information
mold_shop_name = StringField('Mold Shop Name', validators=[DataRequired()])
mold_shop_country = SelectField('Mold Shop Country', validators=[DataRequired()], choices=[
('', 'Select Country'),
('China', 'China'),
('Vietnam', 'Vietnam'),
('Indonesia', 'Indonesia')
])
t1_shoe_factory_name = StringField('T1 Shoe Factory Name', validators=[DataRequired()])
t1_shoe_factory_country = SelectField('T1 Shoe Factory Country', validators=[DataRequired()], choices=[
('', 'Select Country'),
('China', 'China'),
('Vietnam', 'Vietnam'),
('Indonesia', 'Indonesia')
])
t2_component_factory_name = StringField('T2 Component Factory Name', validators=[DataRequired()])
t2_component_factory_country = SelectField('T2 Component Factory Country', validators=[DataRequired()], choices=[
('', 'Select Country'),
('China', 'China'),
('Vietnam', 'Vietnam'),
('Indonesia', 'Indonesia')
])
# Mold Information
model_name = StringField('Model Name', validators=[DataRequired()])
tooling_id = StringField('Tooling ID', validators=[DataRequired(), Length(min=5, max=5), validate_tooling_id_format], id='tooling_id')
season = StringField('Season', validators=[DataRequired()])
# Define stage-type mapping
STAGE_TYPE_MAPPING = {
'CR0': ['Sample'],
'CR1': ['Sample'],
'CR2': ['Sample'],
'SMS': ['A Set'],
'commercialization': ['A Set'],
'production': ['Additional']
}
# Define the mapping of main types to sub-types
MOLD_TYPE_MAPPING = {
'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']
}
# Define the mapping of sub-types to their specific options
MOLD_SPECIFIC_TYPE_OPTIONS = {
'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']
}
mold_main_type = SelectField('Mold Main Type', validators=[DataRequired()], choices=[
('', 'Select Main Type'),
('Rubber', 'Rubber'),
('CMEVA', 'CMEVA'),
('IMEVA', 'IMEVA'),
('Sandal', 'Sandal'),
('Co-shot mold', 'Co-shot mold'),
('PU', 'PU')
], id='mold_main_type')
mold_sub_type = SelectField('Mold Sub Type', validators=[DataRequired()], choices=[
('', 'Select Sub Type')
], id='mold_sub_type')
mold_specific_type = SelectField('Mold Specific Type', validators=[], choices=[
('', 'Select Specific Type')
], id='mold_specific_type', coerce=str, default='')
stage = SelectField('Stage', validators=[DataRequired()], choices=[
('', 'Select Stage'),
('CR0', 'CR0'),
('CR1', 'CR1'),
('CR2', 'CR2'),
('SMS', 'SMS'),
('commercialization', 'Commercialization'),
('production', 'Production')
], id='stage')
type = SelectField('Type', validators=[DataRequired()], choices=[
('', 'Select Type'),
('Sample', 'Sample'),
('A Set', 'A Set'),
('Additional', 'Additional')
], id='type')
# Dates and Approvals
issue_date = DateField('Issue Date', validators=[DataRequired()])
approved_by_mold_shop = StringField('Approved by (Mold Shop Representative)', validators=[DataRequired()])
approved_by_t1_tooling = StringField('Approved by (T1 Tooling Representative)', validators=[DataRequired()])
approved_by_lo_tooling = StringField('Approved by (LO Tooling)', validators=[DataRequired()])
# Mold Cost
sliders_count = SelectField('Number of Sliders', validators=[DataRequired()], choices=[
('', 'Select Number of Sliders'),
('0', '0'),
('1', '1'),
('2', '2'),
('3', '3'),
('4', '4')
])
cavity_count = SelectField('Number of Cavities', validators=[DataRequired()], choices=[
('', 'Select Number of Pairs'),
('0.5', '0.5 pair'),
('1', '1 pair'),
('1.5', '1.5 pair'),
('2', '2 pair'),
('2.5', '2.5 pair'),
('3', '3 pair')
])
digital_texture_type = SelectField('Digital Texture', validators=[DataRequired()], choices=[
('', 'Select Texture Type'),
('adidas_digital', 'adidas digital texture'),
('customized texture', 'customized texture'),
('laser', 'laser texture')
])
complexity_high_sidewall = SelectField('High Sidewall >60mm', validators=[DataRequired()], choices=[('no', 'No'), ('yes', 'Yes')])
submit = SubmitField('Calculate')
def __init__(self, *args, **kwargs):
super(QuotationForm, self).__init__(*args, **kwargs)
# Set initial choices for mold_sub_type based on default mold_main_type
if self.mold_main_type.data in self.MOLD_TYPE_MAPPING:
self.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in self.MOLD_TYPE_MAPPING[self.mold_main_type.data]]
def validate_mold_sub_type(self, field):
main_type = self.mold_main_type.data
print(f"Validating sub_type: {field.data} for main_type: {main_type}")
if main_type and main_type in self.MOLD_TYPE_MAPPING:
valid_sub_types = self.MOLD_TYPE_MAPPING[main_type]
print(f"Valid sub_types for {main_type}: {valid_sub_types}")
if field.data not in valid_sub_types and field.data != '':
print(f"Invalid sub_type: {field.data}. Valid types: {valid_sub_types}")
raise ValidationError('Invalid sub-type for selected main type')
def validate_mold_specific_type(self, field):
"""Validate that the selected specific type is valid for the selected sub type."""
sub_type = self.mold_sub_type.data
if not sub_type:
field.data = ''
return
# Get valid specific types for the selected sub type
valid_specific_types = self.MOLD_SPECIFIC_TYPE_OPTIONS.get(sub_type, [])
# If there are no specific types for this sub-type, the field should be empty
if not valid_specific_types:
field.data = '' # Always clear the field if it shouldn't have a value
return
# If there are specific types, validate the selection
if field.data is None or field.data == '':
raise ValidationError(f'Specific type is required for {sub_type}')
if field.data not in valid_specific_types:
raise ValidationError(f'Invalid specific type for {sub_type}. Valid options are: {", ".join(valid_specific_types)}')
def validate_type(self, field):
stage = self.stage.data
print(f"Validating type: {field.data} for stage: {stage}")
if stage and stage in self.STAGE_TYPE_MAPPING:
valid_types = self.STAGE_TYPE_MAPPING[stage]
print(f"Valid types for {stage}: {valid_types}")
if field.data not in valid_types and field.data != '':
print(f"Invalid type: {field.data}")
raise ValidationError(f'Invalid type for {stage} stage. Allowed types: {", ".join(valid_types)}')
elif field.data != '':
raise ValidationError('Please select a stage first')
def validate_tooling_id(self, field):
validate_tooling_id_format(self, field)
-126
View File
@@ -1,126 +0,0 @@
import os
import psutil
import time
from datetime import datetime
from flask import jsonify
from sqlalchemy import text
from . import db
def check_database():
"""Check database connection and response time."""
try:
start_time = time.time()
# Execute a simple query
db.session.execute(text('SELECT 1'))
response_time = (time.time() - start_time) * 1000 # Convert to milliseconds
return {
'status': 'healthy',
'response_time_ms': round(response_time, 2)
}
except Exception as e:
return {
'status': 'unhealthy',
'error': str(e)
}
def check_disk_space():
"""Check available disk space."""
try:
disk = psutil.disk_usage('/')
return {
'status': 'healthy' if disk.percent < 90 else 'warning',
'total_gb': round(disk.total / (1024**3), 2),
'used_gb': round(disk.used / (1024**3), 2),
'free_gb': round(disk.free / (1024**3), 2),
'percent_used': disk.percent
}
except Exception as e:
return {
'status': 'unhealthy',
'error': str(e)
}
def check_memory():
"""Check memory usage."""
try:
memory = psutil.virtual_memory()
return {
'status': 'healthy' if memory.percent < 90 else 'warning',
'total_gb': round(memory.total / (1024**3), 2),
'used_gb': round(memory.used / (1024**3), 2),
'free_gb': round(memory.available / (1024**3), 2),
'percent_used': memory.percent
}
except Exception as e:
return {
'status': 'unhealthy',
'error': str(e)
}
def check_cpu():
"""Check CPU usage."""
try:
cpu_percent = psutil.cpu_percent(interval=1)
return {
'status': 'healthy' if cpu_percent < 90 else 'warning',
'usage_percent': cpu_percent,
'core_count': psutil.cpu_count()
}
except Exception as e:
return {
'status': 'unhealthy',
'error': str(e)
}
def check_application():
"""Check application-specific metrics."""
try:
# Check if required directories exist
required_dirs = ['instance', 'logs', 'backups']
dir_status = {}
for dir_name in required_dirs:
dir_status[dir_name] = os.path.exists(dir_name)
# Check if log files are writable
log_status = {}
log_files = ['logs/mold_cost_calculator.log', 'logs/error.log', 'logs/access.log']
for log_file in log_files:
log_status[log_file] = os.access(os.path.dirname(log_file), os.W_OK)
return {
'status': 'healthy' if all(dir_status.values()) and all(log_status.values()) else 'warning',
'directories': dir_status,
'log_files': log_status,
'startup_time': datetime.utcnow().isoformat()
}
except Exception as e:
return {
'status': 'unhealthy',
'error': str(e)
}
def get_health_status():
"""Get comprehensive health status of the application."""
checks = {
'database': check_database(),
'disk_space': check_disk_space(),
'memory': check_memory(),
'cpu': check_cpu(),
'application': check_application()
}
# Determine overall status
statuses = [check['status'] for check in checks.values()]
if 'unhealthy' in statuses:
overall_status = 'unhealthy'
elif 'warning' in statuses:
overall_status = 'warning'
else:
overall_status = 'healthy'
return {
'status': overall_status,
'timestamp': datetime.utcnow().isoformat(),
'checks': checks
}
-96
View File
@@ -1,96 +0,0 @@
import os
import logging
from logging.handlers import RotatingFileHandler, SMTPHandler
from pathlib import Path
def configure_logging(app):
"""Configure logging for the application."""
# Create logs directory if it doesn't exist
log_dir = Path('logs')
log_dir.mkdir(exist_ok=True)
# Set up basic logging configuration
logging.basicConfig(
level=app.config['LOG_LEVEL'],
format=app.config['LOG_FORMAT'],
datefmt='%Y-%m-%d %H:%M:%S'
)
# Configure application logger
app.logger.setLevel(app.config['LOG_LEVEL'])
# Remove default handlers
for handler in app.logger.handlers[:]:
app.logger.removeHandler(handler)
# Add file handler for application logs
file_handler = RotatingFileHandler(
'logs/mold_cost_calculator.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=10,
encoding='utf-8'
)
file_handler.setFormatter(logging.Formatter(app.config['LOG_FORMAT']))
file_handler.setLevel(app.config['LOG_LEVEL'])
app.logger.addHandler(file_handler)
# Add file handler for error logs
error_handler = RotatingFileHandler(
'logs/error.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=10,
encoding='utf-8'
)
error_handler.setFormatter(logging.Formatter(app.config['LOG_FORMAT']))
error_handler.setLevel(logging.ERROR)
app.logger.addHandler(error_handler)
# Add file handler for access logs
access_handler = RotatingFileHandler(
'logs/access.log',
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=10,
encoding='utf-8'
)
access_handler.setFormatter(logging.Formatter(app.config['LOG_FORMAT']))
access_handler.setLevel(logging.INFO)
app.logger.addHandler(access_handler)
# Add console handler if LOG_TO_STDOUT is set
if app.config.get('LOG_TO_STDOUT'):
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(app.config['LOG_FORMAT']))
console_handler.setLevel(app.config['LOG_LEVEL'])
app.logger.addHandler(console_handler)
# Add email handler for critical errors in production
if not app.debug and not app.testing and app.config.get('MAIL_SERVER'):
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr=app.config['MAIL_DEFAULT_SENDER'],
toaddrs=[app.config['ADMIN_EMAIL']],
subject='Mold Cost Calculator Error'
)
mail_handler.setLevel(logging.ERROR)
mail_handler.setFormatter(logging.Formatter('''
Message type: %(levelname)s
Location: %(pathname)s:%(lineno)d
Module: %(module)s
Function: %(funcName)s
Time: %(asctime)s
Message:
%(message)s
'''))
app.logger.addHandler(mail_handler)
# Configure SQLAlchemy logging
if app.config.get('SQLALCHEMY_ECHO'):
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
# Configure Werkzeug logging
logging.getLogger('werkzeug').setLevel(logging.INFO)
# Log startup message
app.logger.info('Mold Cost Calculator startup')
return app.logger
-42
View File
@@ -1,42 +0,0 @@
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class CostFactor(db.Model):
"""Model for storing cost factors that can be managed via admin interface"""
id = db.Column(db.Integer, primary_key=True)
category = db.Column(db.String(50), nullable=False) # 'mold', 'complexity', 'texture', 'tax'
subcategory = db.Column(db.String(50)) # For mold types: 'Rubber', 'CMEVA', etc.
name = db.Column(db.String(100), nullable=False) # Factor name
factor_value = db.Column(db.Float, nullable=False) # The actual factor value
calculation_formula = db.Column(db.String(200)) # For complexity factors
percentage = db.Column(db.String(20)) # For display purposes
description = db.Column(db.String(500))
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_by = db.Column(db.Integer, db.ForeignKey('user.id'))
# Relationship
updater = db.relationship('User', backref='cost_factor_updates')
def __repr__(self):
return f'<CostFactor {self.category}:{self.name}={self.factor_value}>'
class CostFactorHistory(db.Model):
"""Model for tracking changes to cost factors"""
id = db.Column(db.Integer, primary_key=True)
cost_factor_id = db.Column(db.Integer, db.ForeignKey('cost_factor.id'), nullable=False)
old_value = db.Column(db.Float)
new_value = db.Column(db.Float, nullable=False)
changed_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
changed_at = db.Column(db.DateTime, default=datetime.utcnow)
change_reason = db.Column(db.String(500))
# Relationships
cost_factor = db.relationship('CostFactor', backref='history')
user = db.relationship('User', backref='cost_factor_changes')
def __repr__(self):
return f'<CostFactorHistory {self.cost_factor_id}:{self.old_value}->{self.new_value}>'
-6
View File
@@ -1,6 +0,0 @@
from .. import db
from .user import User
from .quotation import Quotation
from .cost_factor import CostFactor, CostFactorHistory
__all__ = ['db', 'User', 'Quotation', 'CostFactor', 'CostFactorHistory']
-66
View File
@@ -1,66 +0,0 @@
from datetime import datetime, timezone
from sqlalchemy import event
from .. import db
class CostFactor(db.Model):
"""Model for storing cost factors that can be managed via admin interface"""
__tablename__ = 'cost_factors'
id = db.Column(db.Integer, primary_key=True)
category = db.Column(db.String(50), nullable=False) # 'mold', 'complexity', 'texture', 'tax'
subcategory = db.Column(db.String(50)) # For mold types: 'Rubber', 'CMEVA', etc.
name = db.Column(db.String(100), nullable=False) # Factor name
factor_value = db.Column(db.Float, nullable=False) # The actual factor value
calculation_formula = db.Column(db.String(200)) # For complexity factors
percentage = db.Column(db.String(20)) # For display purposes
description = db.Column(db.String(500))
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
updated_by = db.Column(db.Integer, db.ForeignKey('enterprise_users.id'))
# Relationship
updater = db.relationship('User', backref='cost_factor_updates')
__table_args__ = (
db.Index('ix_cost_factor_category', category),
db.Index('ix_cost_factor_name', name),
db.Index('ix_cost_factor_active', is_active),
)
def __repr__(self):
return f'<CostFactor {self.category}:{self.name}={self.factor_value}>'
class CostFactorHistory(db.Model):
"""Model for tracking changes to cost factors"""
__tablename__ = 'cost_factor_history'
id = db.Column(db.Integer, primary_key=True)
cost_factor_id = db.Column(db.Integer, db.ForeignKey('cost_factors.id'), nullable=False)
old_value = db.Column(db.Float)
new_value = db.Column(db.Float, nullable=False)
changed_by = db.Column(db.Integer, db.ForeignKey('enterprise_users.id'), nullable=False)
changed_at = db.Column(db.DateTime, default=datetime.utcnow)
change_reason = db.Column(db.String(500))
# Relationships
cost_factor = db.relationship('CostFactor', backref='history')
user = db.relationship('User', backref='cost_factor_changes')
__table_args__ = (
db.Index('ix_cost_factor_history_factor_id', cost_factor_id),
db.Index('ix_cost_factor_history_changed_at', changed_at),
)
def __repr__(self):
return f'<CostFactorHistory {self.cost_factor_id}:{self.old_value}->{self.new_value}>'
# Event listeners for model changes
@event.listens_for(CostFactor, 'before_insert')
def set_cost_factor_timestamps(mapper, connection, target):
target.created_at = datetime.now(timezone.utc)
target.updated_at = datetime.now(timezone.utc)
@event.listens_for(CostFactor, 'before_update')
def set_cost_factor_update_timestamp(mapper, connection, target):
target.updated_at = datetime.now(timezone.utc)
-63
View File
@@ -1,63 +0,0 @@
from datetime import datetime, timezone
from sqlalchemy import event
from .. import db
class Quotation(db.Model):
__tablename__ = 'mold_quotations'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('enterprise_users.id'), nullable=False)
# Factory Information
mold_shop_name = db.Column(db.String(100), nullable=False)
mold_shop_country = db.Column(db.String(50), nullable=False)
t1_shoe_factory_name = db.Column(db.String(100), nullable=False)
t1_shoe_factory_country = db.Column(db.String(50), nullable=False)
t2_component_factory_name = db.Column(db.String(100), nullable=False)
t2_component_factory_country = db.Column(db.String(50), nullable=False)
# Mold Information
model_name = db.Column(db.String(100), nullable=False)
tooling_id = db.Column(db.String(5), nullable=False)
season = db.Column(db.String(10), nullable=False)
stage = db.Column(db.String(10), nullable=False)
type = db.Column(db.String(20), nullable=False)
# Dates and Approvals
issue_date = db.Column(db.Date, nullable=False)
approved_by_mold_shop = db.Column(db.String(100), nullable=False)
approved_by_t1_tooling = db.Column(db.String(100), nullable=False)
approved_by_lo_tooling = db.Column(db.String(100), nullable=False)
# Mold Specifications
mold_main_type = db.Column(db.String(50), nullable=False)
mold_sub_type = db.Column(db.String(50), nullable=False)
mold_specific_type = db.Column(db.String(50), nullable=False)
complexity_high_sidewall = db.Column(db.Boolean, default=False)
sliders_count = db.Column(db.Integer, nullable=False)
cavity_count = db.Column(db.Float, nullable=False)
digital_texture_type = db.Column(db.String(50), nullable=False)
# Calculation Results
base_price = db.Column(db.Float, nullable=False)
tax_rate = db.Column(db.Float)
total_cost = db.Column(db.Float, nullable=False)
# Metadata
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
is_archived = db.Column(db.Boolean, default=False)
__table_args__ = (
db.Index('ix_quotation_user_id', user_id),
db.Index('ix_quotation_created_at', created_at),
db.Index('ix_quotation_issue_date', issue_date),
)
def __repr__(self):
return f'<Quotation {self.id}>'
# Event listeners for model changes
@event.listens_for(Quotation, 'before_insert')
def set_quotation_timestamps(mapper, connection, target):
target.created_at = datetime.now(timezone.utc)
-116
View File
@@ -1,116 +0,0 @@
from datetime import datetime, timezone, timedelta
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from sqlalchemy import event, func
from .. import db
import secrets
import logging
logger = logging.getLogger(__name__)
class User(UserMixin, db.Model):
__tablename__ = 'enterprise_users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(25), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
company = db.Column(db.String(100), nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
created_at = db.Column(db.DateTime)
password_changed_at = db.Column(db.DateTime)
is_admin = db.Column(db.Boolean, default=False)
last_login = db.Column(db.DateTime)
is_active = db.Column(db.Boolean, default=True)
failed_login_attempts = db.Column(db.Integer, default=0)
last_failed_login = db.Column(db.DateTime)
reset_token = db.Column(db.String(100), unique=True)
reset_token_expires = db.Column(db.DateTime)
# Relationships
quotations = db.relationship('Quotation', backref='user', lazy=True)
def set_password(self, password):
try:
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
self.password_changed_at = datetime.now(timezone.utc)
self.reset_token = None
self.reset_token_expires = None
except Exception as e:
logger.error(f'Error setting password: {str(e)}', exc_info=True)
raise
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def generate_reset_token(self):
token = secrets.token_urlsafe(16) # Shorter token for display
self.reset_token = token
self.reset_token_expires = datetime.now(timezone.utc) + timedelta(minutes=15) # 15-minute expiry
return token
def verify_reset_token(self, token):
if not self.reset_token or not self.reset_token_expires:
return False
if datetime.now(timezone.utc) > self.reset_token_expires:
return False
return secrets.compare_digest(self.reset_token, token)
@classmethod
def authenticate(cls, email, password):
user = cls.query.filter(func.lower(cls.email) == email.lower()).first()
if user and user.check_password(password):
if not user.is_active:
return None
user.failed_login_attempts = 0
user.last_failed_login = None
db.session.commit()
return user
if user:
user.failed_login_attempts = (user.failed_login_attempts or 0) + 1
user.last_failed_login = datetime.now(timezone.utc)
db.session.commit()
return None
@classmethod
def create(cls, username, email, company, password):
try:
logger.info(f'Checking if username {username} exists...')
if cls.query.filter(func.lower(cls.username) == username.lower()).first():
logger.warning(f'Username {username} already taken')
raise ValueError('Username already taken')
logger.info(f'Checking if email {email} exists...')
if cls.query.filter(func.lower(cls.email) == email.lower()).first():
logger.warning(f'Email {email} already registered')
raise ValueError('Email already registered')
logger.info('Creating new user...')
user = cls(
username=username,
email=email,
company=company,
is_active=True,
failed_login_attempts=0,
is_admin=False
)
logger.info('Setting password...')
user.set_password(password)
logger.info('Adding user to session...')
db.session.add(user)
logger.info('User created successfully')
return user
except Exception as e:
logger.error(f'Error creating user: {str(e)}', exc_info=True)
raise
def __repr__(self):
return f'<User {self.username}>'
# Event listeners for model changes
@event.listens_for(User, 'before_insert')
def set_timestamps(mapper, connection, target):
target.created_at = datetime.now(timezone.utc)
target.password_changed_at = datetime.now(timezone.utc)
-473
View File
@@ -1,473 +0,0 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app, jsonify, make_response
from flask_login import login_required, current_user
from app import create_app, db
from ..forms import QuotationForm
from ..models import User, Quotation, CostFactor, CostFactorHistory
from datetime import datetime, timedelta, timezone
from sqlalchemy import desc, text
import logging
from app import limiter, cache
bp = Blueprint('admin', __name__, url_prefix='/admin')
@bp.before_request
def before_request():
if not current_user.is_authenticated or not current_user.is_admin:
flash('Access denied. Admin privileges required.', 'danger')
return redirect(url_for('main.home'))
@bp.route('/dashboard')
@login_required
@limiter.limit("30 per minute")
@cache.cached(timeout=60, key_prefix='admin_dashboard')
def dashboard():
try:
# Get statistics
total_users = User.query.count()
total_quotations = Quotation.query.count()
recent_quotations = Quotation.query.order_by(desc(Quotation.created_at)).limit(5).all()
# Get user statistics
active_users = User.query.filter(
User.last_login >= datetime.utcnow() - timedelta(days=30)
).count()
# Get quotation statistics by country (using mold_shop_country)
country_stats = db.session.query(
Quotation.mold_shop_country,
db.func.count(Quotation.id)
).group_by(Quotation.mold_shop_country).all()
return render_template('admin/dashboard.html',
total_users=total_users,
total_quotations=total_quotations,
active_users=active_users,
recent_quotations=recent_quotations,
country_stats=country_stats
)
except Exception as e:
current_app.logger.error(f'Admin dashboard error: {str(e)}')
flash('An error occurred while loading the dashboard.', 'error')
return redirect(url_for('main.home'))
@bp.route('/users')
@login_required
@limiter.limit("30 per minute")
@cache.cached(timeout=60, key_prefix='admin_users')
def users():
try:
page = request.args.get('page', 1, type=int)
per_page = 20
users = User.query.order_by(User.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return render_template('admin/users.html', users=users)
except Exception as e:
current_app.logger.error(f'Admin users error: {str(e)}')
flash('An error occurred while loading users.', 'error')
return redirect(url_for('admin.dashboard'))
@bp.route('/quotations')
@login_required
@limiter.limit("30 per minute")
def quotations():
try:
page = request.args.get('page', 1, type=int)
per_page = 20
# Get filter parameters
country = request.args.get('country')
mold_type = request.args.get('mold_type')
date_from_str = request.args.get('date_from')
date_to_str = request.args.get('date_to')
# Convert date strings to datetime objects
date_from = None
date_to = None
today = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
try:
if date_from_str:
date_from = datetime.strptime(date_from_str, '%Y-%m-%d')
if date_to_str:
date_to = datetime.strptime(date_to_str, '%Y-%m-%d')
except ValueError:
flash('Invalid date format. Please use YYYY-MM-DD format.', 'error')
date_from = None
date_to = None
# Build query
query = Quotation.query
if country:
query = query.filter(Quotation.mold_shop_country == country)
if mold_type:
query = query.filter(Quotation.mold_main_type == mold_type)
if date_from:
query = query.filter(Quotation.created_at >= date_from)
if date_to:
# Add one day to include the end date fully
next_day = date_to + timedelta(days=1)
query = query.filter(Quotation.created_at < next_day)
# Get unique countries and mold types for filters
countries = db.session.query(Quotation.mold_shop_country).distinct().all()
countries = [c[0] for c in countries if c[0]]
mold_types = db.session.query(Quotation.mold_main_type).distinct().all()
mold_types = [t[0] for t in mold_types if t[0]]
quotations = query.order_by(desc(Quotation.created_at)).paginate(
page=page, per_page=per_page, error_out=False
)
return render_template('admin/quotations.html',
quotations=quotations,
countries=sorted(countries),
mold_types=sorted(mold_types),
today=today,
filters={
'country': country,
'mold_type': mold_type,
'date_from': date_from,
'date_to': date_to
}
)
except Exception as e:
current_app.logger.error(f'Admin quotations error: {str(e)}')
flash('An error occurred while loading quotations.', 'error')
return redirect(url_for('admin.dashboard'))
@bp.route('/user/<int:user_id>')
@login_required
def user_detail(user_id):
try:
user = User.query.get_or_404(user_id)
quotations = Quotation.query.filter_by(user_id=user_id).order_by(
desc(Quotation.created_at)
).all()
return render_template('admin/user_detail.html',
user=user,
quotations=quotations
)
except Exception as e:
current_app.logger.error(f'Admin user detail error: {str(e)}')
flash('An error occurred while loading user details.', 'error')
return redirect(url_for('admin.users'))
@bp.route('/quotation/<int:id>')
@login_required
@limiter.limit("30 per minute")
@cache.cached(timeout=60, key_prefix='admin_quotation_detail')
def quotation_detail(id):
try:
quotation = Quotation.query.get_or_404(id)
return render_template('admin/quotation_detail.html',
quotation=quotation
)
except Exception as e:
current_app.logger.error(f'Admin quotation detail error: {str(e)}')
flash('An error occurred while loading quotation details.', 'error')
return redirect(url_for('admin.quotations'))
@bp.route('/user/<int:user_id>/toggle', methods=['POST'])
@login_required
def toggle_user(user_id):
try:
user = User.query.get_or_404(user_id)
user.is_active = not user.is_active
db.session.commit()
return jsonify({
'status': 'success',
'is_active': user.is_active
})
except Exception as e:
current_app.logger.error(f'Admin toggle user error: {str(e)}')
db.session.rollback()
return jsonify({
'status': 'error',
'message': 'An error occurred while updating user status'
}), 500
@bp.route('/delete_user', methods=['POST'])
@login_required
@limiter.limit("10 per minute")
def delete_user():
if not request.is_json:
return jsonify({'error': 'Invalid request format'}), 400
data = request.get_json()
user_id = data.get('user_id')
if not user_id:
return jsonify({'error': 'User ID is required'}), 400
try:
user = User.query.get_or_404(user_id)
if user.id == current_user.id:
return jsonify({'error': 'Cannot delete your own account'}), 400
db.session.delete(user)
db.session.commit()
cache.delete_memoized(users)
current_app.logger.info(f'User {user.email} deleted by admin {current_user.email}')
return jsonify({'message': 'User deleted successfully'})
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Error deleting user: {str(e)}')
return jsonify({'error': 'An error occurred while deleting the user'}), 500
@bp.route('/delete_quotation', methods=['POST'])
@login_required
@limiter.limit("10 per minute")
def delete_quotation():
if not request.is_json:
return jsonify({'error': 'Invalid request format'}), 400
data = request.get_json()
quotation_id = data.get('quotation_id')
if not quotation_id:
return jsonify({'error': 'Quotation ID is required'}), 400
try:
quotation = Quotation.query.get_or_404(quotation_id)
db.session.delete(quotation)
db.session.commit()
cache.delete_memoized(quotations)
current_app.logger.info(f'Quotation {quotation.id} deleted by admin {current_user.email}')
return jsonify({'message': 'Quotation deleted successfully'})
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Error deleting quotation: {str(e)}')
return jsonify({'error': 'An error occurred while deleting the quotation'}), 500
@bp.route('/cost-factors')
@login_required
@limiter.limit("30 per minute")
def cost_factors():
"""Display and manage cost factors"""
try:
from ..utils.cost_factor_loader import initialize_default_cost_factors
from flask import current_app
# Add debug logging
current_app.logger.info("Accessing cost_factors route")
# Verify database connection
try:
db.session.execute(text('SELECT 1'))
except Exception as e:
current_app.logger.error(f"Database connection error: {str(e)}")
flash('Database connection error', 'error')
return redirect(url_for('admin.dashboard'))
# Initialize default factors if none exist
try:
initialize_default_cost_factors()
except Exception as e:
current_app.logger.error(f"Error initializing default cost factors: {str(e)}")
flash('Error initializing cost factors', 'error')
return redirect(url_for('admin.dashboard'))
# Get all cost factors grouped by category
try:
mold_factors = CostFactor.query.filter_by(category='mold', is_active=True).order_by(CostFactor.name).all()
complexity_factors = CostFactor.query.filter_by(category='complexity', is_active=True).order_by(CostFactor.name).all()
texture_factors = CostFactor.query.filter_by(category='texture', is_active=True).order_by(CostFactor.name).all()
tax_factors = CostFactor.query.filter_by(category='tax', is_active=True).order_by(CostFactor.name).all()
base_factors = CostFactor.query.filter_by(category='base', is_active=True).order_by(CostFactor.name).all()
except Exception as e:
current_app.logger.error(f"Error querying cost factors: {str(e)}")
flash('Error loading cost factors from database', 'error')
return redirect(url_for('admin.dashboard'))
# Log the number of factors found
current_app.logger.info(f"Found factors - Mold: {len(mold_factors)}, Complexity: {len(complexity_factors)}, "
f"Texture: {len(texture_factors)}, Tax: {len(tax_factors)}, Base: {len(base_factors)}")
# Log individual factors for debugging
current_app.logger.debug("Base Factors: %s", [f"{f.name}: {f.factor_value}" for f in base_factors])
current_app.logger.debug("Mold Factors: %s", [f"{f.name}: {f.factor_value}" for f in mold_factors])
current_app.logger.debug("Complexity Factors: %s", [f"{f.name}: {f.factor_value}" for f in complexity_factors])
current_app.logger.debug("Texture Factors: %s", [f"{f.name}: {f.factor_value}" for f in texture_factors])
current_app.logger.debug("Tax Factors: %s", [f"{f.name}: {f.factor_value}" for f in tax_factors])
# Prepare template context
template_context = {
'mold_factors': mold_factors,
'complexity_factors': complexity_factors,
'texture_factors': texture_factors,
'tax_factors': tax_factors,
'base_factors': base_factors
}
# Log template context
current_app.logger.debug("Template context: %s", template_context)
try:
response = make_response(render_template(
'admin/cost_factors.html',
**template_context
))
except Exception as e:
current_app.logger.error(f"Template rendering error: {str(e)}")
flash('Error rendering the page', 'error')
return redirect(url_for('admin.dashboard'))
# Add headers to prevent caching
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
except Exception as e:
current_app.logger.error(f"Unhandled error in cost_factors route: {str(e)}")
flash('An unexpected error occurred while loading cost factors.', 'error')
return redirect(url_for('admin.dashboard'))
@bp.route('/cost-factors/edit/<int:factor_id>', methods=['GET', 'POST'])
@login_required
@limiter.limit("30 per minute")
def edit_cost_factor(factor_id):
"""Edit a specific cost factor"""
try:
factor = CostFactor.query.get_or_404(factor_id)
if request.method == 'POST':
old_value = factor.factor_value
new_value = float(request.form.get('factor_value'))
change_reason = request.form.get('change_reason', '')
# Update the factor
factor.factor_value = new_value
factor.updated_by = current_user.id
# Create history record
history = CostFactorHistory(
cost_factor_id=factor.id,
old_value=old_value,
new_value=new_value,
changed_by=current_user.id,
change_reason=change_reason
)
db.session.add(history)
db.session.commit()
# Clear the cache
cache.delete_memoized(cost_factors)
cache.delete('admin_cost_factors')
flash(f'Cost factor "{factor.name}" updated successfully.', 'success')
return redirect(url_for('admin.cost_factors'))
return render_template('admin/edit_cost_factor.html', factor=factor)
except Exception as e:
current_app.logger.error(f'Admin edit cost factor error: {str(e)}')
flash('An error occurred while editing the cost factor.', 'error')
return redirect(url_for('admin.cost_factors'))
@bp.route('/cost-factors/history/<int:factor_id>')
@login_required
@limiter.limit("30 per minute")
def cost_factor_history(factor_id):
"""View history of changes for a cost factor"""
try:
factor = CostFactor.query.get_or_404(factor_id)
history = CostFactorHistory.query.filter_by(cost_factor_id=factor_id)\
.order_by(CostFactorHistory.changed_at.desc()).all()
return render_template('admin/cost_factor_history.html',
factor=factor,
history=history
)
except Exception as e:
current_app.logger.error(f'Admin cost factor history error: {str(e)}')
flash('An error occurred while loading cost factor history.', 'error')
return redirect(url_for('admin.cost_factors'))
@bp.route('/cost-factors/reset', methods=['POST'])
@login_required
@limiter.limit("10 per minute")
def reset_cost_factors():
"""Reset cost factors to default values"""
try:
from ..utils.cost_factor_loader import get_default_cost_factors
# Get default values
default_factors, default_tax_rates, default_base = get_default_cost_factors()
# Create a mapping of all default values
default_values = {}
# Add mold factors
for name, value in default_factors['mold'].items():
default_values[name] = value
# Add complexity factors
for name, value in default_factors['complexity'].items():
default_values[name] = value
# Add texture factors
for name, value in default_factors['texture'].items():
default_values[name] = value
# Add tax factors
for name, value in default_tax_rates.items():
default_values[name] = value
# Add base factor
default_values['DEFAULT_NET_BASE'] = default_base
# Update existing cost factors to default values
for factor in CostFactor.query.all():
if factor.name in default_values:
old_value = factor.factor_value
new_value = default_values[factor.name]
if old_value != new_value: # Only update if value changed
factor.factor_value = new_value
factor.updated_by = current_user.id
# Create history record for the reset
history = CostFactorHistory(
cost_factor_id=factor.id,
old_value=old_value,
new_value=new_value,
changed_by=current_user.id,
change_reason='Reset to default value'
)
db.session.add(history)
db.session.commit()
# Clear the cache
cache.delete_memoized(cost_factors)
cache.delete('admin_cost_factors')
flash('Cost factors have been reset to default values.', 'success')
return redirect(url_for('admin.cost_factors'))
except Exception as e:
current_app.logger.error(f'Admin reset cost factors error: {str(e)}')
db.session.rollback()
flash('An error occurred while resetting cost factors.', 'error')
return redirect(url_for('admin.cost_factors'))
@bp.route('/debug-endpoints')
def debug_endpoints():
from flask import current_app
with open('logs/endpoints.log', 'w') as f:
for rule in current_app.url_map.iter_rules():
f.write(f"{rule.endpoint} {rule.methods} {rule.rule}\n")
return 'Endpoints written to logs/endpoints.log', 200
-171
View File
@@ -1,171 +0,0 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app
from flask_login import login_user, logout_user, login_required, current_user
from app.forms import LoginForm, RegistrationForm, RequestPasswordResetForm, ResetPasswordForm
from app import create_app, db
from app import limiter
from datetime import datetime, timezone
from app.config import ALLOWED_EMAILS
import logging
from flask_mail import Message
from app import mail
from app.models import User
bp = Blueprint('auth', __name__)
logger = logging.getLogger(__name__)
@bp.route('/login', methods=['GET', 'POST'])
@limiter.limit("5 per minute")
def login():
if current_user.is_authenticated:
return redirect(url_for('main.calculator'))
form = LoginForm()
if form.validate_on_submit():
try:
email = form.email.data.strip().lower()
password = form.password.data
# Log the login attempt
current_app.logger.info(f'Login attempt for email: {email}')
user = User.authenticate(email, password)
if user:
login_user(user, remember=form.remember_me.data)
user.last_login = datetime.now(timezone.utc)
db.session.commit()
current_app.logger.info(f'User {user.email} logged in successfully')
next_page = request.args.get('next') or url_for('main.calculator')
return redirect(next_page)
else:
current_app.logger.warning(f'Failed login attempt for email: {email}')
flash('Invalid email or password', 'danger')
except Exception as e:
current_app.logger.error(f'Login error: {str(e)}', exc_info=True)
flash('An error occurred during login. Please try again.', 'danger')
return render_template('auth/login.html', form=form)
@bp.route('/register', methods=['GET', 'POST'])
@limiter.limit(lambda: None if current_app.config.get('DEBUG', False) else "10 per hour")
def register():
try:
if current_user.is_authenticated:
return redirect(url_for('main.home'))
form = RegistrationForm()
logger.info('Registration form initialized')
if form.validate_on_submit():
try:
email = form.email.data.strip().lower()
username = form.username.data.strip().lower()
company = form.company.data.strip().upper()
logger.info(f'Processing registration for email: {email}, username: {username}, company: {company}')
# Check if email is in allowed list or has adidas.com domain
if email not in ALLOWED_EMAILS and not email.endswith('@adidas.com'):
logger.warning(f'Registration attempt with non-allowed email: {email}')
flash('Registration is not allowed for this email domain', 'danger')
return render_template('auth/register.html', form=form)
try:
logger.info('Creating new user...')
user = User.create(
username=username,
email=email,
company=company,
password=form.password.data
)
logger.info('User created, committing to database...')
db.session.commit()
logger.info(f'New user registered successfully: {user.email}')
flash('Registration successful! Please login', 'success')
return redirect(url_for('auth.login'))
except ValueError as e:
logger.warning(f'Registration validation error: {str(e)}')
flash(str(e), 'danger')
except Exception as e:
db.session.rollback()
logger.error(f'Registration error: {str(e)}', exc_info=True)
flash('An error occurred during registration', 'danger')
except Exception as e:
logger.error(f'Unexpected error during registration: {str(e)}', exc_info=True)
flash('An unexpected error occurred', 'danger')
elif form.errors:
logger.warning(f'Form validation errors: {form.errors}')
for field, errors in form.errors.items():
for error in errors:
flash(f'{field}: {error}', 'danger')
logger.warning(f'Field {field} validation error: {error}')
logger.info('Rendering registration template')
return render_template('auth/register.html', form=form)
except Exception as e:
logger.error(f'Critical error in registration route: {str(e)}', exc_info=True)
flash('A critical error occurred', 'danger')
return render_template('auth/register.html', form=form)
@bp.route('/logout')
@login_required
def logout():
current_app.logger.info(f'User {current_user.email} logged out')
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('main.home'))
@bp.route('/request-reset', methods=['GET', 'POST'])
@limiter.limit("3 per hour")
def request_reset():
if current_user.is_authenticated:
return redirect(url_for('main.calculator'))
form = RequestPasswordResetForm()
if form.validate_on_submit():
try:
email = form.email.data.strip().lower()
user = User.query.filter_by(email=email).first()
if user:
token = user.generate_reset_token()
db.session.commit()
reset_url = url_for('auth.reset_password', token=token, _external=True)
flash(f'Your password reset link is: {reset_url}', 'info')
flash('This link will expire in 15 minutes.', 'warning')
return redirect(url_for('auth.login'))
# Always show success message to prevent email enumeration
flash('If an account exists with that email, you will receive password reset instructions.', 'info')
return redirect(url_for('auth.login'))
except Exception as e:
current_app.logger.error(f'Error generating reset token: {str(e)}', exc_info=True)
flash('An error occurred while processing your request.', 'danger')
return render_template('auth/request_reset.html', form=form)
@bp.route('/reset-password/<token>', methods=['GET', 'POST'])
@limiter.limit("3 per hour")
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.calculator'))
form = ResetPasswordForm()
if form.validate_on_submit():
try:
user = User.query.filter_by(reset_token=token).first()
if not user or not user.verify_reset_token(token):
flash('The password reset link is invalid or has expired.', 'danger')
return redirect(url_for('auth.request_reset'))
user.set_password(form.password.data)
db.session.commit()
current_app.logger.info(f'Password reset successful for user {user.email}')
flash('Your password has been reset. You can now log in.', 'success')
return redirect(url_for('auth.login'))
except Exception as e:
current_app.logger.error(f'Error resetting password: {str(e)}', exc_info=True)
flash('An error occurred while resetting your password.', 'danger')
return render_template('auth/reset_password.html', form=form)
-383
View File
@@ -1,383 +0,0 @@
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, current_app, flash
from flask_login import current_user, login_required, login_user, logout_user
from app.models import Quotation, User
from app import db
from app.forms import LoginForm, QuotationForm
from app.utils.cost_factor_loader import get_cost_factors
from app.config import ALLOWED_EMAILS
import logging
from datetime import datetime, timezone, timedelta
from app import limiter, cache
import json
from ..utils.calculator import calculate_base_price
from ..health import get_health_status
bp = Blueprint('main', __name__)
logger = logging.getLogger(__name__)
@bp.route('/', methods=['GET', 'POST'])
def home():
if current_user.is_authenticated:
return redirect(url_for('main.calculator'))
return redirect(url_for('auth.login'))
@bp.route('/calculator')
@login_required
def calculator():
try:
form = QuotationForm()
# Set today's date as default for issue_date
if not form.issue_date.data:
form.issue_date.data = datetime.now(timezone.utc).date()
# Get cost factors for the form
cost_factors, tax_rates, default_net_base = get_cost_factors()
# Initialize form with default values
if form.mold_main_type.data in form.MOLD_TYPE_MAPPING:
form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[form.mold_main_type.data]]
# Get quote history with error handling
try:
quote_history = Quotation.query.filter_by(user_id=current_user.id)\
.order_by(Quotation.created_at.desc())\
.limit(5)\
.all()
except Exception as e:
current_app.logger.error(f"Error fetching quote history: {str(e)}")
quote_history = []
return render_template('calculator.html',
form=form,
quote_history=quote_history,
tax_rates=tax_rates,
quotation=None)
except Exception as e:
current_app.logger.error(f"Calculator error: {str(e)}", exc_info=True)
flash('Error loading calculator data', 'danger')
# Create a new form with default values
form = QuotationForm()
if form.mold_main_type.data in form.MOLD_TYPE_MAPPING:
form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[form.mold_main_type.data]]
# Set today's date as default even in error case
form.issue_date.data = datetime.now(timezone.utc).date()
return render_template('calculator.html',
form=form,
quote_history=[],
tax_rates={},
quotation=None)
@bp.route('/calculate', methods=['GET', 'POST'])
@login_required
@limiter.limit("30 per minute")
@cache.cached(timeout=300, key_prefix='calculate_form')
def calculate():
"""Calculate mold cost based on form data"""
try:
form = QuotationForm()
# Log incoming request data
current_app.logger.info('Incoming request data:')
current_app.logger.info(f'Request method: {request.method}')
current_app.logger.info(f'Request form data: {dict(request.form)}')
current_app.logger.info(f'Request JSON data: {request.get_json(silent=True)}')
if request.method == 'POST':
try:
# Log form data before validation
current_app.logger.info('Form data before validation:')
current_app.logger.info(f'Form data: {form.data}')
current_app.logger.info(f'Form errors: {form.errors}')
# Log the selected mold type
current_app.logger.info(f'Selected mold_main_type: {form.mold_main_type.data}')
current_app.logger.info(f'Selected mold_sub_type: {form.mold_sub_type.data}')
current_app.logger.info(f'Selected mold_specific_type: {form.mold_specific_type.data}')
# Update mold_sub_type choices based on selected mold_main_type
main_type = request.form.get('mold_main_type')
current_app.logger.info(f'Updating sub_type choices for main_type: {main_type}')
if main_type in form.MOLD_TYPE_MAPPING:
form.mold_sub_type.choices = [('', 'Select Sub Type')] + [(x, x) for x in form.MOLD_TYPE_MAPPING[main_type]]
current_app.logger.info(f'Updated sub_type choices: {form.mold_sub_type.choices}')
# Update mold_specific_type choices based on selected mold_sub_type
sub_type = request.form.get('mold_sub_type')
if sub_type in form.MOLD_SPECIFIC_TYPE_OPTIONS:
form.mold_specific_type.choices = [('', 'Select Specific Type')] + [(x, x) for x in form.MOLD_SPECIFIC_TYPE_OPTIONS[sub_type]]
else:
form.mold_specific_type.choices = [('', 'Select Specific Type')]
# Check if specific type is needed
if sub_type and sub_type in form.MOLD_SPECIFIC_TYPE_OPTIONS:
# If specific type is needed but not provided, set it to empty string
if not request.form.get('mold_specific_type'):
form.mold_specific_type.data = ''
else:
# If specific type is not needed, set it to empty string
form.mold_specific_type.data = ''
if not form.validate_on_submit():
current_app.logger.warning('Form validation failed:')
current_app.logger.warning(f'Form errors: {form.errors}')
current_app.logger.warning(f'Form data after validation: {form.data}')
current_app.logger.warning(f'Request form data: {dict(request.form)}')
return jsonify({
'success': False,
'error': 'Invalid form data',
'message': 'Please check the form for errors',
'errors': form.errors,
'form_data': dict(request.form)
}), 400
# Get form data
form_data = form.data
current_app.logger.info(f'Processing form data: {form_data}')
# Calculate costs
try:
result = calculate_base_price(
mold_main_type=form.mold_main_type.data,
mold_sub_type=form.mold_sub_type.data,
mold_specific_type=form.mold_specific_type.data,
sliders_count=int(form.sliders_count.data),
cavity_count=float(form.cavity_count.data),
digital_texture_type=form.digital_texture_type.data,
complexity_high_sidewall=(form.complexity_high_sidewall.data == 'yes')
)
current_app.logger.info(f'Calculation result: {result}')
# Get cost factors for tax rate
cost_factors, tax_rates, default_net_base = get_cost_factors()
# Get tax rate for the country
tax_rate = tax_rates.get(form.mold_shop_country.data, 0.0)
# Calculate total cost (base price + tax)
total_cost = result['total_cost'] * (1 + tax_rate)
return jsonify({
'success': True,
'base_price': float(result['base_price']),
'complexity_cost': float(result['complexity_cost']),
'texture_cost': float(result['texture_cost']),
'tax_rate': float(tax_rate),
'total_cost': float(total_cost),
'mold_shop_name': form.mold_shop_name.data,
'mold_shop_country': form.mold_shop_country.data,
't1_shoe_factory_name': form.t1_shoe_factory_name.data,
't1_shoe_factory_country': form.t1_shoe_factory_country.data,
't2_component_factory_name': form.t2_component_factory_name.data,
't2_component_factory_country': form.t2_component_factory_country.data,
'model_name': form.model_name.data,
'tooling_id': form.tooling_id.data,
'season': form.season.data,
'stage': form.stage.data,
'type': form.type.data,
'issue_date': form.issue_date.data.strftime('%Y-%m-%d'),
'approved_by_mold_shop': form.approved_by_mold_shop.data,
'approved_by_t1_tooling': form.approved_by_t1_tooling.data,
'approved_by_lo_tooling': form.approved_by_lo_tooling.data,
'mold_main_type': form.mold_main_type.data,
'mold_sub_type': form.mold_sub_type.data,
'mold_specific_type': form.mold_specific_type.data,
'complexity_high_sidewall': 'yes' if form.complexity_high_sidewall.data == 'yes' else 'no',
'sliders_count': form.sliders_count.data,
'cavity_count': form.cavity_count.data,
'digital_texture_type': form.digital_texture_type.data
})
except Exception as calc_error:
current_app.logger.error(f'Error calculating costs: {str(calc_error)}', exc_info=True)
return jsonify({
'success': False,
'error': 'Calculation error',
'message': str(calc_error)
}), 500
except Exception as form_error:
current_app.logger.error(f'Error processing form: {str(form_error)}', exc_info=True)
return jsonify({
'success': False,
'error': 'Form processing error',
'message': str(form_error)
}), 500
# GET request - render the form
return render_template('calculator.html', form=form)
except Exception as e:
current_app.logger.error(f'Unexpected error in calculate route: {str(e)}', exc_info=True)
return jsonify({
'success': False,
'error': 'Unexpected error',
'message': str(e)
}), 500
@bp.route('/quotations')
@login_required
@limiter.limit("30 per minute")
@cache.cached(timeout=60, key_prefix='user_quotations')
def quotations():
quotations = Quotation.query.filter_by(user_id=current_user.id)\
.order_by(Quotation.created_at.desc()).all()
return render_template('quotations.html', quotations=quotations)
@bp.route('/quotation/<int:id>')
@login_required
@limiter.limit("30 per minute")
@cache.cached(timeout=60, key_prefix='quotation_detail')
def quotation_detail(id):
quotation = Quotation.query.get_or_404(id)
if quotation.user_id != current_user.id and not current_user.is_admin:
flash('Access denied.', 'danger')
return redirect(url_for('main.quotations'))
return render_template('quotation_detail.html', quotation=quotation)
@bp.route('/health')
def health_check():
"""Health check endpoint that returns the status of various system components."""
# Get health status
health_status = get_health_status()
# Set appropriate status code based on overall health
status_code = 200 if health_status['status'] == 'healthy' else 503
# Add rate limiting headers
response = jsonify(health_status)
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response, status_code
@bp.route('/api/recent-quotes')
@login_required
@limiter.limit("30 per minute")
def recent_quotes():
try:
recent_quotes = Quotation.query.filter_by(user_id=current_user.id)\
.order_by(Quotation.created_at.desc())\
.limit(5)\
.all()
return jsonify({
'success': True,
'quotes': [{
'id': quote.id,
'model_name': quote.model_name,
'tooling_id': quote.tooling_id,
'mold_sub_type': quote.mold_sub_type,
'cavity_count': float(quote.cavity_count),
'sliders_count': quote.sliders_count,
'created_at': quote.created_at.strftime('%Y-%m-%d %H:%M'),
'mold_shop_country': quote.mold_shop_country,
'digital_texture_type': quote.digital_texture_type,
'season': quote.season,
'stage': quote.stage,
'base_price': float(quote.base_price),
'tax_rate': float(quote.tax_rate) if quote.tax_rate is not None else 0.0,
'total_cost': float(quote.total_cost)
} for quote in recent_quotes]
})
except Exception as e:
current_app.logger.error(f'Error fetching recent quotes: {str(e)}', exc_info=True)
return jsonify({
'success': False,
'error': 'An error occurred while fetching recent quotes',
'quotes': []
}), 500
@bp.route('/save-quotation', methods=['POST'])
@login_required
@limiter.limit("30 per minute")
def save_quotation():
"""Save a quotation to the database"""
try:
# Get form data
form_data = request.get_json()
if not form_data:
return jsonify({
'success': False,
'error': 'No data provided'
}), 400
# Check for duplicate submission (same user, same data within last 30 seconds)
try:
recent_duplicate = Quotation.query.filter(
Quotation.user_id == current_user.id,
Quotation.model_name == form_data.get('model_name'),
Quotation.tooling_id == form_data.get('tooling_id'),
Quotation.mold_main_type == form_data.get('mold_main_type'),
Quotation.mold_sub_type == form_data.get('mold_sub_type'),
Quotation.created_at >= datetime.now(timezone.utc) - timedelta(seconds=30)
).first()
if recent_duplicate:
current_app.logger.warning(f'Duplicate submission detected for user {current_user.id}, tooling_id {form_data.get("tooling_id")}')
return jsonify({
'success': False,
'error': 'Duplicate submission',
'message': 'This quotation was already submitted recently. Please wait a moment before trying again.',
'quotation_id': recent_duplicate.id
}), 409
except Exception as duplicate_check_error:
current_app.logger.error(f'Error checking for duplicates: {str(duplicate_check_error)}')
# Continue with submission even if duplicate check fails
# Ensure mold_specific_type is not empty or missing
mold_specific_type = form_data.get('mold_specific_type')
if not mold_specific_type:
mold_specific_type = 'N/A'
# Create new quotation
quotation = Quotation(
user_id=current_user.id,
mold_shop_name=form_data.get('mold_shop_name'),
mold_shop_country=form_data.get('mold_shop_country'),
t1_shoe_factory_name=form_data.get('t1_shoe_factory_name'),
t1_shoe_factory_country=form_data.get('t1_shoe_factory_country'),
t2_component_factory_name=form_data.get('t2_component_factory_name'),
t2_component_factory_country=form_data.get('t2_component_factory_country'),
model_name=form_data.get('model_name'),
tooling_id=form_data.get('tooling_id'),
season=form_data.get('season'),
stage=form_data.get('stage'),
type=form_data.get('type'),
issue_date=datetime.strptime(form_data.get('issue_date'), '%Y-%m-%d').date(),
approved_by_mold_shop=form_data.get('approved_by_mold_shop'),
approved_by_t1_tooling=form_data.get('approved_by_t1_tooling'),
approved_by_lo_tooling=form_data.get('approved_by_lo_tooling'),
mold_main_type=form_data.get('mold_main_type'),
mold_sub_type=form_data.get('mold_sub_type'),
mold_specific_type=mold_specific_type,
complexity_high_sidewall=(form_data.get('complexity_high_sidewall') == 'yes'),
sliders_count=int(form_data.get('sliders_count')),
cavity_count=float(form_data.get('cavity_count')),
digital_texture_type=form_data.get('digital_texture_type'),
base_price=float(form_data.get('base_price')),
tax_rate=float(form_data.get('tax_rate')),
total_cost=float(form_data.get('total_cost'))
)
# Save to database
db.session.add(quotation)
db.session.commit()
current_app.logger.info(f'Quotation saved with ID: {quotation.id}')
return jsonify({
'success': True,
'quotation_id': quotation.id,
'message': 'Quotation saved successfully'
})
except Exception as e:
db.session.rollback()
current_app.logger.error(f'Error saving quotation: {str(e)}', exc_info=True)
return jsonify({
'success': False,
'error': 'Failed to save quotation',
'message': str(e)
}), 500
-197
View File
@@ -1,197 +0,0 @@
/* 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
File diff suppressed because one or more lines are too long
-91
View File
@@ -1,91 +0,0 @@
/* 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
@@ -1,144 +0,0 @@
: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
@@ -1,24 +0,0 @@
<?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>

Before

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
@@ -1,638 +0,0 @@
// 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);
}
}
-15
View File
@@ -1,15 +0,0 @@
<div class="list-group">
{% for quote in quote_history %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary me-2">#{{ quote.id }}</span>
{{ quote.created_at.strftime('%Y-%m-%d %H:%M') }}
</div>
<div class="fw-bold">¥{{ "{:,.0f}".format(quote.total_cost) }}</div>
</div>
{% else %}
<div class="list-group-item text-muted">
No calculations yet
</div>
{% endfor %}
</div>
-298
View File
@@ -1,298 +0,0 @@
{% macro render_quotation_details(quotation, show_actions=true) %}
<div class="quotation-details">
<div class="detail-card">
<h3>Basic Information</h3>
<div class="detail-grid">
<div class="detail-item">
<label>ID</label>
<span>{{ quotation.id }}</span>
</div>
<div class="detail-item">
<label>User</label>
<span>{{ quotation.user.username }}</span>
</div>
<div class="detail-item">
<label>Created</label>
<span>{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
</div>
<div class="detail-item">
<label>Status</label>
<span class="badge badge-success">Completed</span>
</div>
</div>
</div>
<div class="detail-card">
<h3>Project Details</h3>
<div class="detail-grid">
<div class="detail-item">
<label>Model Name</label>
<span>{{ quotation.model_name }}</span>
</div>
<div class="detail-item">
<label>Tooling ID</label>
<span>{{ quotation.tooling_id }}</span>
</div>
<div class="detail-item">
<label>Season</label>
<span>{{ quotation.season }}</span>
</div>
<div class="detail-item">
<label>Stage</label>
<span>{{ quotation.stage }}</span>
</div>
<div class="detail-item">
<label>Country</label>
<span>{{ quotation.country }}</span>
</div>
<div class="detail-item">
<label>Issue Date</label>
<span>{{ quotation.issue_date.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</div>
<div class="detail-card">
<h3>Mold Specifications</h3>
<div class="detail-grid">
<div class="detail-item">
<label>Mold Main Type</label>
<span>{{ quotation.mold_main_type }}</span>
</div>
<div class="detail-item">
<label>Mold Sub Type</label>
<span>{{ quotation.mold_sub_type }}</span>
</div>
<div class="detail-item">
<label>High Sidewall Complexity</label>
<span>{{ 'Yes' if quotation.complexity_high_sidewall else 'No' }}</span>
</div>
<div class="detail-item">
<label>Number of Sliders</label>
<span>{{ quotation.sliders_count }}</span>
</div>
<div class="detail-item">
<label>Number of Cavities</label>
<span>{{ quotation.cavity_count }}</span>
</div>
<div class="detail-item">
<label>Digital Texture Type</label>
<span>{{ quotation.digital_texture_type }}</span>
</div>
</div>
</div>
<div class="detail-card">
<h3>Cost Breakdown</h3>
<div class="cost-breakdown">
<div class="cost-item">
<label>Net Base Price</label>
<span>${{ "%.2f"|format(quotation.net_base_price) }}</span>
</div>
<div class="cost-item">
<label>Tax Rate</label>
<span>{{ "%.1f"|format(quotation.tax_rate * 100) }}%</span>
</div>
<div class="cost-item total">
<label>Total Cost</label>
<span>${{ "%.2f"|format(quotation.total_cost) }}</span>
</div>
</div>
</div>
<div class="detail-card">
<h3>Factory Information</h3>
<div class="detail-grid">
<div class="detail-item">
<label>Mold Shop Name</label>
<span>{{ quotation.mold_shop_name }}</span>
</div>
<div class="detail-item">
<label>T1 Factory Name</label>
<span>{{ quotation.t1_factory_name or 'N/A' }}</span>
</div>
<div class="detail-item">
<label>T2 Component Factory</label>
<span>{{ quotation.t2_component_factory or 'N/A' }}</span>
</div>
</div>
</div>
{% if show_actions %}
<div class="detail-actions">
<a href="{{ url_for('admin.quotations') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Quotations
</a>
<button class="btn btn-primary" onclick="window.print()">
<i class="fas fa-print"></i> Print Quotation
</button>
</div>
{% endif %}
</div>
<style>
.quotation-details {
display: flex;
flex-direction: column;
gap: 30px;
}
.detail-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.detail-card h3 {
margin: 0 0 20px;
color: #2c3e50;
font-size: 1.2rem;
font-weight: 600;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.detail-item label {
color: #6c757d;
font-size: 0.9rem;
}
.detail-item span {
font-size: 1.1rem;
color: #2c3e50;
}
.cost-breakdown {
display: flex;
flex-direction: column;
gap: 15px;
}
.cost-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #dee2e6;
}
.cost-item:last-child {
border-bottom: none;
}
.cost-item label {
color: #6c757d;
font-size: 1rem;
}
.cost-item span {
font-size: 1.1rem;
color: #2c3e50;
font-weight: 500;
}
.cost-item.total {
margin-top: 10px;
padding-top: 20px;
border-top: 2px solid #dee2e6;
}
.cost-item.total label {
font-size: 1.2rem;
font-weight: 600;
color: #2c3e50;
}
.cost-item.total span {
font-size: 1.4rem;
font-weight: 700;
color: #28a745;
}
.detail-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 5px;
text-decoration: none;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-success {
background-color: #28a745;
color: white;
}
@media (max-width: 768px) {
.detail-grid {
grid-template-columns: 1fr;
}
.detail-actions {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
}
@media print {
.detail-actions {
display: none;
}
.detail-card {
break-inside: avoid;
box-shadow: none;
border: 1px solid #dee2e6;
}
}
</style>
{% endmacro %}
-130
View File
@@ -1,130 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Admin Panel{% endblock %}</title>
{% block styles %}
<!-- Bootstrap CSS -->
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-icons.css') }}">
<!-- Admin Styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin.css') }}">
<!-- Custom Styles -->
<style>
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--light-color: #f8f9fa;
--dark-color: #212529;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #f8f9fa;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card {
border: none;
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
transition: box-shadow 0.15s ease-in-out;
}
.card:hover {
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.15);
}
.btn {
border-radius: 0.375rem;
}
.table {
border-radius: 0.375rem;
overflow: hidden;
}
.alert {
border: none;
border-radius: 0.375rem;
}
</style>
{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('admin.dashboard') }}">
<i class="bi bi-gear-fill me-2"></i>Admin Panel
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.dashboard' %}active{% endif %}"
href="{{ url_for('admin.dashboard') }}">
<i class="bi bi-graph-up me-1"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.users' %}active{% endif %}"
href="{{ url_for('admin.users') }}">
<i class="bi bi-people me-1"></i>Users
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.quotations' %}active{% endif %}"
href="{{ url_for('admin.quotations') }}">
<i class="bi bi-file-earmark-text me-1"></i>Quotations
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.cost_factors' %}active{% endif %}"
href="{{ url_for('admin.cost_factors') }}">
<i class="bi bi-calculator me-1"></i>Cost Factors
</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">
<i class="bi bi-box-arrow-right me-1"></i>Logout
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid py-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
{% block scripts %}
<!-- Bootstrap Bundle with Popper -->
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<!-- jQuery -->
<script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
{% endblock %}
</body>
</html>
@@ -1,198 +0,0 @@
{% extends "admin/base.html" %}
{% block admin_title %}Cost Factor History{% endblock %}
{% block admin_header %}Change History: {{ factor.name }}{% endblock %}
{% block admin_actions %}
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Cost Factors
</a>
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-primary">
<i class="fas fa-edit"></i> Edit Factor
</a>
{% endblock %}
{% block admin_content %}
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Factor Information</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">Name</dt>
<dd class="col-sm-8">{{ factor.name }}</dd>
<dt class="col-sm-4">Category</dt>
<dd class="col-sm-8">
<span class="badge bg-primary">{{ factor.category }}</span>
</dd>
<dt class="col-sm-4">Current Value</dt>
<dd class="col-sm-8">
<span class="badge bg-success">{{ "%.4f"|format(factor.factor_value) }}</span>
</dd>
<dt class="col-sm-4">Status</dt>
<dd class="col-sm-8">
{% if factor.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</dd>
<dt class="col-sm-4">Created</dt>
<dd class="col-sm-8">{{ factor.created_at.strftime('%Y-%m-%d %H:%M') if factor.created_at else 'Unknown' }}</dd>
<dt class="col-sm-4">Last Updated</dt>
<dd class="col-sm-8">{{ factor.updated_at.strftime('%Y-%m-%d %H:%M') if factor.updated_at else 'Never' }}</dd>
</dl>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Change History</h5>
</div>
<div class="card-body">
{% if history %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Date & Time</th>
<th>Changed By</th>
<th>Old Value</th>
<th>New Value</th>
<th>Change</th>
<th>Reason</th>
</tr>
</thead>
<tbody>
{% for change in history %}
<tr>
<td>{{ change.changed_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
{% if change.user %}
<span class="badge bg-info">{{ change.user.username }}</span>
{% else %}
<span class="text-muted">Unknown</span>
{% endif %}
</td>
<td>
{% if change.old_value is not none %}
<span class="badge bg-secondary">{{ "%.4f"|format(change.old_value) }}</span>
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</td>
<td>
<span class="badge bg-success">{{ "%.4f"|format(change.new_value) }}</span>
</td>
<td>
{% if change.old_value is not none %}
{% set diff = change.new_value - change.old_value %}
{% if diff > 0 %}
<span class="badge bg-danger">+{{ "%.4f"|format(diff) }}</span>
{% elif diff < 0 %}
<span class="badge bg-warning">{{ "%.4f"|format(diff) }}</span>
{% else %}
<span class="badge bg-secondary">No change</span>
{% endif %}
{% else %}
<span class="badge bg-info">Initial</span>
{% endif %}
</td>
<td>
{% if change.change_reason %}
<small class="text-muted">{{ change.change_reason }}</small>
{% else %}
<span class="text-muted">No reason provided</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-history fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Changes Yet</h5>
<p class="text-muted">This factor hasn't been modified since it was created.</p>
</div>
{% endif %}
</div>
</div>
{% if history %}
<div class="card mt-3">
<div class="card-header">
<h5 class="mb-0">Summary</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<h4 class="text-primary">{{ history|length }}</h4>
<small class="text-muted">Total Changes</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-success">{{ history|selectattr('change_reason')|list|length }}</h4>
<small class="text-muted">With Reasons</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-info">{{ history|selectattr('user')|list|length }}</h4>
<small class="text-muted">By Known Users</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-warning">{{ (history|last).changed_at.strftime('%Y-%m-%d') if history else 'N/A' }}</h4>
<small class="text-muted">Last Modified</small>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.badge {
font-size: 0.875em;
}
dl.row dt {
font-weight: 600;
color: #6c757d;
}
dl.row dd {
margin-bottom: 0.5rem;
}
.table th {
background-color: #f8f9fa;
border-top: none;
font-weight: 600;
}
.text-center h4 {
margin-bottom: 0.25rem;
}
</style>
{% endblock %}
-558
View File
@@ -1,558 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Cost Factors Management{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">
<i class="bi bi-gear-fill me-2 text-primary"></i>Cost Factors Management
</h1>
<p class="text-muted mb-0">Configure calculation parameters and factors</p>
</div>
<div class="d-flex gap-2">
<button onclick="resetFactors()" class="btn btn-warning">
<i class="bi bi-arrow-clockwise me-1"></i>Reset to Defaults
</button>
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i>Back to Dashboard
</a>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Calculation Logic Diagram -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-light border-0">
<h3 class="card-title mb-0">
<i class="bi bi-diagram-3 me-2 text-dark"></i>Calculation Logic
</h3>
</div>
<div class="card-body">
<div class="mermaid">
graph TD
A[Base Cost] --> B[Base Factors]
B --> C[Mold Factors]
C --> D[Complexity Factors]
D --> E[Texture Factors]
E --> F[Tax Factors]
F --> G[Final Cost]
B --> B1[Base Factor 1]
B --> B2[Base Factor 2]
C --> C1[Mold Factor 1]
C --> C2[Mold Factor 2]
D --> D1[Complexity Factor 1]
D --> D2[Complexity Factor 2]
E --> E1[Texture Factor 1]
E --> E2[Texture Factor 2]
F --> F1[Tax Rate]
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-primary bg-opacity-10 mx-auto mb-3">
<i class="bi bi-calculator text-primary fs-1"></i>
</div>
<h4 class="mb-1">{{ base_factors|length if base_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Base Factors</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-success bg-opacity-10 mx-auto mb-3">
<i class="bi bi-building text-success fs-1"></i>
</div>
<h4 class="mb-1">{{ mold_factors|length if mold_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Mold Factors</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-warning bg-opacity-10 mx-auto mb-3">
<i class="bi bi-puzzle text-warning fs-1"></i>
</div>
<h4 class="mb-1">{{ complexity_factors|length if complexity_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Complexity</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-info bg-opacity-10 mx-auto mb-3">
<i class="bi bi-palette text-info fs-1"></i>
</div>
<h4 class="mb-1">{{ texture_factors|length if texture_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Texture</p>
</div>
</div>
</div>
<div class="col-xl-2 col-md-4 col-sm-6 mb-3">
<div class="card statistics-card border-0 shadow-sm h-100">
<div class="card-body text-center">
<div class="icon-wrapper bg-danger bg-opacity-10 mx-auto mb-3">
<i class="bi bi-percent text-danger fs-1"></i>
</div>
<h4 class="mb-1">{{ tax_factors|length if tax_factors else 0 }}</h4>
<p class="text-muted mb-0 small">Tax Factors</p>
</div>
</div>
</div>
</div>
<!-- Cost Factors Sections -->
<div class="row">
<div class="col-12">
<!-- Base Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-primary bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-layers me-2 text-primary"></i>Base Factors
<span class="badge bg-primary ms-2">{{ base_factors|length if base_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if base_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in base_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-calculator text-primary"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-primary fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Base calculation factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No base factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Mold Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-success bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-building me-2 text-success"></i>Mold Factors
<span class="badge bg-success ms-2">{{ mold_factors|length if mold_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if mold_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in mold_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-box text-success"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-success fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Mold-related factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-success">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No mold factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Complexity Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-warning bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-puzzle me-2 text-warning"></i>Complexity Factors
<span class="badge bg-warning ms-2">{{ complexity_factors|length if complexity_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if complexity_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in complexity_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-gear text-warning"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-warning fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Complexity-related factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-warning">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No complexity factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Texture Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-info bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-brush me-2 text-info"></i>Texture Factors
<span class="badge bg-info ms-2">{{ texture_factors|length if texture_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if texture_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Factor Name</th>
<th class="border-0">Value</th>
<th class="border-0">Description</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in texture_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-palette text-info"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.category }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-info fs-6">{{ "%.2f"|format(factor.factor_value) }}</span>
</td>
<td>{{ factor.description or 'Texture-related factor' }}</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No texture factors found.</p>
</div>
{% endif %}
</div>
</div>
<!-- Tax Factors -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-danger bg-opacity-10 border-0">
<h3 class="card-title mb-0">
<i class="bi bi-percent me-2 text-danger"></i>Tax Factors
<span class="badge bg-danger ms-2">{{ tax_factors|length if tax_factors else 0 }}</span>
</h3>
</div>
<div class="card-body">
{% if tax_factors %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Country/Region</th>
<th class="border-0">Tax Rate</th>
<th class="border-0 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for factor in tax_factors %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-danger bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-flag text-danger"></i>
</div>
<div>
<div class="fw-medium">{{ factor.name }}</div>
<small class="text-muted">{{ factor.description or 'Tax rate' }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-danger fs-6">{{ "%.1f"|format(factor.factor_value * 100) }}%</span>
</td>
<td class="text-end">
<a href="{{ url_for('admin.edit_cost_factor', factor_id=factor.id) }}" class="btn btn-sm btn-outline-danger">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-clock-history me-1"></i>History
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-exclamation-triangle fs-1 text-warning mb-3"></i>
<p class="text-muted mb-0">No tax factors found.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Add Mermaid.js for diagram rendering -->
<script src="{{ url_for('static', filename='js/mermaid.min.js') }}"></script>
<script>
mermaid.initialize({
startOnLoad: true,
theme: 'default',
securityLevel: 'loose',
fontFamily: '"Helvetica Neue", Arial, sans-serif'
});
document.addEventListener('DOMContentLoaded', function() {
console.log('Cost Factors Template Loaded');
try {
const debugInfo = {
baseFactors: {{ base_factors|length if base_factors else 0 }},
moldFactors: {{ mold_factors|length if mold_factors else 0 }},
complexityFactors: {{ complexity_factors|length if complexity_factors else 0 }},
textureFactors: {{ texture_factors|length if texture_factors else 0 }},
taxFactors: {{ tax_factors|length if tax_factors else 0 }}
};
console.log('Debug Info:', debugInfo);
} catch (error) {
console.error('Error in debug logging:', error);
}
});
function resetFactors() {
if (confirm('Are you sure you want to reset all cost factors to their default values? This action cannot be undone.')) {
fetch('{{ url_for("admin.reset_cost_factors") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
alert('Error resetting factors. Please try again.');
}
}).catch(error => {
console.error('Error:', error);
alert('Error resetting factors. Please try again.');
});
}
}
</script>
{% block styles %}
{{ super() }}
<style>
/* Card Styles */
.card {
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
/* Table Styles */
.table th {
font-weight: 600;
color: #495057;
}
.table td {
vertical-align: middle;
}
/* Badge Styles */
.badge {
padding: 0.5em 0.8em;
}
.badge.fs-6 {
font-size: 0.875rem !important;
}
/* Icon Styles */
.avatar-sm i {
font-size: 1.25rem;
}
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fadeIn 0.3s ease-out;
}
/* Statistics Cards */
.statistics-card {
border-radius: 1rem;
overflow: hidden;
}
.statistics-card .icon-wrapper {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
/* Mermaid Diagram */
.mermaid {
background: white;
padding: 1rem;
border-radius: 0.5rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.statistics-card {
margin-bottom: 1rem;
}
.table-responsive {
margin: 0 -1rem;
}
}
</style>
{% endblock %}
{% endblock %}
-245
View File
@@ -1,245 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Admin Dashboard{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">
<i class="bi bi-speedometer2 me-2 text-primary"></i>Admin Dashboard
</h1>
<p class="text-muted mb-0">Overview of system statistics and recent activity</p>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Total Users</h6>
<h3 class="mb-0">{{ total_users }}</h3>
</div>
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
<i class="bi bi-people text-primary fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>{{ active_users }} active
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Total Quotations</h6>
<h3 class="mb-0">{{ total_quotations }}</h3>
</div>
<div class="bg-success bg-opacity-10 rounded-circle p-3">
<i class="bi bi-file-earmark-text text-success fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-info">
<i class="bi bi-graph-up me-1"></i>All time
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Active Regions</h6>
<h3 class="mb-0">{{ active_regions }}</h3>
</div>
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
<i class="bi bi-globe text-warning fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-warning">
<i class="bi bi-flag me-1"></i>Active regions
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted mb-1">Recent Quotes</h6>
<h3 class="mb-0">{{ recent_quotations|length }}</h3>
</div>
<div class="bg-info bg-opacity-10 rounded-circle p-3">
<i class="bi bi-clock text-info fs-1"></i>
</div>
</div>
<div class="mt-3">
<span class="badge bg-info">
<i class="bi bi-calendar me-1"></i>Last 5 quotes
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Recent Activity -->
<div class="col-lg-8 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-graph-up me-2 text-primary"></i>Recent Activity
</h5>
</div>
<div class="card-body">
{% if recent_quotations %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Project</th>
<th>Amount</th>
<th>Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for quotation in recent_quotations %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 32px; height: 32px;">
<i class="bi bi-person text-primary"></i>
</div>
<div>
<div class="fw-medium">{{ quotation.user.username }}</div>
<small class="text-muted">{{ quotation.user.email }}</small>
</div>
</div>
</td>
<td>{{ quotation.project_name }}</td>
<td>
<span class="badge bg-success">${{ "%.2f"|format(quotation.total_cost) }}</span>
</td>
<td>{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<span class="badge bg-primary">Completed</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-center mt-3">
<a href="{{ url_for('admin.quotations') }}" class="btn btn-outline-primary">
<i class="bi bi-eye me-1"></i>View All Quotations
</a>
</div>
{% else %}
<div class="text-center py-4">
<i class="bi bi-file-earmark-text fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">No recent quotations found.</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-lg-4 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-lightning me-2 text-warning"></i>Quick Actions
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-primary">
<i class="bi bi-people me-2"></i>Manage Users
</a>
<a href="{{ url_for('admin.quotations') }}" class="btn btn-outline-success">
<i class="bi bi-file-earmark-text me-2"></i>View Quotations
</a>
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-warning">
<i class="bi bi-gear me-2"></i>Cost Factors
</a>
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-info">
<i class="bi bi-calculator me-2"></i>Calculator Settings
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.card {
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.avatar-sm {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.table th {
font-weight: 600;
color: #495057;
}
.table td {
vertical-align: middle;
}
.badge {
font-size: 0.875em;
}
.btn {
border-radius: 0.375rem;
}
</style>
{% endblock %}
-238
View File
@@ -1,238 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Edit Cost Factor: {{ factor.name }}{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">
<i class="bi bi-pencil-square me-2 text-primary"></i>Edit Cost Factor
</h1>
<p class="text-muted mb-0">{{ factor.name }} ({{ factor.category }})</p>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Back to Cost Factors
</a>
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="row">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-gear me-2"></i>Edit Factor Details
</h5>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="category" class="form-label fw-semibold">Category</label>
<input type="text" class="form-control bg-light" id="category" value="{{ factor.category }}" readonly>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="name" class="form-label fw-semibold">Name</label>
<input type="text" class="form-control bg-light" id="name" value="{{ factor.name }}" readonly>
</div>
</div>
</div>
<div class="mb-3">
<label for="factor_value" class="form-label fw-semibold">
Factor Value <span class="text-danger">*</span>
</label>
<input type="number" class="form-control form-control-lg" id="factor_value" name="factor_value"
value="{{ "%.4f"|format(factor.factor_value) }}" step="0.0001" min="0" required>
<div class="form-text">
{% if factor.category == 'tax' %}
<i class="bi bi-info-circle me-1"></i>Enter as decimal (e.g., 0.06 for 6%)
{% elif factor.category == 'base' %}
<i class="bi bi-info-circle me-1"></i>Enter the base price value
{% else %}
<i class="bi bi-info-circle me-1"></i>Enter the multiplier factor
{% endif %}
</div>
</div>
{% if factor.calculation_formula %}
<div class="mb-3">
<label for="calculation_formula" class="form-label fw-semibold">Calculation Formula</label>
<input type="text" class="form-control bg-light" id="calculation_formula"
value="{{ factor.calculation_formula }}" readonly>
</div>
{% endif %}
{% if factor.percentage %}
<div class="mb-3">
<label for="percentage" class="form-label fw-semibold">Percentage</label>
<input type="text" class="form-control bg-light" id="percentage"
value="{{ factor.percentage }}" readonly>
</div>
{% endif %}
<div class="mb-3">
<label for="description" class="form-label fw-semibold">Description</label>
<textarea class="form-control bg-light" id="description" rows="3" readonly>{{ factor.description }}</textarea>
</div>
<div class="mb-4">
<label for="change_reason" class="form-label fw-semibold">
<i class="bi bi-chat-text me-1"></i>Reason for Change
</label>
<textarea class="form-control" id="change_reason" name="change_reason" rows="3"
placeholder="Please provide a reason for this change (optional but recommended)"></textarea>
<div class="form-text">
<i class="bi bi-clock-history me-1"></i>This will be recorded in the change history for audit purposes.
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('admin.cost_factors') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle me-1"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i> Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-info-circle me-2"></i>Current Information
</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-4 fw-semibold">Category</dt>
<dd class="col-sm-8">
<span class="badge bg-primary">{{ factor.category }}</span>
</dd>
<dt class="col-sm-4 fw-semibold">Name</dt>
<dd class="col-sm-8">{{ factor.name }}</dd>
<dt class="col-sm-4 fw-semibold">Current Value</dt>
<dd class="col-sm-8">
<span class="badge bg-success fs-6">{{ "%.4f"|format(factor.factor_value) }}</span>
</dd>
<dt class="col-sm-4 fw-semibold">Status</dt>
<dd class="col-sm-8">
{% if factor.is_active %}
<span class="badge bg-success">
<i class="bi bi-check-circle me-1"></i>Active
</span>
{% else %}
<span class="badge bg-danger">
<i class="bi bi-x-circle me-1"></i>Inactive
</span>
{% endif %}
</dd>
<dt class="col-sm-4 fw-semibold">Created</dt>
<dd class="col-sm-8">{{ factor.created_at.strftime('%Y-%m-%d %H:%M') if factor.created_at else 'Unknown' }}</dd>
<dt class="col-sm-4 fw-semibold">Last Updated</dt>
<dd class="col-sm-8">{{ factor.updated_at.strftime('%Y-%m-%d %H:%M') if factor.updated_at else 'Never' }}</dd>
{% if factor.updater %}
<dt class="col-sm-4 fw-semibold">Updated By</dt>
<dd class="col-sm-8">{{ factor.updater.username }}</dd>
{% endif %}
</dl>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-light border-0">
<h5 class="card-title mb-0">
<i class="bi bi-lightning me-2"></i>Quick Actions
</h5>
</div>
<div class="card-body">
<a href="{{ url_for('admin.cost_factor_history', factor_id=factor.id) }}"
class="btn btn-outline-info btn-sm w-100 mb-2">
<i class="bi bi-clock-history me-1"></i> View Change History
</a>
<a href="{{ url_for('admin.cost_factors') }}"
class="btn btn-outline-secondary btn-sm w-100">
<i class="bi bi-list me-1"></i> Back to All Factors
</a>
</div>
</div>
</div>
</div>
</div>
<style>
.form-control[readonly] {
background-color: #f8f9fa !important;
border-color: #dee2e6;
color: #6c757d;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
}
.badge {
font-size: 0.875em;
}
dl.row dt {
color: #6c757d;
font-size: 0.9rem;
}
dl.row dd {
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.card {
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-1px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.btn {
border-radius: 0.375rem;
}
.form-control-lg {
font-size: 1.1rem;
padding: 0.75rem 1rem;
}
</style>
{% endblock %}
-146
View File
@@ -1,146 +0,0 @@
{% extends "admin/base.html" %}
{% block admin_title %}Quotation Details{% endblock %}
{% block admin_header %}Quotation Details{% endblock %}
{% 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 %}
{{ render_quotation_details(quotation) }}
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.quotation-details {
display: flex;
flex-direction: column;
gap: 30px;
}
.detail-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.detail-card h3 {
margin: 0 0 20px;
color: #2c3e50;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.detail-item {
display: flex;
flex-direction: column;
gap: 5px;
}
.detail-item label {
color: #6c757d;
font-size: 0.9rem;
}
.detail-item span {
font-size: 1.1rem;
color: #2c3e50;
}
.cost-breakdown {
display: flex;
flex-direction: column;
gap: 15px;
}
.cost-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #dee2e6;
}
.cost-item:last-child {
border-bottom: none;
}
.cost-item label {
color: #6c757d;
font-size: 1rem;
}
.cost-item span {
font-size: 1.1rem;
color: #2c3e50;
font-weight: 500;
}
.cost-item.total {
margin-top: 10px;
padding-top: 20px;
border-top: 2px solid #dee2e6;
}
.cost-item.total label {
font-size: 1.2rem;
font-weight: 600;
color: #2c3e50;
}
.cost-item.total span {
font-size: 1.4rem;
font-weight: 700;
color: #28a745;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 5px;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
.badge-success {
background-color: #28a745;
color: white;
}
@media (max-width: 768px) {
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
-174
View File
@@ -1,174 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Quotations - Admin{% endblock %}
{% block content %}
<div class="admin-container">
<div class="admin-card">
<div class="admin-card-header">
<h2 class="mb-0">Quotations Management</h2>
</div>
<div class="admin-card-body">
<div class="admin-filters">
<form method="get" class="admin-filter-form" id="quotationFilterForm">
<div class="admin-form-group">
<select name="country" class="admin-form-control">
<option value="">All Countries</option>
{% for country in countries %}
<option value="{{ country }}" {% if filters.country == country %}selected{% endif %}>
{{ country }}
</option>
{% endfor %}
</select>
</div>
<div class="admin-form-group">
<select name="mold_type" class="admin-form-control">
<option value="">All Mold Types</option>
{% for type in mold_types %}
<option value="{{ type }}" {% if filters.mold_type == type %}selected{% endif %}>
{{ type }}
</option>
{% endfor %}
</select>
</div>
<div class="admin-form-group">
<input type="date" name="date_from" id="date_from" class="admin-form-control"
value="{{ filters.date_from.strftime('%Y-%m-%d') if filters.date_from else '' }}"
placeholder="From Date">
</div>
<div class="admin-form-group">
<input type="date" name="date_to" id="date_to" class="admin-form-control"
value="{{ filters.date_to.strftime('%Y-%m-%d') if filters.date_to else '' }}"
placeholder="To Date">
</div>
<div class="admin-form-group">
<button type="submit" class="admin-btn admin-btn-primary">Filter</button>
<a href="{{ url_for('admin.quotations') }}" class="admin-btn admin-btn-secondary">Clear</a>
</div>
</form>
</div>
{% if quotations.items %}
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>User</th>
<th>Model</th>
<th>Country</th>
<th>Mold Type</th>
<th>Base Price</th>
<th>Total Cost</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for quotation in quotations.items %}
<tr>
<td>{{ quotation.id }}</td>
<td>{{ quotation.user.username }}</td>
<td>{{ quotation.model_name }}</td>
<td>{{ quotation.mold_shop_country }}</td>
<td>{{ quotation.mold_main_type }}</td>
<td>${{ "%.2f"|format(quotation.base_price) }}</td>
<td>${{ "%.2f"|format(quotation.total_cost) }}</td>
<td>{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<a href="{{ url_for('admin.quotation_detail', id=quotation.id) }}"
class="admin-btn admin-btn-info admin-btn-sm">
<i class="fas fa-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if quotations.pages > 1 %}
<div class="admin-pagination">
{% if quotations.has_prev %}
<a href="{{ url_for('admin.quotations', page=quotations.prev_num, **filters) }}"
class="admin-pagination-link">Previous</a>
{% endif %}
<span class="admin-pagination-info">
Page {{ quotations.page }} of {{ quotations.pages }}
</span>
{% if quotations.has_next %}
<a href="{{ url_for('admin.quotations', page=quotations.next_num, **filters) }}"
class="admin-pagination-link">Next</a>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="text-center py-4">
<p class="text-muted mb-0">No quotations found matching your criteria.</p>
{% if filters.country or filters.mold_type or filters.date_from or filters.date_to %}
<a href="{{ url_for('admin.quotations') }}" class="admin-btn admin-btn-primary mt-3">Clear Filters</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Function to get today's date in YYYY-MM-DD format
function getTodayDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// Get date input elements
const dateFromInput = document.getElementById('date_from');
const dateToInput = document.getElementById('date_to');
const form = document.getElementById('quotationFilterForm');
// Set default values if empty
if (!dateFromInput.value) {
dateFromInput.value = getTodayDate();
}
if (!dateToInput.value) {
dateToInput.value = getTodayDate();
}
// Prevent form submission if dates are invalid
form.addEventListener('submit', function(event) {
if (!dateFromInput.value || !dateToInput.value) {
event.preventDefault();
if (!dateFromInput.value) dateFromInput.value = getTodayDate();
if (!dateToInput.value) dateToInput.value = getTodayDate();
}
});
// Add event listeners to handle invalid dates
dateFromInput.addEventListener('change', function() {
if (!this.value) this.value = getTodayDate();
});
dateToInput.addEventListener('change', function() {
if (!this.value) this.value = getTodayDate();
});
});
</script>
{% endblock %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin.css') }}">
{% endblock %}
-1
View File
@@ -1 +0,0 @@
-253
View File
@@ -1,253 +0,0 @@
{% extends "admin/base.html" %}
{% block title %}Users - Admin Panel{% endblock %}
{% block content %}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="get" class="row g-3 align-items-end">
<div class="col-md-8">
<label for="search" class="form-label">Search Users</label>
<input type="text" id="search" name="search" placeholder="Search by username or email..."
value="{{ request.args.get('search', '') }}" class="form-control">
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-search me-2"></i>Search
</button>
</div>
</form>
</div>
</div>
<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-users me-2 text-primary"></i>
User Management
</h5>
</div>
<div class="card-body p-0">
<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">Username</th>
<th class="border-0">Email</th>
<th class="border-0">Status</th>
<th class="border-0">Role</th>
<th class="border-0">Last Login</th>
<th class="border-0">Created</th>
<th class="border-0 text-center">Actions</th>
</tr>
</thead>
<tbody>
{% for user in users.items %}
<tr>
<td>
<span class="badge bg-secondary">#{{ user.id }}</span>
</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="fas fa-user text-primary"></i>
</div>
<span class="fw-medium">{{ user.username }}</span>
</div>
</td>
<td>
<span class="text-muted">{{ user.email }}</span>
</td>
<td>
<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>
</td>
<td>
<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>
</td>
<td>
<small class="text-muted">
{{ user.last_login.strftime('%m/%d/%Y %H:%M') if user.last_login else 'Never' }}
</small>
</td>
<td>
<small class="text-muted">
{{ user.created_at.strftime('%m/%d/%Y') }}
</small>
</td>
<td class="text-center">
<div class="btn-group" role="group">
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
class="btn btn-outline-info btn-sm"
title="View Details">
<i class="fas fa-eye"></i>
</a>
<button type="button"
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')"
title="{{ 'Deactivate' if user.is_active else 'Activate' }} User">
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i>
</button>
{% if user.id != current_user.id %}
<button type="button"
class="btn btn-outline-danger btn-sm"
onclick="deleteUser({{ user.id }})"
title="Delete User">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pagination -->
{% if users.pages > 1 %}
<div class="d-flex justify-content-between align-items-center mt-4">
<div class="text-muted">
Showing {{ users.items|length }} of {{ users.total }} users
</div>
<nav aria-label="User pagination">
<ul class="pagination pagination-sm mb-0">
{% if users.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.prev_num, search=request.args.get('search', '')) }}">
<i class="fas fa-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in users.iter_pages() %}
{% if page_num %}
{% if page_num != users.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=page_num, search=request.args.get('search', '')) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if users.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.users', page=users.next_num, search=request.args.get('search', '')) }}">
<i class="fas fa-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
function toggleUserStatus(userId, status) {
const button = event.target.closest('button');
const isActive = status === 'warning';
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');
});
}
function deleteUser(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: userId })
})
.then(response => response.json())
.then(data => {
if (data.message) {
location.reload();
} else {
alert('Error: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the user');
});
}
</script>
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.avatar-sm {
font-size: 0.875rem;
}
.table th {
font-weight: 600;
color: #495057;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table td {
vertical-align: middle;
}
.btn-group .btn {
margin-right: 2px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
</style>
{% endblock %}
-168
View File
@@ -1,168 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h2 class="mb-4">Admin Management</h2>
<!-- User Management Section -->
<div class="card mb-4">
<div class="card-header">
<h3 class="mb-0">User Management</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Company</th>
<th>Registration Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.company }}</td>
<td>{{ user.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
{% if user.is_admin %}
<span class="badge bg-primary">Admin</span>
{% else %}
<span class="badge bg-success">User</span>
{% endif %}
</td>
<td>
{% if not user.is_admin %}
<button class="btn btn-danger btn-sm"
onclick="deleteUser({{ user.id }})"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
Delete
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Quotation Management Section -->
<div class="card">
<div class="card-header">
<h3 class="mb-0">Quotation Management</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>User</th>
<th>Model</th>
<th>Tooling ID</th>
<th>Total Cost</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for quotation in quotations %}
<tr>
<td>{{ quotation.id }}</td>
<td>{{ quotation.user.username }}</td>
<td>{{ quotation.model_name }}</td>
<td>{{ quotation.tooling_id }}</td>
<td>${{ "%.2f"|format(quotation.total_cost) }}</td>
<td>{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<button class="btn btn-danger btn-sm"
onclick="deleteQuotation({{ quotation.id }})"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
Delete
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this item?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDelete">Delete</button>
</div>
</div>
</div>
</div>
<script>
let itemToDelete = null;
let deleteType = null;
function deleteUser(userId) {
itemToDelete = userId;
deleteType = 'user';
}
function deleteQuotation(quoteId) {
itemToDelete = quoteId;
deleteType = 'quotation';
}
document.getElementById('confirmDelete').addEventListener('click', function() {
if (!itemToDelete || !deleteType) return;
const url = deleteType === 'user' ? '/admin/delete_user' : '/admin/delete_quotation';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
},
body: JSON.stringify({ id: itemToDelete })
})
.then(response => response.json())
.then(data => {
if (data.message) {
location.reload();
} else {
alert('Error deleting item');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error deleting item');
})
.finally(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteModal'));
modal.hide();
});
});
</script>
{% endblock %}
-128
View File
@@ -1,128 +0,0 @@
{% extends "base.html" %}
{% block title %}Admin Portal - MoldCost Pro{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-lock me-2"></i>Quotation Records</h2>
<div>
<a href="{{ url_for('calculator') }}" class="btn btn-secondary">
<i class="fas fa-calculator me-2"></i>Calculator
</a>
<a href="{{ url_for('logout') }}" class="btn btn-danger">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</a>
</div>
</div>
<div class="card shadow">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Company</th>
<th>User</th>
<th class="text-end">Model</th>
<th class="text-end">Tooling ID</th>
<th class="text-end">Total</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{% for quotation in quotations %}
<tr>
<td>{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ quotation.user.company }}</td>
<td>{{ quotation.user.username }}</td>
<td class="text-end">{{ quotation.model_name }}</td>
<td class="text-end">{{ quotation.tooling_id }}</td>
<td class="text-end fw-bold">${{ "{:,.2f}".format(quotation.total_cost) }}</td>
<td>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
data-bs-target="#detailModal{{ quotation.id }}">
<i class="fas fa-search"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% for quotation in quotations %}
<div class="modal fade" id="detailModal{{ quotation.id }}" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Quotation Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row mb-4">
<div class="col-md-6">
<h6 class="text-muted mb-3">Basic Information</h6>
<dl class="row">
<dt class="col-sm-4">Date</dt>
<dd class="col-sm-8">{{ quotation.created_at.strftime('%Y-%m-%d %H:%M') }}</dd>
<dt class="col-sm-4">Company</dt>
<dd class="col-sm-8">{{ quotation.user.company }}</dd>
<dt class="col-sm-4">User</dt>
<dd class="col-sm-8">{{ quotation.user.username }}</dd>
<dt class="col-sm-4">Model</dt>
<dd class="col-sm-8">{{ quotation.model_name }}</dd>
<dt class="col-sm-4">Tooling ID</dt>
<dd class="col-sm-8">{{ quotation.tooling_id }}</dd>
</dl>
</div>
<div class="col-md-6">
<h6 class="text-muted mb-3">Mold Specifications</h6>
<dl class="row">
<dt class="col-sm-4">Mold Type</dt>
<dd class="col-sm-8">{{ quotation.mold_main_type }} - {{ quotation.mold_sub_type }}</dd>
<dt class="col-sm-4">Cavities</dt>
<dd class="col-sm-8">{{ quotation.cavity_count }} cavity</dd>
<dt class="col-sm-4">Sliders</dt>
<dd class="col-sm-8">{{ quotation.sliders_count }} slider</dd>
<dt class="col-sm-4">Texture</dt>
<dd class="col-sm-8">{{ quotation.digital_texture_type }}</dd>
<dt class="col-sm-4">High Sidewall</dt>
<dd class="col-sm-8">{{ 'Yes' if quotation.complexity_high_sidewall else 'No' }}</dd>
</dl>
</div>
</div>
<div class="row">
<div class="col-12">
<h6 class="text-muted mb-3">Cost Breakdown</h6>
<dl class="row">
<dt class="col-sm-4">Base Price</dt>
<dd class="col-sm-8">${{ "{:,.2f}".format(quotation.net_base_price) }}</dd>
<dt class="col-sm-4">Tax Rate</dt>
<dd class="col-sm-8">{{ "{:.1%}".format(quotation.tax_rate) }}</dd>
<dt class="col-sm-4">Total Cost</dt>
<dd class="col-sm-8 fw-bold">${{ "{:,.2f}".format(quotation.total_cost) }}</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}
-58
View File
@@ -1,58 +0,0 @@
{% extends "base.html" %}
{% block title %}Login - Mold Cost Calculator{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Login</h4>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.login') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
{% for error in form.email.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
{% for error in form.password.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">Don't have an account? <a href="{{ url_for('auth.register') }}">Register here</a></p>
<p class="mb-0 mt-2"><a href="{{ url_for('auth.request_reset') }}">Forgot your password?</a></p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
-77
View File
@@ -1,77 +0,0 @@
{% extends "base.html" %}
{% block title %}Register - Mold Cost Calculator{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Register</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('auth.register') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
{% for error in form.username.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
{% for error in form.email.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.company.label(class="form-label") }}
{{ form.company(class="form-control" + (" is-invalid" if form.company.errors else "")) }}
{% for error in form.company.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
{% for error in form.password.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password_confirm.label(class="form-label") }}
{{ form.password_confirm(class="form-control" + (" is-invalid" if form.password_confirm.errors else "")) }}
{% for error in form.password_confirm.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3 form-check">
{{ form.terms(class="form-check-input" + (" is-invalid" if form.terms.errors else "")) }}
{{ form.terms.label(class="form-check-label") }}
{% for error in form.terms.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">Already have an account? <a href="{{ url_for('auth.login') }}">Login here</a></p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
-44
View File
@@ -1,44 +0,0 @@
{% extends "base.html" %}
{% block title %}Request Password Reset - Mold Cost Calculator{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Request Password Reset</h4>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.request_reset') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
{% for error in form.email.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">Remember your password? <a href="{{ url_for('auth.login') }}">Login here</a></p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
-52
View File
@@ -1,52 +0,0 @@
{% extends "base.html" %}
{% block title %}Reset Password - Mold Cost Calculator{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Reset Password</h4>
</div>
<div class="card-body">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
{% for error in form.password.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password_confirm.label(class="form-label") }}
{{ form.password_confirm(class="form-control" + (" is-invalid" if form.password_confirm.errors else "")) }}
{% for error in form.password_confirm.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="d-grid gap-2">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">Remember your password? <a href="{{ url_for('auth.login') }}">Login here</a></p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
-156
View File
@@ -1,156 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}{% endblock %} - Mold Cost Calculator</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🧮</text></svg>">
<!-- Bootstrap CSS -->
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap-icons.css') }}">
<!-- Custom Styles -->
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<!-- Custom Styles -->
<style>
:root {
--primary-color: #0d6efd;
--secondary-color: #6c757d;
--accent-color: #0d6efd;
--success-color: #198754;
--warning-color: #ffc107;
--danger-color: #dc3545;
--light-color: #f8f9fa;
--dark-color: #212529;
--text-color: #212529;
--text-muted: #6c757d;
--border-color: #dee2e6;
--shadow-color: rgba(0, 0, 0, 0.1);
}
body {
font-family: 'Inter', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: #f8f9fa;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.navbar {
background-color: var(--primary-color);
padding: 1rem 0;
box-shadow: 0 2px 4px var(--shadow-color);
position: sticky;
top: 0;
z-index: 1000;
}
.navbar-brand {
color: white;
text-decoration: none;
font-size: 1.5rem;
font-weight: 600;
}
.nav-link {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: all 0.3s ease;
}
.nav-link:hover {
color: white;
background-color: rgba(255, 255, 255, 0.2);
}
.nav-link.active {
color: white;
background-color: rgba(255, 255, 255, 0.2);
}
.main-content {
flex: 1;
padding: 2rem 0;
}
.footer {
background-color: var(--primary-color);
color: white;
padding: 1rem 0;
text-align: center;
margin-top: auto;
}
</style>
{% block head %}{% endblock %}
{% block styles %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container">
<a class="navbar-brand" href="{{ url_for('main.home') }}">
<i class="bi bi-calculator me-2"></i>Mold Cost Calculator
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.calculator' %}active{% endif %}"
href="{{ url_for('main.calculator') }}">
<i class="bi bi-calculator me-1"></i>Calculator
</a>
</li>
</ul>
{% if current_user.is_authenticated %}
<div class="d-flex align-items-center">
<span class="text-white me-3">
<i class="bi bi-person me-2"></i>{{ current_user.username }}
</span>
<a href="{{ url_for('auth.logout') }}" class="btn btn-outline-light">
<i class="bi bi-box-arrow-right me-2"></i>Logout
</a>
</div>
{% endif %}
</div>
</div>
</nav>
<main class="main-content">
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</main>
<footer class="footer">
<div class="container">
<p class="mb-0">PCT & Tooling. All rights reserved.</p>
</div>
</footer>
<!-- Bootstrap Bundle with Popper -->
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<!-- jQuery -->
<script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
-215
View File
@@ -1,215 +0,0 @@
{% 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 %}
-30
View File
@@ -1,30 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">Error {{ code }}</h4>
</div>
<div class="card-body">
<h5 class="card-title">{{ message }}</h5>
<p class="card-text">
{% if code == 404 %}
The page you're looking for doesn't exist.
{% elif code == 403 %}
You don't have permission to access this resource.
{% elif code == 400 %}
The request was invalid.
{% else %}
An unexpected error occurred. Please try again later.
{% endif %}
</p>
<a href="{{ url_for('main.home') }}" class="btn btn-primary">Return to Home</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
-14
View File
@@ -1,14 +0,0 @@
{% extends "base.html" %}
{% block title %}Bad Request - MoldCost Pro{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="text-center">
<h1 class="display-1">400</h1>
<h2 class="mb-4">Bad Request</h2>
<p class="lead">The request was invalid or cannot be served.</p>
<a href="{{ url_for('main.home') }}" class="btn btn-primary">Return Home</a>
</div>
</div>
{% endblock %}
-14
View File
@@ -1,14 +0,0 @@
{% extends "base.html" %}
{% block title %}Forbidden - MoldCost Pro{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="text-center">
<h1 class="display-1">403</h1>
<h2 class="mb-4">Access Forbidden</h2>
<p class="lead">You don't have permission to access this resource.</p>
<a href="{{ url_for('main.home') }}" class="btn btn-primary">Return Home</a>
</div>
</div>
{% endblock %}
-14
View File
@@ -1,14 +0,0 @@
{% extends "base.html" %}
{% block title %}Not Found - MoldCost Pro{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="text-center">
<h1 class="display-1">404</h1>
<h2 class="mb-4">Page Not Found</h2>
<p class="lead">The page you're looking for doesn't exist.</p>
<a href="{{ url_for('main.home') }}" class="btn btn-primary">Return Home</a>
</div>
</div>
{% endblock %}
-22
View File
@@ -1,22 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-warning text-white">
<h4 class="mb-0">Conflict</h4>
</div>
<div class="card-body">
<h5 class="card-title">Data Constraint Error</h5>
<p class="card-text">{{ message }}</p>
<p class="card-text">This error typically occurs when trying to create a duplicate record or violate a data constraint.</p>
<p class="card-text">Please check your input and try again.</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">Return to Home</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
-14
View File
@@ -1,14 +0,0 @@
{% extends "base.html" %}
{% block title %}Server Error - MoldCost Pro{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="text-center">
<h1 class="display-1">500</h1>
<h2 class="mb-4">Server Error</h2>
<p class="lead">Something went wrong on our end. Please try again later.</p>
<a href="{{ url_for('main.home') }}" class="btn btn-primary">Return Home</a>
</div>
</div>
{% endblock %}
-22
View File
@@ -1,22 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-warning text-white">
<h4 class="mb-0">Service Temporarily Unavailable</h4>
</div>
<div class="card-body">
<h5 class="card-title">Database Connection Error</h5>
<p class="card-text">{{ message }}</p>
<p class="card-text">Our technical team has been notified and is working to resolve this issue.</p>
<p class="card-text">Please try again in a few minutes.</p>
<a href="{{ url_for('main.index') }}" class="btn btn-primary">Return to Home</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
-102
View File
@@ -1,102 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container-main py-5">
<div class="hero-section bg-gradient-primary text-white rounded-3 p-5 shadow-lg">
<div class="text-center">
<h1 class="display-4 fw-bold mb-4">
<i class="bi bi-gear-connected"></i>
MoldCost Pro
</h1>
<p class="lead mb-4">Precision Mold Cost Calculations</p>
<div class="d-grid gap-3 d-md-flex justify-content-md-center">
{% if current_user.is_authenticated %}
<a href="{{ url_for('calculator') }}" class="btn btn-light btn-lg px-4">
<i class="bi bi-calculator me-2"></i>
Go to Calculator
</a>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-light btn-lg px-4">
<i class="bi bi-box-arrow-in-right me-2"></i>
Get Started
</a>
<a href="{{ url_for('register') }}" class="btn btn-outline-light btn-lg px-4">
<i class="bi bi-person-plus me-2"></i>
Register
</a>
{% endif %}
</div>
</div>
</div>
<!-- Features Section -->
<div class="row g-4 mt-5">
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<i class="bi bi-speedometer2 display-4 text-primary"></i>
<h3 class="mt-3">Fast Calculation</h3>
<p class="text-muted">Instant mold cost estimates with industry-standard algorithms</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<i class="bi bi-clock-history display-4 text-primary"></i>
<h3 class="mt-3">History Tracking</h3>
<p class="text-muted">Review your previous quotes and calculations</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<i class="bi bi-shield-lock display-4 text-primary"></i>
<h3 class="mt-3">Secure Data</h3>
<p class="text-muted">Enterprise-grade security for your sensitive data</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block styles %}
<style>
.hero-section {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
position: relative;
overflow: hidden;
}
.hero-section:before {
content: "";
position: absolute;
top: -50%;
left: -50%;
right: -50%;
bottom: -50%;
background: linear-gradient(45deg,
rgba(255,255,255,0.1) 25%,
transparent 25%,
transparent 50%,
rgba(255,255,255,0.1) 50%,
rgba(255,255,255,0.1) 75%,
transparent 75%,
transparent
);
background-size: 40px 40px;
animation: animateStripes 4s linear infinite;
opacity: 0.15;
}
@keyframes animateStripes {
0% { transform: translateY(0); }
100% { transform: translateY(-40px); }
}
</style>
{% endblock %}
-82
View File
@@ -1,82 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Commercial Invoice - {{ quote.id }} | MoldCost Pro</title>
<style>
:root {
--primary-color: #1a365d;
--secondary-color: #2c5282;
--accent-color: #c53030;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #1a202c;
line-height: 1.6;
margin: 0;
padding: 20px;
}
.letterhead {
border-bottom: 2px solid var(--primary-color);
margin-bottom: 2rem;
padding-bottom: 1.5rem;
}
.company-logo {
height: 60px;
margin-bottom: 1rem;
}
.legal-disclaimer {
font-size: 0.75rem;
color: #718096;
margin-top: 3rem;
}
@media print {
.page-break { page-break-before: always; }
}
</style>
</head>
<body>
<div class="letterhead">
<img src="https://cdn.example.com/logo.svg"
alt="MoldCost Pro"
class="company-logo">
<div class="flex justify-between text-sm text-gray-600">
<div>
<p>MoldCost Pro Limited</p>
<p>123 Manufacturing Blvd, Shenzhen, CN</p>
</div>
<div class="text-right">
<p>T: +86 (755) 1234-5678</p>
<p>E: sales@moldcostpro.com</p>
<p>W: www.moldcostpro.com</p>
</div>
</div>
</div>
<div class="header-section">
<h1 class="text-2xl font-bold">Commercial Invoice</h1>
<div class="flex justify-between text-sm text-gray-600 mt-2">
<div>
<p><strong>Quote ID:</strong> MC-{{ quote.id|string|replace(' ', '0') }}</p>
<p><strong>Date:</strong> {{ quote.created_at.strftime('%d-%b-%Y') }}</p>
</div>
<div>
<p><strong>Currency:</strong> USD</p>
<p><strong>Valid Until:</strong> {{ (quote.created_at + datetime.timedelta(days=30)).strftime('%d-%b-%Y') }}</p>
</div>
</div>
</div>
{% from "_quotation_details.html" import render_quotation_details %}
{{ render_quotation_details(quote, show_actions=false) }}
<div class="legal-disclaimer">
<p>This quotation is subject to our standard terms and conditions. Prices valid for 30 days from quotation date. All measurements tolerances ±0.5% unless otherwise specified. Export documentation extra.</p>
</div>
</body>
</html>
-68
View File
@@ -1,68 +0,0 @@
{% extends "base.html" %}
{% block title %}Terms of Service - Mold Cost Online Tool{% endblock %}
{% block content %}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card shadow">
<div class="card-body p-5">
<h3 class="mb-4 text-center">Terms of Service</h3>
<div class="terms-content">
<h4 class="mb-3">1. Acceptance of Terms</h4>
<p>By accessing and using the Mold Cost Online Tool, you agree to be bound by these Terms of Service.</p>
<h4 class="mb-3 mt-4">2. Use of Service</h4>
<p>The Mold Cost Online Tool is provided for professional use in the footwear industry. Users must:</p>
<ul>
<li>Provide accurate and complete information</li>
<li>Maintain the confidentiality of their account</li>
<li>Use the service in compliance with all applicable laws</li>
</ul>
<h4 class="mb-3 mt-4">3. Data Privacy</h4>
<p>We are committed to protecting your data:</p>
<ul>
<li>All calculations and quotations are stored securely</li>
<li>Personal information is used only for service provision</li>
<li>Data is not shared with third parties without consent</li>
</ul>
<h4 class="mb-3 mt-4">4. Intellectual Property</h4>
<p>The Mold Cost Online Tool and its contents are protected by intellectual property rights. Users may not:</p>
<ul>
<li>Copy or reproduce the tool's algorithms</li>
<li>Reverse engineer the service</li>
<li>Use the service for unauthorized commercial purposes</li>
</ul>
<h4 class="mb-3 mt-4">5. Disclaimer</h4>
<p>The tool provides estimates based on industry standards. While we strive for accuracy:</p>
<ul>
<li>Results are estimates only</li>
<li>Actual costs may vary</li>
<li>Users should verify calculations independently</li>
</ul>
<h4 class="mb-3 mt-4">6. Account Termination</h4>
<p>We reserve the right to terminate accounts that:</p>
<ul>
<li>Violate these terms</li>
<li>Engage in fraudulent activity</li>
<li>Misuse the service</li>
</ul>
<div class="mt-5 text-center">
<a href="{{ url_for('register') }}" class="btn btn-primary">
<i class="bi bi-arrow-left me-2"></i>Back to Registration
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
View File
-56
View File
@@ -1,56 +0,0 @@
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
@@ -1,139 +0,0 @@
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
-64
View File
@@ -1,64 +0,0 @@
import os
import sys
import ctypes
from ctypes.util import find_library
from pathlib import Path
class DLLLoaderError(Exception):
"""自定义DLL加载异常"""
pass
def setup_dependencies(dll_paths=None):
"""配置DLL搜索路径"""
if dll_paths is None:
dll_paths = [
Path(__file__).parent / 'libs',
Path(os.getenv('LOCALAPPDATA')) / 'MSYS2' / 'mingw64' / 'bin'
]
os.environ['PATH'] = os.pathsep.join([
str(p.resolve()) for p in dll_paths if p.exists()
]) + os.pathsep + os.environ.get('PATH', '')
def load_dll(dll_name, mode=ctypes.DEFAULT_MODE, handle_errors=True):
"""增强版DLL加载函数"""
try:
# 尝试标准加载方式
dll = ctypes.CDLL(dll_name, mode=mode)
print(f"✅ 成功加载 {dll_name}")
return dll
except OSError as e:
if handle_errors:
# 尝试通过PATH查找
lib_path = find_library(dll_name)
if lib_path:
return ctypes.CDLL(lib_path, mode=mode)
# 尝试MSYS2路径
msys_path = Path(os.getenv('LOCALAPPDATA')) / 'MSYS2' / 'mingw64' / 'bin' / dll_name
if msys_path.exists():
return ctypes.CDLL(str(msys_path), mode=mode)
# 生成详细错误报告
path_list = os.environ.get('PATH', '').split(os.pathsep)
raise DLLLoaderError(
f"无法加载 {dll_name}\n"
f"错误详情: {str(e)}\n"
f"搜索路径:\n" + "\n".join(path_list)
)
else:
raise
# 自动配置默认路径
try:
setup_dependencies()
except Exception as e:
print(f"⚠️ 初始化依赖失败: {e}")
if __name__ == '__main__':
# 自测试代码
try:
test_dll = load_dll('gtk-3-0.dll')
print(f"测试DLL句柄: {test_dll._handle}")
except DLLLoaderError as e:
print(f"❌ 自测试失败: {e}")