Backend:
- New model: Customer (customers table) — name, nickname, phone, email, notes, is_active
- Order model: add customer_id nullable FK → customers
- New router: /api/customers/ — list (with ?search=), create, get, update, soft-delete,
GET /{id}/orders (visit history with totals)
- Each CustomerOut includes visit_count + total_spent (computed from closed/paid orders)
- PUT /api/orders/{id}/customer — assign or unassign a customer; manager-only;
validates customer is active; broadcasts order_updated SSE event
- GET /api/orders/{id} now returns customer_id, customer_name (with nickname), customer_phone
- Migration: CREATE TABLE customers + ALTER TABLE orders ADD COLUMN customer_id
Frontend:
- CustomersPage (/customers): searchable list (name/nickname/phone), avatar initials,
visit count + total_spent on each row; click → detail panel slides in showing stats
(visits, total spent, avg ticket), contact info, notes, full visit history
- OrderDetailPage: new Πελάτης card — shows assigned customer in blue if set;
for open orders shows "+ Ανάθεση πελάτη" button → inline search dropdown → assign;
Αφαίρεση button to unassign
- Sidebar: Users icon for Πελάτες (between Επαφές and Ρυθμίσεις)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
4.3 KiB
Python
118 lines
4.3 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from sqlalchemy.orm import Session
|
|
from typing import List, Optional
|
|
|
|
from database import get_db
|
|
from models.customers import Customer
|
|
from models.order import Order, OrderItem
|
|
from models.user import User
|
|
from schemas.customers import CustomerCreate, CustomerUpdate, CustomerOut, AssignCustomerRequest
|
|
from routers.deps import require_manager, get_current_user
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _customer_out(c: Customer, db: Session) -> dict:
|
|
orders = db.query(Order).filter(
|
|
Order.customer_id == c.id,
|
|
Order.status.in_(["closed", "paid"]),
|
|
).all()
|
|
total_spent = sum(
|
|
sum(i.unit_price * i.quantity for i in o.items if i.status in ("active", "paid"))
|
|
for o in orders
|
|
)
|
|
return {
|
|
"id": c.id,
|
|
"name": c.name,
|
|
"nickname": c.nickname,
|
|
"phone": c.phone,
|
|
"email": c.email,
|
|
"notes": c.notes,
|
|
"is_active": c.is_active,
|
|
"created_at": c.created_at,
|
|
"created_by_id": c.created_by_id,
|
|
"visit_count": len(orders),
|
|
"total_spent": round(total_spent, 2),
|
|
}
|
|
|
|
|
|
@router.get("/", response_model=List[CustomerOut])
|
|
def list_customers(
|
|
search: Optional[str] = None,
|
|
include_inactive: bool = False,
|
|
db: Session = Depends(get_db),
|
|
user: User = Depends(require_manager),
|
|
):
|
|
q = db.query(Customer)
|
|
if not include_inactive:
|
|
q = q.filter(Customer.is_active == True)
|
|
if search:
|
|
term = f"%{search}%"
|
|
q = q.filter(
|
|
Customer.name.ilike(term) |
|
|
Customer.nickname.ilike(term) |
|
|
Customer.phone.ilike(term)
|
|
)
|
|
customers = q.order_by(Customer.name).all()
|
|
return [_customer_out(c, db) for c in customers]
|
|
|
|
|
|
@router.post("/", response_model=CustomerOut, status_code=status.HTTP_201_CREATED)
|
|
def create_customer(body: CustomerCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
|
customer = Customer(**body.model_dump(), created_by_id=user.id)
|
|
db.add(customer)
|
|
db.commit()
|
|
db.refresh(customer)
|
|
return _customer_out(customer, db)
|
|
|
|
|
|
@router.get("/{customer_id}", response_model=CustomerOut)
|
|
def get_customer(customer_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
|
c = db.query(Customer).filter(Customer.id == customer_id).first()
|
|
if not c:
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
return _customer_out(c, db)
|
|
|
|
|
|
@router.put("/{customer_id}", response_model=CustomerOut)
|
|
def update_customer(customer_id: int, body: CustomerUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
|
c = db.query(Customer).filter(Customer.id == customer_id).first()
|
|
if not c:
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
for field, value in body.model_dump(exclude_none=True).items():
|
|
setattr(c, field, value)
|
|
db.commit()
|
|
db.refresh(c)
|
|
return _customer_out(c, db)
|
|
|
|
|
|
@router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
def delete_customer(customer_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
|
c = db.query(Customer).filter(Customer.id == customer_id).first()
|
|
if not c:
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
c.is_active = False
|
|
db.commit()
|
|
|
|
|
|
@router.get("/{customer_id}/orders")
|
|
def customer_orders(customer_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)):
|
|
c = db.query(Customer).filter(Customer.id == customer_id).first()
|
|
if not c:
|
|
raise HTTPException(status_code=404, detail="Customer not found")
|
|
from models.table import Table
|
|
orders = db.query(Order).filter(Order.customer_id == customer_id).order_by(Order.opened_at.desc()).all()
|
|
tables_map = {t.id: (t.label or f"T{t.number}") for t in db.query(Table).all()}
|
|
result = []
|
|
for o in orders:
|
|
total = sum(i.unit_price * i.quantity for i in o.items if i.status in ("active", "paid"))
|
|
result.append({
|
|
"id": o.id,
|
|
"table_name": tables_map.get(o.table_id) if o.table_id else None,
|
|
"opened_at": o.opened_at,
|
|
"status": o.status,
|
|
"total": round(total, 2),
|
|
"item_count": sum(i.quantity for i in o.items if i.status in ("active", "paid")),
|
|
})
|
|
return {"orders": result}
|