05/06 Phase 2: updated and added new files

This commit is contained in:
2026-05-06 17:40:21 -04:00
parent d924e41d9e
commit 8da0c11290
28 changed files with 2113 additions and 35 deletions
+121 -2
View File
@@ -1,7 +1,126 @@
"""
app/admin/plans/routes.py
Phase 2 implementation.
Subscription plan management: create, edit, toggle active.
Feature flags stored as JSON. All changes logged.
"""
from flask import Blueprint
import logging
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required
from app.extensions import db
from app.models.platform import Plan
from app.admin.utils import superadmin_required, log_admin_action, model_to_dict
logger = logging.getLogger(__name__)
plans_bp = Blueprint("plans", __name__, url_prefix="/plans")
_AUDIT_FIELDS = ["name", "price_monthly", "max_staff", "max_locations", "features_json", "is_active"]
# All known feature flags — used to build the form checkboxes
ALL_FEATURES = [
"pos", "appointments", "customers", "services", "promotions",
"appointment_reminders", "customer_reviews", "reconciliation",
"basic_reports", "inventory", "commission", "full_reports",
"multi_location", "online_booking", "waitlist", "marketing",
]
@plans_bp.route("/")
@login_required
@superadmin_required
def index():
all_plans = Plan.query.order_by(Plan.price_monthly.asc()).all()
return render_template("admin/plans/index.html", plans=all_plans)
@plans_bp.route("/new", methods=["GET", "POST"])
@login_required
@superadmin_required
def create():
if request.method == "POST":
plan, error = _plan_from_form(None)
if error:
return render_template("admin/plans/form.html", mode="create",
all_features=ALL_FEATURES, error=error)
db.session.add(plan)
db.session.flush()
log_admin_action("plan.create", "plan", plan.id,
after=model_to_dict(plan, _AUDIT_FIELDS))
db.session.commit()
logger.info("Plan created: id=%s name=%s", plan.id, plan.name)
flash(f"Plan \'{plan.name}\' created.", "success")
return redirect(url_for("plans.index"))
return render_template("admin/plans/form.html", mode="create", all_features=ALL_FEATURES)
@plans_bp.route("/<int:plan_id>/edit", methods=["GET", "POST"])
@login_required
@superadmin_required
def edit(plan_id):
plan = Plan.query.get_or_404(plan_id)
before = model_to_dict(plan, _AUDIT_FIELDS)
if request.method == "POST":
_, error = _plan_from_form(plan)
if error:
return render_template("admin/plans/form.html", mode="edit",
plan=plan, all_features=ALL_FEATURES, error=error)
log_admin_action("plan.edit", "plan", plan.id,
before=before, after=model_to_dict(plan, _AUDIT_FIELDS))
db.session.commit()
logger.info("Plan edited: id=%s name=%s", plan.id, plan.name)
flash(f"Plan \'{plan.name}\' updated.", "success")
return redirect(url_for("plans.index"))
return render_template("admin/plans/form.html", mode="edit",
plan=plan, all_features=ALL_FEATURES)
@plans_bp.route("/<int:plan_id>/toggle-active", methods=["POST"])
@login_required
@superadmin_required
def toggle_active(plan_id):
plan = Plan.query.get_or_404(plan_id)
before = model_to_dict(plan, _AUDIT_FIELDS)
plan.is_active = not plan.is_active
action = "plan.activate" if plan.is_active else "plan.deactivate"
log_admin_action(action, "plan", plan.id, before=before,
after=model_to_dict(plan, _AUDIT_FIELDS))
db.session.commit()
status = "activated" if plan.is_active else "deactivated"
flash(f"Plan \'{plan.name}\' {status}.", "success")
return redirect(url_for("plans.index"))
def _plan_from_form(plan):
"""Parse form data into a Plan object. Returns (plan, error_string|None)."""
name = request.form.get("name", "").strip()
if not name:
return plan, "Plan name is required."
try:
price = float(request.form.get("price_monthly", 0))
except ValueError:
return plan, "Invalid price."
max_staff_raw = request.form.get("max_staff", "").strip()
max_loc_raw = request.form.get("max_locations", "").strip()
max_staff = int(max_staff_raw) if max_staff_raw.isdigit() else None
max_locations = int(max_loc_raw) if max_loc_raw.isdigit() else None
features = {flag: (request.form.get(f"feature_{flag}") == "1") for flag in ALL_FEATURES}
if plan is None:
plan = Plan(name=name)
else:
plan.name = name
plan.price_monthly = price
plan.max_staff = max_staff
plan.max_locations = max_locations
plan.features_json = features
plan.is_active = request.form.get("is_active") == "1"
return plan, None