Add centralized admin settings (company info, social links, quick calls, sidebar visibility toggles, theme, logo upload) with singleton pattern and full CRUD API. Add active/dedicated customer count endpoints for dashboard stats. Fix automatic assignment route to use PUT instead of GET. Refactor oil price endpoint to use schema serialization with null safety. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
250 lines
9.3 KiB
Python
Executable File
250 lines
9.3 KiB
Python
Executable File
import logging
|
|
import base64
|
|
import json
|
|
from flask import request
|
|
from flask_login import current_user, logout_user, login_user, login_required
|
|
from app.admin import admin
|
|
from app import db
|
|
from app.common.responses import error_response, success_response
|
|
from datetime import datetime
|
|
from app.classes.pricing import (
|
|
Pricing_Oil_Oil,
|
|
Pricing_Oil_Oil_schema)
|
|
from app.classes.admin import Admin_Company, Admin_Company_schema, Call, Admin_Settings, Admin_Settings_schema
|
|
from app.common.decorators import admin_required, login_required as custom_login_required
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
@admin.route("/oil/create", methods=["POST"])
|
|
@admin_required
|
|
def create_oil_price():
|
|
"""
|
|
Changes the price for oil deliveries
|
|
"""
|
|
logger.info("POST /admin/oil/create - Creating new oil price")
|
|
now = datetime.utcnow()
|
|
price_from_supplier = request.json["price_from_supplier"]
|
|
price_for_customer = request.json["price_for_customer"]
|
|
price_for_employee = request.json["price_for_employee"]
|
|
|
|
# Legacy single-tier pricing (for backward compatibility, use tier1 values)
|
|
price_same_day_tier1 = request.json.get("price_same_day_tier1", 0)
|
|
price_prime_tier1 = request.json.get("price_prime_tier1", 0)
|
|
price_emergency_tier1 = request.json.get("price_emergency_tier1", 0)
|
|
|
|
# Get all tier pricing
|
|
price_same_day_tier2 = request.json.get("price_same_day_tier2", 0)
|
|
price_same_day_tier3 = request.json.get("price_same_day_tier3", 0)
|
|
price_same_day_tier4 = request.json.get("price_same_day_tier4", 0)
|
|
price_same_day_tier5 = request.json.get("price_same_day_tier5", 0)
|
|
|
|
price_prime_tier2 = request.json.get("price_prime_tier2", 0)
|
|
price_prime_tier3 = request.json.get("price_prime_tier3", 0)
|
|
price_prime_tier4 = request.json.get("price_prime_tier4", 0)
|
|
price_prime_tier5 = request.json.get("price_prime_tier5", 0)
|
|
|
|
price_emergency_tier2 = request.json.get("price_emergency_tier2", 0)
|
|
price_emergency_tier3 = request.json.get("price_emergency_tier3", 0)
|
|
price_emergency_tier4 = request.json.get("price_emergency_tier4", 0)
|
|
price_emergency_tier5 = request.json.get("price_emergency_tier5", 0)
|
|
|
|
new_admin_oil_price = Pricing_Oil_Oil(
|
|
price_from_supplier=price_from_supplier,
|
|
price_for_customer=price_for_customer,
|
|
price_for_employee=price_for_employee,
|
|
# Legacy columns (use tier1 for backward compatibility)
|
|
price_same_day=price_same_day_tier1,
|
|
price_prime=price_prime_tier1,
|
|
price_emergency=price_emergency_tier1,
|
|
# Tier pricing
|
|
price_same_day_tier1=price_same_day_tier1,
|
|
price_same_day_tier2=price_same_day_tier2,
|
|
price_same_day_tier3=price_same_day_tier3,
|
|
price_same_day_tier4=price_same_day_tier4,
|
|
price_same_day_tier5=price_same_day_tier5,
|
|
price_prime_tier1=price_prime_tier1,
|
|
price_prime_tier2=price_prime_tier2,
|
|
price_prime_tier3=price_prime_tier3,
|
|
price_prime_tier4=price_prime_tier4,
|
|
price_prime_tier5=price_prime_tier5,
|
|
price_emergency_tier1=price_emergency_tier1,
|
|
price_emergency_tier2=price_emergency_tier2,
|
|
price_emergency_tier3=price_emergency_tier3,
|
|
price_emergency_tier4=price_emergency_tier4,
|
|
price_emergency_tier5=price_emergency_tier5,
|
|
date=now,
|
|
)
|
|
# new_admin_oil_price = Pricing_Oil_Oil(
|
|
# price_from_supplier=price_from_supplier,
|
|
# price_for_customer=price_for_customer,
|
|
# price_for_employee=price_for_employee,
|
|
# price_same_day=price_same_day,
|
|
# price_prime=price_prime,
|
|
# date=now,
|
|
# )
|
|
|
|
db.session.add(new_admin_oil_price)
|
|
db.session.commit()
|
|
|
|
return success_response({'price': new_admin_oil_price.id})
|
|
|
|
|
|
|
|
@admin.route("/oil/get", methods=["GET"])
|
|
@admin_required
|
|
def get_oil_price():
|
|
"""
|
|
gets oil prices
|
|
"""
|
|
logger.info("GET /admin/oil/get - Fetching current oil prices")
|
|
get_oil_prices = (db.session
|
|
.query(Pricing_Oil_Oil)
|
|
.order_by(Pricing_Oil_Oil.date.desc())
|
|
.first())
|
|
price_schema = Pricing_Oil_Oil_schema(many=False)
|
|
return success_response({"price": price_schema.dump(get_oil_prices)})
|
|
|
|
|
|
@admin.route("/company/<int:company_id>", methods=["GET"])
|
|
@admin_required
|
|
def get_company(company_id):
|
|
logger.info(f"GET /admin/company/{company_id} - Fetching company data")
|
|
get_data_company = (db.session
|
|
.query(Admin_Company)
|
|
.first())
|
|
|
|
company_schema = Admin_Company_schema(many=False)
|
|
return success_response({"company": company_schema.dump(get_data_company)})
|
|
|
|
@admin.route("/voip_routing", methods=["GET"])
|
|
@admin_required
|
|
def get_voip_routing():
|
|
"""
|
|
Gets the current VOIP routing (latest Call record's current_phone)
|
|
"""
|
|
logger.info("GET /admin/voip_routing - Fetching current VoIP routing")
|
|
latest_call = (db.session
|
|
.query(Call)
|
|
.order_by(Call.created_at.desc())
|
|
.first())
|
|
if latest_call:
|
|
return success_response({"current_phone": latest_call.current_phone})
|
|
else:
|
|
return error_response("No VoIP routing found", 404)
|
|
|
|
|
|
# ============================================
|
|
# Admin Settings Endpoints
|
|
# ============================================
|
|
|
|
DEFAULT_QUICK_CALLS = json.dumps([
|
|
{"name": "WB Hill Tank Springfield", "phone": "(413) 525-3678"},
|
|
{"name": "LW Tank Uxbridge", "phone": "(508) 234-6000"},
|
|
{"name": "Trask Tank Worcester", "phone": "(508) 791-5064"},
|
|
{"name": "David Mechanic", "phone": "(774) 239-3776"},
|
|
{"name": "Spring Rebuilders", "phone": "(508) 799-9342"},
|
|
])
|
|
|
|
def _get_or_create_settings():
|
|
"""Get the singleton settings row, creating it with defaults on first access."""
|
|
settings = db.session.query(Admin_Settings).first()
|
|
if not settings:
|
|
settings = Admin_Settings(
|
|
company_name='Auburn Oil',
|
|
link_facebook='https://www.facebook.com/auburnoil',
|
|
link_google='https://www.google.com/search?client=firefox-b-1-d&sca_esv=02c44965d6d4b280&sca_upv=1&cs=1&output=search&kgmid=/g/11wcbqrx5l&q=Auburn+Oil&shndl=30&shem=lsde&source=sh/x/loc/act/m1/1&kgs=52995d809762cd61',
|
|
link_website='https://auburnoil.com',
|
|
link_google_review='https://g.page/r/CZHnPQ85LsMUEBM/review',
|
|
quick_calls=DEFAULT_QUICK_CALLS,
|
|
show_automatics=True,
|
|
show_stats=True,
|
|
show_service=True,
|
|
show_ticker=True,
|
|
default_theme='ocean',
|
|
)
|
|
db.session.add(settings)
|
|
db.session.commit()
|
|
return settings
|
|
|
|
|
|
@admin.route("/settings", methods=["GET"])
|
|
@custom_login_required
|
|
def get_settings():
|
|
"""Get admin settings (any logged-in user can read)."""
|
|
logger.info("GET /admin/settings")
|
|
settings = _get_or_create_settings()
|
|
schema = Admin_Settings_schema(many=False)
|
|
return success_response({"settings": schema.dump(settings)})
|
|
|
|
|
|
@admin.route("/settings", methods=["PUT"])
|
|
@admin_required
|
|
def update_settings():
|
|
"""Update admin settings (admin only). Does not update logo."""
|
|
logger.info("PUT /admin/settings")
|
|
settings = _get_or_create_settings()
|
|
data = request.json
|
|
|
|
allowed_fields = [
|
|
'company_name', 'link_facebook', 'link_google', 'link_website',
|
|
'link_google_review', 'quick_calls', 'show_automatics', 'show_stats',
|
|
'show_service', 'show_ticker', 'default_theme',
|
|
]
|
|
for field in allowed_fields:
|
|
if field in data:
|
|
value = data[field]
|
|
if field == 'quick_calls' and isinstance(value, list):
|
|
value = json.dumps(value)
|
|
setattr(settings, field, value)
|
|
|
|
settings.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
schema = Admin_Settings_schema(many=False)
|
|
return success_response({"settings": schema.dump(settings)})
|
|
|
|
|
|
@admin.route("/settings/logo", methods=["POST"])
|
|
@admin_required
|
|
def upload_logo():
|
|
"""Upload a logo image (admin only). Max 2MB."""
|
|
logger.info("POST /admin/settings/logo")
|
|
if 'logo' not in request.files:
|
|
return error_response("No file provided", 400)
|
|
|
|
file = request.files['logo']
|
|
if file.filename == '':
|
|
return error_response("No file selected", 400)
|
|
|
|
# 2MB limit
|
|
file_data = file.read()
|
|
if len(file_data) > 2 * 1024 * 1024:
|
|
return error_response("File too large. Maximum size is 2MB.", 400)
|
|
|
|
mime_type = file.content_type or 'image/png'
|
|
encoded = base64.b64encode(file_data).decode('utf-8')
|
|
|
|
settings = _get_or_create_settings()
|
|
settings.logo_base64 = encoded
|
|
settings.logo_mime_type = mime_type
|
|
settings.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
schema = Admin_Settings_schema(many=False)
|
|
return success_response({"settings": schema.dump(settings)})
|
|
|
|
|
|
@admin.route("/settings/logo", methods=["DELETE"])
|
|
@admin_required
|
|
def delete_logo():
|
|
"""Delete the custom logo (admin only)."""
|
|
logger.info("DELETE /admin/settings/logo")
|
|
settings = _get_or_create_settings()
|
|
settings.logo_base64 = None
|
|
settings.logo_mime_type = None
|
|
settings.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
schema = Admin_Settings_schema(many=False)
|
|
return success_response({"settings": schema.dump(settings)})
|