136 lines
5.3 KiB
Python
136 lines
5.3 KiB
Python
"""
|
|
app/tenant/customers/routes.py
|
|
Customer management: list (search), create, view, edit, soft-delete, restore.
|
|
"""
|
|
import logging
|
|
from flask import Blueprint, render_template, redirect, url_for, flash, request, g
|
|
from flask_login import login_required
|
|
from app.extensions import db
|
|
from app.models.salon import Customer, Appointment, Transaction
|
|
from app.decorators import require_role, demo_readonly
|
|
from app.tenant.utils import log_tenant_action
|
|
|
|
logger = logging.getLogger(__name__)
|
|
customers_bp = Blueprint("customers", __name__, url_prefix="/customers")
|
|
|
|
|
|
@customers_bp.route("/")
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
def index():
|
|
q = request.args.get("q", "").strip()
|
|
query = Customer.query.filter_by(tenant_id=g.tenant.id).filter(
|
|
Customer.deleted_at.is_(None))
|
|
if q:
|
|
query = query.filter(
|
|
db.or_(
|
|
Customer.name.ilike(f"%{q}%"),
|
|
Customer.phone.ilike(f"%{q}%"),
|
|
Customer.email.ilike(f"%{q}%"),
|
|
)
|
|
)
|
|
customers = query.order_by(Customer.name).limit(200).all()
|
|
return render_template("tenant/customers/index.html", customers=customers, q=q)
|
|
|
|
|
|
@customers_bp.route("/new", methods=["GET", "POST"])
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
@demo_readonly
|
|
def create():
|
|
if request.method == "POST":
|
|
name = request.form.get("name", "").strip()
|
|
phone = request.form.get("phone", "").strip() or None
|
|
if not name:
|
|
return render_template("tenant/customers/form.html",
|
|
mode="create", error="Name is required.")
|
|
customer = Customer(
|
|
tenant_id=g.tenant.id,
|
|
name=name, phone=phone,
|
|
email=request.form.get("email", "").strip() or None,
|
|
date_of_birth=_parse_date(request.form.get("date_of_birth", "")),
|
|
notes=request.form.get("notes", "").strip() or None,
|
|
is_active=True, loyalty_points=0, no_show_count=0,
|
|
)
|
|
db.session.add(customer)
|
|
db.session.flush()
|
|
log_tenant_action("customer.create", "customer", customer.id, {"name": name})
|
|
db.session.commit()
|
|
flash(f"Customer \'{name}\' created.", "success")
|
|
return redirect(url_for("customers.view", customer_id=customer.id))
|
|
return render_template("tenant/customers/form.html", mode="create")
|
|
|
|
|
|
@customers_bp.route("/<int:customer_id>")
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
def view(customer_id):
|
|
customer = Customer.query.filter_by(
|
|
id=customer_id, tenant_id=g.tenant.id).filter(
|
|
Customer.deleted_at.is_(None)).first_or_404()
|
|
appointments = Appointment.query.filter_by(
|
|
tenant_id=g.tenant.id, customer_id=customer_id
|
|
).order_by(Appointment.start_time.desc()).limit(20).all()
|
|
transactions = Transaction.query.filter_by(
|
|
tenant_id=g.tenant.id, customer_id=customer_id
|
|
).filter(Transaction.voided_at.is_(None)).order_by(
|
|
Transaction.created_at.desc()).limit(20).all()
|
|
return render_template("tenant/customers/view.html",
|
|
customer=customer,
|
|
appointments=appointments,
|
|
transactions=transactions)
|
|
|
|
|
|
@customers_bp.route("/<int:customer_id>/edit", methods=["GET", "POST"])
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
@demo_readonly
|
|
def edit(customer_id):
|
|
customer = Customer.query.filter_by(
|
|
id=customer_id, tenant_id=g.tenant.id).filter(
|
|
Customer.deleted_at.is_(None)).first_or_404()
|
|
if request.method == "POST":
|
|
name = request.form.get("name", "").strip()
|
|
if not name:
|
|
return render_template("tenant/customers/form.html",
|
|
mode="edit", customer=customer,
|
|
error="Name is required.")
|
|
customer.name = name
|
|
customer.phone = request.form.get("phone", "").strip() or None
|
|
customer.email = request.form.get("email", "").strip() or None
|
|
customer.date_of_birth = _parse_date(request.form.get("date_of_birth", ""))
|
|
customer.notes = request.form.get("notes", "").strip() or None
|
|
log_tenant_action("customer.edit", "customer", customer.id, {"name": name})
|
|
db.session.commit()
|
|
flash(f"Customer \'{name}\' updated.", "success")
|
|
return redirect(url_for("customers.view", customer_id=customer.id))
|
|
return render_template("tenant/customers/form.html",
|
|
mode="edit", customer=customer)
|
|
|
|
|
|
@customers_bp.route("/<int:customer_id>/delete", methods=["POST"])
|
|
@login_required
|
|
@require_role("tenant_admin")
|
|
@demo_readonly
|
|
def soft_delete(customer_id):
|
|
customer = Customer.query.filter_by(
|
|
id=customer_id, tenant_id=g.tenant.id).filter(
|
|
Customer.deleted_at.is_(None)).first_or_404()
|
|
from datetime import datetime, timezone
|
|
customer.deleted_at = datetime.now(timezone.utc)
|
|
log_tenant_action("customer.delete", "customer", customer.id,
|
|
{"name": customer.name})
|
|
db.session.commit()
|
|
flash(f"Customer \'{customer.name}\' deleted.", "success")
|
|
return redirect(url_for("customers.index"))
|
|
|
|
|
|
def _parse_date(value):
|
|
if not value:
|
|
return None
|
|
try:
|
|
from datetime import date
|
|
return date.fromisoformat(value)
|
|
except ValueError:
|
|
return None
|