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

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2607
View File
File diff suppressed because one or more lines are too long
+638
View File
@@ -0,0 +1,638 @@
// static/js/mold_calculator.js
// Check if script is already initialized
if (window.moldCalculatorInitialized) {
console.log('Mold calculator already initialized, skipping...');
} else {
window.moldCalculatorInitialized = true;
// Wait for DOM to be fully loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeCalculator);
} else {
initializeCalculator();
}
}
function initializeCalculator() {
console.log('DOM Content Loaded - Initializing mold calculator');
// Get form elements
const form = document.getElementById('calcForm');
const mainSelect = document.getElementById('mold_main_type');
const subSelect = document.getElementById('mold_sub_type');
const specificTypeSelect = document.getElementById('mold_specific_type');
const stageSelect = document.getElementById('stage');
const typeSelect = document.getElementById('type');
const toolingIdInput = document.getElementById('tooling_id');
const submitButton = document.getElementById('calculateBtn');
console.log('Main select element:', mainSelect);
console.log('Sub select element:', subSelect);
console.log('Specific type select element:', specificTypeSelect);
console.log('Stage select element:', stageSelect);
console.log('Type select element:', typeSelect);
console.log('Tooling ID input:', toolingIdInput);
console.log('Submit button:', submitButton);
// Check if submit button exists
if (!submitButton) {
console.error('Submit button not found! Looking for element with ID "calculateBtn"');
// Try alternative selectors
const alternativeButton = form.querySelector('button[type="submit"]');
if (alternativeButton) {
console.log('Found submit button using alternative selector:', alternativeButton);
} else {
console.error('No submit button found with any selector!');
return; // Exit initialization if we can't find the submit button
}
}
// Define mappings
const moldTypeMapping = {
'Rubber': ['Flat Outsole', 'Cupsole', 'Rubber Sole'],
'CMEVA': ['2 Plates Mold', '3 Plates Mold', '2 Plates with in-mold channel', 'Breathable + in-mold channel'],
'IMEVA': ['2 Plates Mold', '3 Plates Mold'],
'Sandal': ['Sandal with Upper Bandage', '2 Plate without Upper Bandage'],
'Co-shot mold': ['Co-shot mold', 'Foaming mold'],
'PU': ['2 Plates Mold', '3 Plates Mold']
};
const specificTypeOptions = {
'Flat Outsole': ['Flat', 'Flat with Top/Heel Tip'],
'Cupsole': ['Top PL', 'Visible PL'],
'Rubber Sole': ['Top PL', 'Short PL (Heel/Forefoot)', 'Visible PL', 'Wave PL']
};
const stageTypeMapping = {
'CR0': ['Sample'],
'CR1': ['Sample'],
'CR2': ['Sample'],
'SMS': ['A Set'],
'commercialization': ['A Set'],
'production': ['Additional']
};
// Function to update sub-type options
function updateSubTypeOptions() {
console.log('Populating sub-types');
const selectedType = mainSelect.value;
console.log('Selected type:', selectedType);
// Clear current options
subSelect.innerHTML = '<option value="">Select Sub Type</option>';
if (selectedType && moldTypeMapping[selectedType]) {
console.log('Found mapping for type:', selectedType);
const options = moldTypeMapping[selectedType];
console.log('Adding options:', options);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
subSelect.appendChild(opt);
});
}
// Reset specific type
specificTypeSelect.innerHTML = '<option value="">Select Specific Type</option>';
specificTypeSelect.disabled = true;
}
// Function to update specific type options
function updateSpecificTypeOptions() {
const selectedSubType = subSelect.value;
console.log('Selected sub-type:', selectedSubType);
// Clear current options
specificTypeSelect.innerHTML = '<option value="">Select Specific Type</option>';
if (selectedSubType && specificTypeOptions[selectedSubType]) {
console.log('Found specific type options for:', selectedSubType);
const options = specificTypeOptions[selectedSubType];
console.log('Adding specific type options:', options);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
specificTypeSelect.appendChild(opt);
});
specificTypeSelect.disabled = false;
} else {
specificTypeSelect.disabled = true;
}
}
// Function to update type options based on stage
function updateTypeOptions() {
const selectedStage = stageSelect.value;
console.log('Selected stage:', selectedStage);
// Clear current options
typeSelect.innerHTML = '<option value="">Select Type</option>';
if (selectedStage && stageTypeMapping[selectedStage]) {
const options = stageTypeMapping[selectedStage];
console.log('Adding type options:', options);
options.forEach(option => {
const opt = document.createElement('option');
opt.value = option;
opt.textContent = option;
typeSelect.appendChild(opt);
});
// Enable the type select
typeSelect.disabled = false;
} else {
// Disable the type select if no stage is selected
typeSelect.disabled = true;
}
}
// Add event listeners
mainSelect.addEventListener('change', function() {
console.log('Main type changed:', this.value);
updateSubTypeOptions();
});
subSelect.addEventListener('change', function() {
console.log('Sub type changed:', this.value);
updateSpecificTypeOptions();
});
stageSelect.addEventListener('change', function() {
console.log('Stage changed:', this.value);
updateTypeOptions();
});
// Add tooling ID validation
if (toolingIdInput) {
console.log('Tooling ID input found, adding validation');
toolingIdInput.addEventListener('input', function(e) {
console.log('Tooling ID input event triggered, value:', this.value);
// Remove any non-numeric characters
let value = this.value.replace(/\D/g, '');
// Limit to 5 digits
if (value.length > 5) {
value = value.substring(0, 5);
}
this.value = value;
console.log('Tooling ID value after validation:', this.value);
});
toolingIdInput.addEventListener('paste', function(e) {
console.log('Tooling ID paste event triggered');
e.preventDefault();
const pastedText = (e.clipboardData || window.clipboardData).getData('text');
const numericOnly = pastedText.replace(/\D/g, '').substring(0, 5);
this.value = numericOnly;
console.log('Tooling ID value after paste validation:', this.value);
});
} else {
console.error('Tooling ID input element not found!');
// Try alternative selectors
const alternativeSelectors = [
'input[name="tooling_id"]',
'input[placeholder*="tooling"]',
'input[placeholder*="Tooling"]'
];
for (let selector of alternativeSelectors) {
const element = document.querySelector(selector);
if (element) {
console.log('Found tooling ID input using selector:', selector, element);
break;
}
}
}
// Initialize options
updateSubTypeOptions();
updateTypeOptions();
// Function to reset form
function resetForm(form) {
console.log('Resetting form...');
// Reset all select elements to their first option
const selects = form.querySelectorAll('select');
selects.forEach(select => {
select.selectedIndex = 0;
select.dispatchEvent(new Event('change'));
});
// Reset all input elements
const inputs = form.querySelectorAll('input');
inputs.forEach(input => {
input.value = '';
});
// Reset all checkboxes and radio buttons
const checkboxes = form.querySelectorAll('input[type="checkbox"], input[type="radio"]');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
});
// Disable dependent dropdowns
if (subSelect) subSelect.disabled = true;
if (specificTypeSelect) specificTypeSelect.disabled = true;
if (typeSelect) typeSelect.disabled = true;
// Show reset notification
const notification = document.createElement('div');
notification.className = 'alert alert-info alert-dismissible fade show mt-3';
notification.innerHTML = `
<i class="fas fa-info-circle me-2"></i>
Form has been reset. You can now create a new quotation.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
form.insertBefore(notification, form.firstChild);
console.log('Form reset complete');
}
// Handle form submission
form.addEventListener('submit', async function(e) {
e.preventDefault();
console.log('Form submitted');
// Get submit button robustly
let submitButton = document.getElementById('calculateBtn') || form.querySelector('button[type="submit"]');
if (!submitButton) {
console.error('Submit button not found during form submission!');
alert('Error: Submit button not found. Please refresh the page and try again.');
return;
}
// Check if submission is already in progress
if (submitButton.disabled || form.dataset.submitting === 'true') {
console.log('Form submission already in progress or cooldown active, ignoring duplicate click');
return;
}
// Set submission state
form.dataset.submitting = 'true';
// Disable submit button and show loading state
submitButton.disabled = true;
const originalButtonText = submitButton.innerHTML;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Calculating...';
submitButton.classList.add('btn-secondary');
submitButton.classList.remove('btn-primary');
submitButton.title = 'Please wait while calculating...';
try {
// Log all form fields
console.log('Form fields:');
for (let [key, value] of new FormData(form).entries()) {
console.log(`${key}: ${value}`);
}
// Validate required fields
const mainType = mainSelect.value;
const subType = subSelect.value;
const specificType = specificTypeSelect.value;
const stage = stageSelect.value;
const type = typeSelect.value;
const toolingId = toolingIdInput ? toolingIdInput.value : '';
console.log('Required fields:', {
mainType,
subType,
specificType,
stage,
type,
toolingId
});
if (!mainType) {
alert('Please select a main type');
return;
}
if (!subType) {
alert('Please select a sub type');
return;
}
if (!stage) {
alert('Please select a stage');
return;
}
if (!type) {
alert('Please select a type');
return;
}
// Validate tooling ID
if (!toolingId) {
alert('Please enter a Tooling ID');
return;
}
if (!/^\d{5}$/.test(toolingId)) {
alert('Tooling ID must be exactly 5 digits');
return;
}
// Check if specific type is required
const validSpecificTypes = specificTypeOptions[subType] || [];
if (validSpecificTypes.length > 0 && !specificType) {
alert('Please select a specific type');
return;
}
// Create form data
const formData = new FormData(form);
// Add CSRF token to form data
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
formData.append('csrf_token', csrfToken);
// Remove specific type if it's not needed
if (validSpecificTypes.length === 0) {
formData.delete('mold_specific_type');
}
// Log the final form data being sent
console.log('Final form data being sent:');
for (let [key, value] of formData.entries()) {
console.log(`${key}: ${value}`);
}
const response = await fetch('/calculate', {
method: 'POST',
body: formData
});
console.log('Response status:', response.status);
const responseText = await response.text();
console.log('Response text:', responseText);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, body: ${responseText}`);
}
const data = JSON.parse(responseText);
console.log('Calculation result:', data);
// Get the result element
const resultElement = document.getElementById('result');
// Update the result display
updateResultsDisplay(data);
// Save the quotation to the database
try {
console.log('Saving quotation to database...');
const saveResponse = await fetch('/save-quotation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data)
});
const saveResult = await saveResponse.json();
console.log('Save result:', saveResult);
if (saveResult.success) {
console.log('Quotation saved successfully with ID:', saveResult.quotation_id);
// Show success message
const successMessage = document.createElement('div');
successMessage.className = 'alert alert-success alert-dismissible fade show';
successMessage.innerHTML = `
<i class="fas fa-check-circle me-2"></i>
Quotation saved successfully! ID: ${saveResult.quotation_id}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
resultElement.insertBefore(successMessage, resultElement.firstChild);
// Reset form after successful save
resetForm(form);
// Refresh quote history
setTimeout(() => {
refreshQuoteHistory();
}, 1000);
} else if (saveResult.duplicate) {
console.log('Duplicate submission detected:', saveResult.message);
const duplicateMessage = document.createElement('div');
duplicateMessage.className = 'alert alert-warning alert-dismissible fade show';
duplicateMessage.innerHTML = `
<i class="fas fa-exclamation-triangle me-2"></i>
${saveResult.message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
resultElement.insertBefore(duplicateMessage, resultElement.firstChild);
// Reset form even on duplicate to prevent repeated attempts
resetForm(form);
}
} catch (saveError) {
console.error('Error saving quotation:', saveError);
const errorMessage = document.createElement('div');
errorMessage.className = 'alert alert-danger alert-dismissible fade show';
errorMessage.innerHTML = `
<i class="fas fa-exclamation-circle me-2"></i>
Error saving quotation. Please try again later.
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
resultElement.insertBefore(errorMessage, resultElement.firstChild);
}
} catch (error) {
console.error('Error:', error);
alert('An error occurred while calculating. Please check the console for details.');
} finally {
// Re-enable submit button and restore original state after a delay
setTimeout(() => {
let submitButton = document.getElementById('calculateBtn') || form.querySelector('button[type="submit"]');
if (submitButton) {
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
submitButton.classList.remove('btn-secondary');
submitButton.classList.add('btn-primary');
submitButton.title = 'Calculate mold cost';
}
// Reset submission state
form.dataset.submitting = 'false';
}, 2000); // 2 second cooldown before allowing new submission
}
});
}
// Function to update results display
function updateResultsDisplay(data) {
const resultElement = document.getElementById('result');
if (!resultElement) {
console.error('Result element not found');
return;
}
// Format the results to match the provided screenshot
const formattedResults = `
<div class="card bg-white shadow-sm p-4 mb-4" style="border-radius: 1rem; border: 1px solid #e3e6ea;">
<div class="mb-4 pb-2 border-bottom d-flex align-items-center" style="font-size:1.4rem;font-weight:600;color:#1976d2;">
<i class="fas fa-clipboard-list me-2"></i>Calculation Results
</div>
<!-- Factory Information -->
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-industry me-2"></i>Factory Information
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="card p-3 h-100 bg-light border-0">
<div class="fw-bold">Mold Shop</div>
<div>${data.mold_shop_name || '-'}</div>
<div class="text-muted small">${data.mold_shop_country || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100 bg-light border-0">
<div class="fw-bold">T1 Factory</div>
<div>${data.t1_shoe_factory_name || '-'}</div>
<div class="text-muted small">${data.t1_shoe_factory_country || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100 bg-light border-0">
<div class="fw-bold">T2 Factory</div>
<div>${data.t2_component_factory_name || '-'}</div>
<div class="text-muted small">${data.t2_component_factory_country || '-'}</div>
</div>
</div>
</div>
</div>
<!-- Mold Information -->
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-cube me-2"></i>Mold Information
</div>
<div class="row g-3">
<div class="col-md-4">
<div class="card p-3 h-100">
<div class="fw-bold">Model</div>
<div>${data.model_name || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100">
<div class="fw-bold">Tooling</div>
<div>${data.tooling_id || '-'}</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-3 h-100">
<div class="fw-bold">Season & Stage</div>
<div>${(data.season || '-') + '/' + (data.stage || '-')}</div>
</div>
</div>
</div>
</div>
<!-- Mold Specifications -->
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-wrench me-2"></i>Mold Specifications
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="card p-3 h-100">
<div class="fw-bold">Mold Type</div>
<div>${data.mold_main_type || '-'}</div>
<div class="text-muted small">${data.mold_sub_type || ''}</div>
</div>
</div>
<div class="col-md-6">
<div class="card p-3 h-100">
<div class="fw-bold mb-2">Complexity Factors</div>
<div class="row">
<div class="col-6 small">High Sidewall</div>
<div class="col-6 small text-end">${data.complexity_high_sidewall === 'yes' ? '<b>Yes</b>' : '-'}</div>
<div class="col-6 small">Sliders</div>
<div class="col-6 small text-end">${data.sliders_count ? `<b>${data.sliders_count} slider${data.sliders_count > 1 ? 's' : ''}</b>` : '-'}</div>
<div class="col-6 small">Cavities</div>
<div class="col-6 small text-end">${data.cavity_count ? `<b>${data.cavity_count} pair</b>` : '-'}</div>
<div class="col-6 small">Digital Texture</div>
<div class="col-6 small text-end">${data.digital_texture_type ? `<b>${data.digital_texture_type}</b>` : '-'}</div>
</div>
</div>
</div>
</div>
</div>
<!-- Cost Breakdown & Approvals -->
<div class="row g-3">
<div class="col-md-8">
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-table me-2"></i>Cost Breakdown
</div>
<div class="card p-0">
<table class="table mb-0">
<tbody>
<tr><td>Base Price</td><td class="text-end">$${Number(data.base_price).toFixed(2)}</td></tr>
<tr><td>Complexity Adjustment</td><td class="text-end">$${Number(data.complexity_cost).toFixed(2)}</td></tr>
<tr><td>Digital Texture Cost</td><td class="text-end">$${Number(data.texture_cost).toFixed(2)}</td></tr>
<tr class="table-primary fw-bold"><td>Total Cost</td><td class="text-end">$${Number(data.total_cost).toFixed(2)}</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="mb-4">
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
<i class="fas fa-check-circle me-2"></i>Approvals
</div>
<div class="card p-3">
<div class="mb-3 text-muted small"><i class="far fa-calendar-alt me-1"></i>Issue Date: <b>${data.issue_date || '-'}</b></div>
<div class="mb-2">Mold Shop<br><b>${data.approved_by_mold_shop || '-'}</b></div>
<div class="mb-2">T1 Tooling<br><b>${data.approved_by_t1_tooling || '-'}</b></div>
<div>LO Tooling<br><b>${data.approved_by_lo_tooling || '-'}</b></div>
</div>
</div>
</div>
</div>
</div>
`;
resultElement.innerHTML = formattedResults;
}
// Helper function to format currency
function formatCurrency(value) {
if (value === null || value === undefined) {
return 'N/A';
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(value);
}
// Refresh quote history
async function refreshQuoteHistory() {
try {
const response = await fetch('/quote-history');
if (response.ok) {
const data = await response.json();
// Update quote history display
const historyContainer = document.getElementById('quote-history');
if (historyContainer) {
historyContainer.innerHTML = data.html;
}
}
} catch (error) {
console.error('Error refreshing quote history:', error);
}
}
+15
View File
@@ -0,0 +1,15 @@
<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
@@ -0,0 +1,298 @@
{% 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
@@ -0,0 +1,130 @@
<!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>
@@ -0,0 +1,198 @@
{% 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
@@ -0,0 +1,558 @@
{% 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
@@ -0,0 +1,245 @@
{% 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 %}
@@ -0,0 +1,238 @@
{% 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 %}
@@ -0,0 +1,146 @@
{% 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
@@ -0,0 +1,174 @@
{% 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
@@ -0,0 +1 @@
+253
View File
@@ -0,0 +1,253 @@
{% 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
@@ -0,0 +1,168 @@
{% 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
@@ -0,0 +1,128 @@
{% 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
@@ -0,0 +1,58 @@
{% 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
@@ -0,0 +1,77 @@
{% 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
@@ -0,0 +1,44 @@
{% 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 %}
@@ -0,0 +1,52 @@
{% 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
@@ -0,0 +1,156 @@
<!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
@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% block head %}
<meta name="csrf-token" content="{{ csrf_token() }}">
{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Mold Cost Calculator</h2>
{% if current_user.is_authenticated and current_user.is_admin %}
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-primary">
<i class="fas fa-cog me-2"></i>Admin Portal
</a>
{% endif %}
</div>
<form id="calcForm" class="row g-3" method="POST" action="/calculate">
{{ form.csrf_token }}
<!-- Factory Information -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Factory Information</h6>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.mold_shop_name.label(class="form-label") }}
{{ form.mold_shop_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.mold_shop_country.label(class="form-label") }}
{{ form.mold_shop_country(class="form-control") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.t1_shoe_factory_name.label(class="form-label") }}
{{ form.t1_shoe_factory_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.t1_shoe_factory_country.label(class="form-label") }}
{{ form.t1_shoe_factory_country(class="form-control") }}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
{{ form.t2_component_factory_name.label(class="form-label") }}
{{ form.t2_component_factory_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.t2_component_factory_country.label(class="form-label") }}
{{ form.t2_component_factory_country(class="form-control") }}
</div>
</div>
</div>
</div>
</div>
<!-- Mold Information -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Mold Information</h6>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.model_name.label(class="form-label") }}
{{ form.model_name(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.tooling_id.label(class="form-label") }}
{{ form.tooling_id(class="form-control") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="form-group">
{{ form.season.label(class="form-label") }}
{{ form.season(class="form-control", id="season") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.stage.label(class="form-label") }}
{{ form.stage(class="form-control", id="stage") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.type.label(class="form-label") }}
{{ form.type(class="form-control", id="type") }}
</div>
</div>
</div>
</div>
</div>
<!-- Mold Specifications -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Mold Specifications</h6>
<div class="row mb-3">
<div class="col-md-4">
<div class="form-group">
{{ form.mold_main_type.label(class="form-label") }}
{{ form.mold_main_type(class="form-control", id="mold_main_type") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.mold_sub_type.label(class="form-label") }}
{{ form.mold_sub_type(class="form-control", id="mold_sub_type") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.mold_specific_type.label(class="form-label") }}
{{ form.mold_specific_type(class="form-control", id="mold_specific_type") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.sliders_count.label(class="form-label") }}
{{ form.sliders_count(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.cavity_count.label(class="form-label") }}
{{ form.cavity_count(class="form-control") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.digital_texture_type.label(class="form-label") }}
{{ form.digital_texture_type(class="form-control") }}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
{{ form.complexity_high_sidewall.label(class="form-label") }}
{{ form.complexity_high_sidewall(class="form-control") }}
</div>
</div>
</div>
</div>
</div>
<!-- Dates & Approvals -->
<div class="card mb-4">
<div class="card-body">
<h6 class="card-title mb-3">Dates & Approvals</h6>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group">
{{ form.issue_date.label(class="form-label") }}
{{ form.issue_date(class="form-control", type="date") }}
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="form-group">
{{ form.approved_by_mold_shop.label(class="form-label") }}
{{ form.approved_by_mold_shop(class="form-control") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.approved_by_t1_tooling.label(class="form-label") }}
{{ form.approved_by_t1_tooling(class="form-control") }}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
{{ form.approved_by_lo_tooling.label(class="form-label") }}
{{ form.approved_by_lo_tooling(class="form-control") }}
</div>
</div>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="text-center">
<button type="submit" id="calculateBtn" class="btn btn-primary btn-lg">Calculate</button>
</div>
</form>
<!-- Results Section -->
<div id="result" class="mt-4"></div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script id="mold-calculator-script" src="{{ url_for('static', filename='js/mold_calculator.js', v='1.4') }}"></script>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
{% 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
@@ -0,0 +1,14 @@
{% 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
@@ -0,0 +1,14 @@
{% 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
@@ -0,0 +1,14 @@
{% 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
@@ -0,0 +1,22 @@
{% 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
@@ -0,0 +1,14 @@
{% 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
@@ -0,0 +1,22 @@
{% 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
@@ -0,0 +1,102 @@
{% 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
@@ -0,0 +1,82 @@
<!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
@@ -0,0 +1,68 @@
{% 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
@@ -0,0 +1,56 @@
from ..utils.cost_factor_loader import get_cost_factors
def calculate_base_price(mold_main_type, mold_sub_type, mold_specific_type, sliders_count, cavity_count, digital_texture_type, complexity_high_sidewall=False):
"""
Calculate the base price for a mold based on various factors.
Args:
mold_main_type (str): The main type of the mold
mold_sub_type (str): The sub type of the mold
mold_specific_type (str): The specific type of the mold
sliders_count (int): Number of sliders
cavity_count (int): Number of cavities
digital_texture_type (str): Type of digital texture
complexity_high_sidewall (bool): Whether the mold has high sidewall complexity
Returns:
dict: A dictionary containing all cost components
"""
# Get cost factors from database (or fallback to defaults)
cost_factors, tax_rates, default_net_base = get_cost_factors()
# Start with the default net base
base_price = default_net_base
# Apply mold type factor
if mold_specific_type in cost_factors['mold']:
base_price *= cost_factors['mold'][mold_specific_type]
# Calculate complexity cost
complexity_cost = 0
# Add slider complexity
if sliders_count > 0:
complexity_cost += base_price * (sliders_count * cost_factors['complexity']['slider'])
# Add cavity complexity
if cavity_count > 1:
complexity_cost += base_price * ((cavity_count - 1) * cost_factors['complexity']['cavity'])
# Add high sidewall complexity
if complexity_high_sidewall:
complexity_cost += base_price * cost_factors['complexity']['high_sidewall']
# Calculate texture cost
texture_cost = 0
if digital_texture_type in cost_factors['texture']:
texture_cost = base_price * cost_factors['texture'][digital_texture_type]
# Calculate total cost
total_cost = base_price + complexity_cost + texture_cost
return {
'base_price': round(base_price, 2),
'complexity_cost': round(complexity_cost, 2),
'texture_cost': round(texture_cost, 2),
'total_cost': round(total_cost, 2)
}
+139
View File
@@ -0,0 +1,139 @@
from ..models import CostFactor
from .. import db
import logging
logger = logging.getLogger(__name__)
# Single source of truth for default cost factors
DEFAULT_COST_FACTORS = [
# Mold Type Factors
{'category': 'mold', 'name': 'Flat', 'factor_value': 1.0, 'description': 'Standard flat outsole'},
{'category': 'mold', 'name': 'Flat with Top/Heel Tip', 'factor_value': 1.25, 'description': 'Flat with additional tip features'},
{'category': 'mold', 'name': 'Top PL', 'factor_value': 1.05, 'description': 'Top part line mold'},
{'category': 'mold', 'name': 'Visible PL', 'factor_value': 1.28, 'description': 'Visible part line mold'},
{'category': 'mold', 'name': 'Short PL (Heel/Forefoot)', 'factor_value': 1.18, 'description': 'Short part line for specific areas'},
{'category': 'mold', 'name': 'Wave PL', 'factor_value': 1.32, 'description': 'Wave pattern part line'},
{'category': 'mold', 'name': 'Multi-color PL', 'factor_value': 1.35, 'description': 'Multi-color part line mold'},
{'category': 'mold', 'name': '2 Plates Mold', 'factor_value': 1.0, 'description': 'Standard 2-plate configuration'},
{'category': 'mold', 'name': '3 Plates Mold', 'factor_value': 1.35, 'description': 'Complex 3-plate configuration'},
{'category': 'mold', 'name': '2 Plates with in-mold channel', 'factor_value': 1.25, 'description': '2-plate with channel features'},
{'category': 'mold', 'name': 'Breathable + in-mold channel', 'factor_value': 1.45, 'description': 'Breathable with channel features'},
{'category': 'mold', 'name': 'Sandal with Upper Bandage', 'factor_value': 1.15, 'description': 'Sandal with bandage features'},
{'category': 'mold', 'name': '2 Plate without Upper Bandage', 'factor_value': 1.05, 'description': 'Standard 2-plate sandal'},
{'category': 'mold', 'name': 'Co-shot mold', 'factor_value': 1.25, 'description': 'Multi-material injection'},
{'category': 'mold', 'name': 'Foaming mold', 'factor_value': 1.4, 'description': 'Foam injection process'},
# Complexity Factors
{'category': 'complexity', 'name': 'high_sidewall', 'factor_value': 0.18, 'calculation_formula': 'Base Price × 0.18', 'description': 'Additional cost for high sidewall molds'},
{'category': 'complexity', 'name': 'slider', 'factor_value': 0.1, 'calculation_formula': 'Base Price × (Number of Sliders × 0.10)', 'description': 'Cost per slider added to mold'},
{'category': 'complexity', 'name': 'cavity', 'factor_value': 0.12, 'calculation_formula': 'Base Price × ((Number of Cavities - 1) × 0.12)', 'description': 'Cost for additional cavities beyond 1'},
# Texture Factors
{'category': 'texture', 'name': 'adidas_digital', 'factor_value': 0.0, 'percentage': '0%', 'description': 'No additional cost for standard adidas texture'},
{'category': 'texture', 'name': 'customized texture', 'factor_value': 0.15, 'percentage': '15%', 'description': '15% of base price for custom texture'},
{'category': 'texture', 'name': 'laser', 'factor_value': 0.2, 'percentage': '20%', 'description': '20% of base price for laser texture'},
# Tax Rates
{'category': 'tax', 'name': 'China', 'factor_value': 0.06, 'percentage': '6%', 'description': '6% VAT for China'},
{'category': 'tax', 'name': 'Vietnam', 'factor_value': 0.10, 'percentage': '10%', 'description': '10% VAT for Vietnam'},
{'category': 'tax', 'name': 'Indonesia', 'factor_value': 0.07, 'percentage': '7%', 'description': '7% VAT for Indonesia'},
# Base Price
{'category': 'base', 'name': 'DEFAULT_NET_BASE', 'factor_value': 12.5, 'description': 'Default base price for calculations'}
]
def load_cost_factors_from_db():
"""
Load cost factors from database and return them in the same format as constants.
Falls back to hardcoded values if database is not available.
"""
try:
# Get all active cost factors
factors = CostFactor.query.filter_by(is_active=True).all()
if not factors:
logger.warning("No cost factors found in database, using hardcoded defaults")
return get_default_cost_factors()
# Build the cost factors structure
cost_factors = {
'mold': {},
'complexity': {},
'texture': {}
}
tax_rates = {}
default_net_base = 12.5 # Default value
for factor in factors:
if factor.category == 'mold':
cost_factors['mold'][factor.name] = factor.factor_value
elif factor.category == 'complexity':
cost_factors['complexity'][factor.name] = factor.factor_value
elif factor.category == 'texture':
cost_factors['texture'][factor.name] = factor.factor_value
elif factor.category == 'tax':
tax_rates[factor.name] = factor.factor_value
elif factor.category == 'base':
default_net_base = factor.factor_value
logger.info(f"Loaded {len(factors)} cost factors from database")
return cost_factors, tax_rates, default_net_base
except Exception as e:
logger.error(f"Error loading cost factors from database: {str(e)}")
logger.info("Falling back to hardcoded cost factors")
return get_default_cost_factors()
def get_default_cost_factors():
"""Return hardcoded cost factors as fallback - derived from single source of truth"""
cost_factors = {
'mold': {},
'complexity': {},
'texture': {}
}
tax_rates = {}
default_net_base = 12.5
# Build the structure from the single source of truth
for factor_data in DEFAULT_COST_FACTORS:
category = factor_data['category']
name = factor_data['name']
value = factor_data['factor_value']
if category == 'mold':
cost_factors['mold'][name] = value
elif category == 'complexity':
cost_factors['complexity'][name] = value
elif category == 'texture':
cost_factors['texture'][name] = value
elif category == 'tax':
tax_rates[name] = value
elif category == 'base':
default_net_base = value
return cost_factors, tax_rates, default_net_base
def get_cost_factors():
"""Get current cost factors (from database or defaults)"""
return load_cost_factors_from_db()
def initialize_default_cost_factors():
"""Initialize the database with default cost factors if none exist"""
try:
if CostFactor.query.count() == 0:
logger.info("Initializing database with default cost factors")
# Use the single source of truth
for factor_data in DEFAULT_COST_FACTORS:
factor = CostFactor(**factor_data)
db.session.add(factor)
db.session.commit()
logger.info(f"Initialized {len(DEFAULT_COST_FACTORS)} default cost factors")
except Exception as e:
logger.error(f"Error initializing default cost factors: {str(e)}")
db.session.rollback()
raise
+64
View File
@@ -0,0 +1,64 @@
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}")