Files
eamco_office_api/app/admin/views.py
Edwin Eames 3066754821 feat: add admin settings system and improve customer/pricing endpoints
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>
2026-02-27 18:45:06 -05:00

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)})