import logging from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import JSONResponse from fastapi.encoders import jsonable_encoder from sqlalchemy.orm import Session from datetime import datetime, date, timedelta from database import get_db from app.auth import get_current_user from app.models.service import Service_Service, Service_Parts, Service_Plans from app.models.customer import Customer_Customer from app.models.auto import Auto_Delivery from app.models.auth import Auth_User from app.constants import DEFAULT_PAGE_SIZE from app.schema.service import ( ServiceResponse, ServiceCreateRequest, ServiceUpdateRequest, ServiceCostUpdateRequest, ServicePartsResponse, ServicePartsUpdateRequest, ServicePlanResponse, ServicePlanCreateRequest, ServicePlanUpdateRequest, CalendarEvent ) logger = logging.getLogger(__name__) router = APIRouter( prefix="/service", tags=["service"], ) def success_response(data: dict = None, status_code: int = 200): """Standard success response.""" response = {"ok": True} if data: response.update(data) return JSONResponse(content=jsonable_encoder(response), status_code=status_code) def error_response(message: str, status_code: int = 400): """Standard error response.""" return JSONResponse( content={"ok": False, "error": message}, status_code=status_code ) def serialize_service(service: Service_Service) -> dict: """Serialize a service record to dict with proper date formatting.""" return { "id": service.id, "customer_id": service.customer_id, "customer_name": service.customer_name, "customer_address": service.customer_address, "customer_town": service.customer_town, "customer_state": service.customer_state, "customer_zip": service.customer_zip, "type_service_call": service.type_service_call, "when_ordered": service.when_ordered.isoformat() if service.when_ordered else None, "scheduled_date": service.scheduled_date.isoformat() if service.scheduled_date else None, "description": service.description, "service_cost": str(service.service_cost) if service.service_cost is not None else None, "payment_type": service.payment_type, "payment_card_id": service.payment_card_id, "payment_status": service.payment_status, } def serialize_parts(parts: Service_Parts) -> dict: """Serialize a service parts record to dict.""" return { "id": parts.id, "customer_id": parts.customer_id, "oil_filter": parts.oil_filter, "oil_filter_2": parts.oil_filter_2, "oil_nozzle": parts.oil_nozzle, "oil_nozzle_2": parts.oil_nozzle_2, "hot_water_tank": parts.hot_water_tank, } def serialize_plan(plan: Service_Plans) -> dict: """Serialize a service plan record to dict.""" return { "id": plan.id, "customer_id": plan.customer_id, "contract_plan": plan.contract_plan, "contract_years": plan.contract_years, "contract_start_date": plan.contract_start_date.isoformat() if plan.contract_start_date else None, } @router.get("/all", status_code=200) def get_all_service_calls( db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Get all services for calendar view.""" logger.info("GET /service/all - Fetching all service calls for calendar") try: all_services = db.query(Service_Service).all() color_map = { 0: {"backgroundColor": "blue", "textColor": "white"}, 1: {"backgroundColor": "red", "textColor": "white"}, 2: {"backgroundColor": "green", "textColor": "white"}, 3: {"backgroundColor": "yellow", "textColor": "black"}, 4: {"backgroundColor": "black", "textColor": "white"} } service_type_map = {0: 'Tune-up', 1: 'No Heat', 2: 'Fix', 3: 'Tank Install', 4: 'Other'} calendar_events = [] for service_record in all_services: service_type_text = service_type_map.get(service_record.type_service_call, 'Service') event_title = f"{service_type_text}: {service_record.customer_name}" event_colors = color_map.get(service_record.type_service_call, {"backgroundColor": "gray", "textColor": "white"}) start_date = service_record.scheduled_date.isoformat() if service_record.scheduled_date else None event_data = { "id": service_record.id, "title": event_title, "start": start_date, "end": None, "extendedProps": { "customer_id": service_record.customer_id, "description": service_record.description, "type_service_call": service_record.type_service_call, "service_cost": str(service_record.service_cost) if service_record.service_cost is not None else None }, "backgroundColor": event_colors.get("backgroundColor"), "textColor": event_colors.get("textColor"), "borderColor": event_colors.get("backgroundColor") } calendar_events.append(event_data) return success_response({"events": calendar_events}) except Exception as e: logger.error(f"Error in /service/all: {e}") return error_response(str(e), 500) @router.get("/upcoming", status_code=200) def get_upcoming_service_calls( db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Fetches a list of all future service calls from today onwards.""" logger.info("GET /service/upcoming - Fetching upcoming service calls") now = datetime.now() upcoming_services = ( db.query(Service_Service) .filter(Service_Service.scheduled_date >= now) .order_by(Service_Service.scheduled_date.asc()) .limit(DEFAULT_PAGE_SIZE) .all() ) result = [serialize_service(s) for s in upcoming_services] return success_response({"services": result}) @router.get("/past", status_code=200) def get_past_service_calls( db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Fetches a list of all past service calls before today.""" past_services = ( db.query(Service_Service) .filter(Service_Service.scheduled_date < datetime.combine(date.today(), datetime.min.time())) .order_by(Service_Service.scheduled_date.asc()) .limit(DEFAULT_PAGE_SIZE) .all() ) result = [serialize_service(s) for s in past_services] return success_response({"services": result}) @router.get("/today", status_code=200) def get_today_service_calls( db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Fetches a list of all service calls for today.""" start_of_today = datetime.combine(date.today(), datetime.min.time()) start_of_tomorrow = datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) today_services = ( db.query(Service_Service) .filter(Service_Service.scheduled_date >= start_of_today) .filter(Service_Service.scheduled_date < start_of_tomorrow) .order_by(Service_Service.scheduled_date.asc()) .limit(DEFAULT_PAGE_SIZE) .all() ) result = [serialize_service(s) for s in today_services] return success_response({"services": result}) @router.get("/upcoming/count", status_code=200) def get_upcoming_service_calls_count( db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Get count of upcoming service calls.""" now = datetime.now() try: count = db.query(Service_Service).filter(Service_Service.scheduled_date >= now).count() return success_response({"count": count}) except Exception as e: return error_response(str(e), 500) @router.get("/for-customer/{customer_id}", status_code=200) def get_service_calls_for_customer( customer_id: int, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Get all service calls for a specific customer.""" service_records = ( db.query(Service_Service) .filter(Service_Service.customer_id == customer_id) .order_by(Service_Service.scheduled_date.desc()) .all() ) result = [serialize_service(s) for s in service_records] return success_response({"services": result}) @router.get("/{id}", status_code=200) def get_service_by_id( id: int, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Get a specific service call by ID.""" service_record = db.query(Service_Service).filter(Service_Service.id == id).first() if not service_record: raise HTTPException(status_code=404, detail="Service not found") return success_response({"service": serialize_service(service_record)}) @router.post("/create", status_code=201) async def create_service_call( request: Request, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Create a new service call.""" data = await request.json() if not data: return error_response("No data provided", 400) cus_id = data.get('customer_id') get_customer = db.query(Customer_Customer).filter(Customer_Customer.id == cus_id).first() if not get_customer: return error_response(f"Customer with id {cus_id} not found.", 404) scheduled_datetime_str = data.get('expected_delivery_date') scheduled_datetime_obj = datetime.fromisoformat(scheduled_datetime_str) new_service_call = Service_Service( customer_id=get_customer.id, customer_name=get_customer.customer_first_name + ' ' + get_customer.customer_last_name, customer_address=get_customer.customer_address, customer_town=get_customer.customer_town, customer_state=get_customer.customer_state, customer_zip=get_customer.customer_zip, type_service_call=data.get('type_service_call'), when_ordered=datetime.utcnow(), scheduled_date=scheduled_datetime_obj, description=data.get('description'), service_cost=None, ) db.add(new_service_call) db.commit() return success_response({"id": new_service_call.id}, 201) @router.put("/update/{id}", status_code=200) async def update_service_call( id: int, request: Request, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Update a service call.""" service_record = db.query(Service_Service).filter(Service_Service.id == id).first() if not service_record: raise HTTPException(status_code=404, detail="Service not found") data = await request.json() if not data: return error_response("No data provided", 400) scheduled_datetime_str = data.get('scheduled_date') if scheduled_datetime_str: service_record.scheduled_date = datetime.fromisoformat(scheduled_datetime_str) service_record.type_service_call = data.get('type_service_call', service_record.type_service_call) service_record.description = data.get('description', service_record.description) service_record.service_cost = data.get('service_cost', service_record.service_cost) try: db.commit() return success_response({"service": serialize_service(service_record)}) except Exception as e: db.rollback() return error_response(str(e), 500) @router.put("/update-cost/{id}", status_code=200) async def update_service_cost( id: int, request: Request, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Dedicated endpoint to update only the service cost for a service call.""" try: service_record = db.query(Service_Service).filter(Service_Service.id == id).first() if not service_record: raise HTTPException(status_code=404, detail="Service not found") data = await request.json() if not data: return error_response("No data provided", 400) new_cost = data.get('service_cost') if new_cost is None: return error_response("service_cost is required", 400) try: new_cost_float = float(new_cost) except (ValueError, TypeError): return error_response("service_cost must be a valid number", 400) service_record.service_cost = new_cost_float db.commit() return success_response({ "service_id": id, "service_cost_updated": new_cost_float, "message": f"Service {id} cost updated to ${new_cost_float}" }) except HTTPException: raise except Exception as e: db.rollback() logger.error(f"Error updating service cost for service {id}: {e}") return error_response(str(e), 500) @router.delete("/delete/{id}", status_code=200) def delete_service_call( id: int, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Delete a service call.""" service_record = db.query(Service_Service).filter(Service_Service.id == id).first() if not service_record: raise HTTPException(status_code=404, detail="Service not found") try: db.delete(service_record) db.commit() return success_response({"message": "Service deleted successfully"}) except Exception as e: db.rollback() return error_response(str(e), 500) # Service Parts endpoints @router.get("/parts/customer/{customer_id}", status_code=200) def get_service_parts( customer_id: int, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Get service parts for a customer.""" parts = db.query(Service_Parts).filter(Service_Parts.customer_id == customer_id).first() if parts: return success_response({"parts": serialize_parts(parts)}) else: return success_response({"parts": { "customer_id": customer_id, "oil_filter": "", "oil_filter_2": "", "oil_nozzle": "", "oil_nozzle_2": "", "hot_water_tank": 0 }}) @router.post("/parts/update/{customer_id}", status_code=200) async def update_service_parts( customer_id: int, request: Request, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Update service parts for a customer.""" try: data = await request.json() if not data: return error_response("No data provided", 400) get_customer = db.query(Customer_Customer).filter(Customer_Customer.id == customer_id).first() parts = db.query(Service_Parts).filter(Service_Parts.customer_id == customer_id).first() if not parts: parts = Service_Parts(customer_id=customer_id) db.add(parts) parts.oil_filter = data.get('oil_filter', parts.oil_filter) parts.oil_filter_2 = data.get('oil_filter_2', parts.oil_filter_2) parts.oil_nozzle = data.get('oil_nozzle', parts.oil_nozzle) parts.oil_nozzle_2 = data.get('oil_nozzle_2', parts.oil_nozzle_2) parts.hot_water_tank = data.get('hot_water_tank', parts.hot_water_tank if parts.hot_water_tank is not None else 0) # Sync to Auto_Delivery if customer is automatic if get_customer and get_customer.customer_automatic == 1: get_auto = db.query(Auto_Delivery).filter(Auto_Delivery.customer_id == customer_id).first() if get_auto: get_auto.hot_water_summer = parts.hot_water_tank db.add(get_auto) db.commit() return success_response({"message": "Service parts updated successfully"}) except Exception as e: db.rollback() return error_response(str(e), 500) # Service Plans endpoints @router.get("/plans/active", status_code=200) def get_active_service_plans( db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Get all active service plans (contract_plan > 0).""" try: plans = db.query(Service_Plans).filter(Service_Plans.contract_plan > 0).all() result = [] for plan in plans: plan_dict = serialize_plan(plan) customer = db.query(Customer_Customer).filter(Customer_Customer.id == plan.customer_id).first() if customer: plan_dict['customer_name'] = f"{customer.customer_first_name} {customer.customer_last_name}" plan_dict['customer_address'] = customer.customer_address plan_dict['customer_town'] = customer.customer_town result.append(plan_dict) return success_response({"plans": result}) except Exception as e: return error_response(str(e), 500) @router.get("/plans/customer/{customer_id}", status_code=200) def get_customer_service_plan( customer_id: int, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Get service plan for a specific customer.""" try: plan = db.query(Service_Plans).filter(Service_Plans.customer_id == customer_id).first() if plan: return success_response({"plan": serialize_plan(plan)}) else: return success_response({"plan": None}) except Exception as e: return error_response(str(e), 500) @router.post("/plans/create", status_code=201) async def create_service_plan( request: Request, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Create a new service plan for a customer.""" data = await request.json() if not data: return error_response("No data provided", 400) try: new_plan = Service_Plans( customer_id=data['customer_id'], contract_plan=data['contract_plan'], contract_years=data['contract_years'], contract_start_date=datetime.fromisoformat(data['contract_start_date']).date() ) db.add(new_plan) db.commit() return success_response({"plan": serialize_plan(new_plan)}, 201) except Exception as e: db.rollback() return error_response(str(e), 500) @router.put("/plans/update/{customer_id}", status_code=200) async def update_service_plan( customer_id: int, request: Request, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Update existing service plan for a customer.""" data = await request.json() if not data: return error_response("No data provided", 400) try: plan = db.query(Service_Plans).filter(Service_Plans.customer_id == customer_id).first() if not plan: # Create new plan if it doesn't exist plan = Service_Plans(customer_id=customer_id) db.add(plan) plan.contract_plan = data.get('contract_plan', plan.contract_plan) plan.contract_years = data.get('contract_years', plan.contract_years) if data.get('contract_start_date'): plan.contract_start_date = datetime.fromisoformat(data['contract_start_date']).date() db.commit() return success_response({"plan": serialize_plan(plan)}) except Exception as e: db.rollback() return error_response(str(e), 500) @router.delete("/plans/delete/{customer_id}", status_code=200) def delete_service_plan( customer_id: int, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Delete service plan for a customer.""" try: plan = db.query(Service_Plans).filter(Service_Plans.customer_id == customer_id).first() if not plan: return error_response("Service plan not found", 404) db.delete(plan) db.commit() return success_response({"message": "Service plan deleted successfully"}) except Exception as e: db.rollback() return error_response(str(e), 500) @router.put("/payment/{service_id}/{payment_type}", status_code=200) def process_service_payment( service_id: int, payment_type: int, db: Session = Depends(get_db), current_user: Auth_User = Depends(get_current_user) ): """Process payment for a service call.""" service = db.query(Service_Service).filter(Service_Service.id == service_id).first() if not service: return error_response("Service not found", 404) service.payment_type = payment_type service.payment_status = 2 try: db.commit() return success_response({}) except Exception as e: db.rollback() return error_response(str(e), 500)