05/07 Phase 3: initial codes
This commit is contained in:
@@ -1,7 +1,135 @@
|
||||
"""
|
||||
app/tenant/customers/routes.py
|
||||
Phase 3+ implementation.
|
||||
Customer management: list (search), create, view, edit, soft-delete, restore.
|
||||
"""
|
||||
from flask import Blueprint
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user