diff --git a/app/admin/views.py b/app/admin/views.py index d7b0d6a..7f2395f 100755 --- a/app/admin/views.py +++ b/app/admin/views.py @@ -1,4 +1,6 @@ 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 @@ -8,8 +10,8 @@ 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 -from app.common.decorators import admin_required +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__) @@ -129,3 +131,119 @@ def get_voip_routing(): 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)}) diff --git a/app/classes/admin.py b/app/classes/admin.py index fd9883a..d60772a 100755 --- a/app/classes/admin.py +++ b/app/classes/admin.py @@ -1,5 +1,6 @@ from app import db, ma from datetime import datetime +import json class Admin_Company(db.Model): @@ -30,4 +31,30 @@ class Call(db.Model): id = db.Column(db.Integer, primary_key=True, index=True) current_phone = db.Column(db.String(500)) - created_at = db.Column(db.DateTime, default=datetime.utcnow) \ No newline at end of file + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class Admin_Settings(db.Model): + __tablename__ = 'admin_settings' + __table_args__ = {"schema": "public"} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + updated_at = db.Column(db.TIMESTAMP(), default=datetime.utcnow, onupdate=datetime.utcnow) + logo_base64 = db.Column(db.Text, nullable=True) + logo_mime_type = db.Column(db.VARCHAR(50), nullable=True) + company_name = db.Column(db.VARCHAR(250), default='Auburn Oil') + link_facebook = db.Column(db.VARCHAR(500), nullable=True) + link_google = db.Column(db.VARCHAR(500), nullable=True) + link_website = db.Column(db.VARCHAR(500), nullable=True) + link_google_review = db.Column(db.VARCHAR(500), nullable=True) + quick_calls = db.Column(db.Text, nullable=True) + show_automatics = db.Column(db.Boolean, default=True) + show_stats = db.Column(db.Boolean, default=True) + show_service = db.Column(db.Boolean, default=True) + show_ticker = db.Column(db.Boolean, default=True) + default_theme = db.Column(db.VARCHAR(50), default='ocean') + + +class Admin_Settings_schema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Admin_Settings \ No newline at end of file diff --git a/app/customer/views.py b/app/customer/views.py index c2d5a42..1fccb9a 100755 --- a/app/customer/views.py +++ b/app/customer/views.py @@ -8,7 +8,8 @@ from app.common.decorators import login_required as common_login_required from app.common.responses import error_response, success_response logger = logging.getLogger(__name__) -from datetime import datetime +from datetime import datetime, timedelta +from sqlalchemy import func from app.classes.cards import Card_Card from app.classes.customer import \ Customer_Customer, \ @@ -18,6 +19,7 @@ from app.classes.customer import \ Customer_Tank_Inspection_schema,\ Customer_Tank_Inspection from app.classes.service import Service_Parts +from app.classes.delivery import Delivery_Delivery from app.classes.admin import Admin_Company from app.classes.auto import Auto_Delivery,Auto_Delivery_schema from app.classes.stats_customer import Stats_Customer @@ -437,6 +439,38 @@ def customer_count(): return success_response({'count': get_customer}) +@customer.route("/count/active", methods=["GET"]) +@login_required +def customer_count_active(): + """ + Count distinct customers who had a delivery within the past year. + """ + logger.info("GET /customer/count/active - Getting active past year count") + one_year_ago = datetime.utcnow().date() - timedelta(days=365) + count = (db.session + .query(func.count(func.distinct(Delivery_Delivery.customer_id))) + .filter(Delivery_Delivery.when_delivered >= one_year_ago) + .scalar()) + return success_response({'count': count or 0}) + + +@customer.route("/count/dedicated", methods=["GET"]) +@login_required +def customer_count_dedicated(): + """ + Count distinct customers with more than one finalized delivery. + """ + logger.info("GET /customer/count/dedicated - Getting dedicated customer count") + subquery = (db.session + .query(Delivery_Delivery.customer_id) + .filter(Delivery_Delivery.delivery_status == 10) + .group_by(Delivery_Delivery.customer_id) + .having(func.count(Delivery_Delivery.id) > 1) + .subquery()) + count = db.session.query(func.count()).select_from(subquery).scalar() + return success_response({'count': count or 0}) + + @customer.route("/automatic/status/", methods=["GET"]) @login_required def customer_automatic_status(customer_id): @@ -475,7 +509,7 @@ def get_all_automatic_deliveries(): -@customer.route("/automatic/assign/", methods=["GET"]) +@customer.route("/automatic/assign/", methods=["PUT"]) @login_required def customer_automatic_assignment(customer_id): """ diff --git a/app/info/views.py b/app/info/views.py index a2deccf..3d4ec87 100755 --- a/app/info/views.py +++ b/app/info/views.py @@ -52,14 +52,10 @@ def get_oil_price_today(): .query(Pricing_Oil_Oil) .order_by(Pricing_Oil_Oil.date.desc()) .first()) - return success_response({ - 'price_from_supplier': get_price_query.price_from_supplier, - 'price_for_customer': get_price_query.price_for_customer, - 'price_for_employee': get_price_query.price_for_employee, - 'price_same_day': get_price_query.price_same_day, - 'price_prime': get_price_query.price_prime, - 'price_emergency': get_price_query.price_emergency, - }) + if get_price_query: + delivery_schema = Pricing_Oil_Oil_schema(many=False) + return success_response(delivery_schema.dump(get_price_query)) + return error_response("No pricing data found", 404) @info.route("/price/oil/table", methods=["GET"]) diff --git a/app/stats/views.py b/app/stats/views.py index d0bee1b..dd889ae 100755 --- a/app/stats/views.py +++ b/app/stats/views.py @@ -2,6 +2,7 @@ import logging from datetime import date from app.stats import stats import datetime +from flask import request from app import db from app.common.responses import success_response from app.classes.delivery import Delivery_Delivery diff --git a/migrations/versions/a1b2c3d4e5f6_add_admin_settings_table.py b/migrations/versions/a1b2c3d4e5f6_add_admin_settings_table.py new file mode 100644 index 0000000..ccee037 --- /dev/null +++ b/migrations/versions/a1b2c3d4e5f6_add_admin_settings_table.py @@ -0,0 +1,44 @@ +"""Add admin_settings table + +Revision ID: a1b2c3d4e5f6 +Revises: 3d217261c994 +Create Date: 2026-02-08 00:00:00.000000 + +NOTE: Move this file to migrations/versions/ before running flask db upgrade. +Run: sudo mv eamco_office_api/a1b2c3d4e5f6_add_admin_settings_table.py eamco_office_api/migrations/versions/ +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'a1b2c3d4e5f6' +down_revision = '3d217261c994' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'admin_settings', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('updated_at', sa.TIMESTAMP(), nullable=True), + sa.Column('logo_base64', sa.Text(), nullable=True), + sa.Column('logo_mime_type', sa.VARCHAR(length=50), nullable=True), + sa.Column('company_name', sa.VARCHAR(length=250), server_default='Auburn Oil', nullable=True), + sa.Column('link_facebook', sa.VARCHAR(length=500), nullable=True), + sa.Column('link_google', sa.VARCHAR(length=500), nullable=True), + sa.Column('link_website', sa.VARCHAR(length=500), nullable=True), + sa.Column('link_google_review', sa.VARCHAR(length=500), nullable=True), + sa.Column('quick_calls', sa.Text(), nullable=True), + sa.Column('show_automatics', sa.Boolean(), server_default='true', nullable=True), + sa.Column('show_stats', sa.Boolean(), server_default='true', nullable=True), + sa.Column('show_service', sa.Boolean(), server_default='true', nullable=True), + sa.Column('show_ticker', sa.Boolean(), server_default='true', nullable=True), + sa.Column('default_theme', sa.VARCHAR(length=50), server_default='ocean', nullable=True), + sa.PrimaryKeyConstraint('id'), + schema='public' + ) + + +def downgrade(): + op.drop_table('admin_settings', schema='public')