import logging from flask import request from app.service import service from app import db from app.common.responses import error_response, success_response from datetime import datetime, date, timedelta from app.classes.customer import (Customer_Customer) from app.classes.service import (Service_Service, Service_Service_schema, Service_Parts, Service_Parts_schema, Service_Plans, Service_Plans_schema ) from app.classes.auto import Auto_Delivery from flask_login import login_required from app.constants import DEFAULT_PAGE_SIZE logger = logging.getLogger(__name__) @service.route("/all", methods=["GET"]) @login_required def get_all_service_calls(): logger.info("GET /service/all - Fetching all service calls for calendar") try: all_services = Service_Service.query.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"}) # Use the schema to safely get the date string serialized_record = Service_Service_schema().dump(service_record) start_date = serialized_record.get('scheduled_date') 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) # --- THIS IS THE FIX --- # The logic from /all has been copied here to ensure a consistent data structure. @service.route("/upcoming", methods=["GET"]) @login_required def get_upcoming_service_calls(): """ 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 = ( Service_Service.query .filter(Service_Service.scheduled_date >= now) .order_by(Service_Service.scheduled_date.asc()) .limit(DEFAULT_PAGE_SIZE) .all() ) service_schema = Service_Service_schema(many=True) result = service_schema.dump(upcoming_services) return success_response({"services": result}) @service.route("/past", methods=["GET"]) @login_required def get_past_service_calls(): """ Fetches a list of all past service calls before today. """ past_services = ( Service_Service.query .filter(Service_Service.scheduled_date < datetime.combine(date.today(), datetime.min.time())) .order_by(Service_Service.scheduled_date.asc()) .limit(DEFAULT_PAGE_SIZE) .all() ) service_schema = Service_Service_schema(many=True) result = service_schema.dump(past_services) return success_response({"services": result}) @service.route("/today", methods=["GET"]) @login_required def get_today_service_calls(): """ 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 = ( Service_Service.query .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() ) service_schema = Service_Service_schema(many=True) result = service_schema.dump(today_services) return success_response({"services": result}) @service.route("/upcoming/count", methods=["GET"]) @login_required def get_upcoming_service_calls_count(): now = datetime.now() try: count = (db.session.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) @service.route("/for-customer/", methods=["GET"]) @login_required def get_service_calls_for_customer(customer_id): service_records = (Service_Service.query.filter_by(customer_id=customer_id).order_by(Service_Service.scheduled_date.desc()).all()) service_schema = Service_Service_schema(many=True) result = service_schema.dump(service_records) return success_response({"services": result}) @service.route("/create", methods=["POST"]) @login_required def create_service_call(): data = request.get_json() if not data: return error_response("No data provided", 400) cus_id=data.get('customer_id') get_customer = (db.session.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.session.add(new_service_call) db.session.commit() return success_response({"id": new_service_call.id}, 201) @service.route("/update-cost/", methods=["PUT"]) @login_required def update_service_cost(id): """ Dedicated endpoint to update only the service cost for a service call. This is used after payment capture/charge to update the actual amount. """ try: # Find the service service_record = Service_Service.query.get_or_404(id) # Get request data - only service_cost data = request.get_json() if not data: return error_response("No data provided", 400) # Extract and validate the service_cost new_cost = data.get('service_cost') if new_cost is None: return error_response("service_cost is required", 400) # Convert to float for validation try: new_cost_float = float(new_cost) except (ValueError, TypeError): return error_response("service_cost must be a valid number", 400) # Update the service_cost service_record.service_cost = new_cost_float # Commit the transaction db.session.commit() # Return success response return success_response({ "service_id": id, "service_cost_updated": new_cost_float, "message": f"Service {id} cost updated to ${new_cost_float}" }) except Exception as e: db.session.rollback() logger.error(f"Error updating service cost for service {id}: {e}") return error_response(str(e), 500) @service.route("/update/", methods=["PUT"]) @login_required def update_service_call(id): service_record = Service_Service.query.get_or_404(id) data = request.get_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.session.commit() service_schema = Service_Service_schema(many=False) return success_response({"service": service_schema.dump(service_record)}) except Exception as e: db.session.rollback() return error_response(str(e), 500) # Service Plans CRUD endpoints @service.route("/plans/active", methods=["GET"]) @login_required def get_active_service_plans(): """ Get all active service plans (contract_plan > 0) """ try: plans = Service_Plans.query.filter(Service_Plans.contract_plan > 0).all() plans_schema = Service_Plans_schema(many=True) result = plans_schema.dump(plans) # Add customer info to each plan for plan in result: customer = Customer_Customer.query.get(plan['customer_id']) if customer: plan['customer_name'] = f"{customer.customer_first_name} {customer.customer_last_name}" plan['customer_address'] = customer.customer_address plan['customer_town'] = customer.customer_town return success_response({"plans": result}) except Exception as e: return error_response(str(e), 500) @service.route("/plans/customer/", methods=["GET"]) @login_required def get_customer_service_plan(customer_id): """ Get service plan for a specific customer """ try: plan = Service_Plans.query.filter_by(customer_id=customer_id).first() if plan: plan_schema = Service_Plans_schema() return success_response({"plan": plan_schema.dump(plan)}) else: return success_response({"plan": None}) except Exception as e: return error_response(str(e), 500) @service.route("/plans/create", methods=["POST"]) @login_required def create_service_plan(): """ Create a new service plan for a customer """ data = request.get_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']) ) db.session.add(new_plan) db.session.commit() plan_schema = Service_Plans_schema() return success_response({"plan": plan_schema.dump(new_plan)}, 201) except Exception as e: db.session.rollback() return error_response(str(e), 500) @service.route("/plans/update/", methods=["PUT"]) @login_required def update_service_plan(customer_id): """ Update existing service plan for a customer """ data = request.get_json() if not data: return error_response("No data provided", 400) try: plan = Service_Plans.query.filter_by(customer_id=customer_id).first() if not plan: # Create new plan if it doesn't exist plan = Service_Plans(customer_id=customer_id) db.session.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']) db.session.commit() plan_schema = Service_Plans_schema() return success_response({"plan": plan_schema.dump(plan)}) except Exception as e: db.session.rollback() return error_response(str(e), 500) @service.route("/plans/delete/", methods=["DELETE"]) @login_required def delete_service_plan(customer_id): """ Delete service plan for a customer """ try: plan = Service_Plans.query.filter_by(customer_id=customer_id).first() if not plan: return error_response("Service plan not found", 404) db.session.delete(plan) db.session.commit() return success_response({"message": "Service plan deleted successfully"}) except Exception as e: db.session.rollback() return error_response(str(e), 500) @service.route("/", methods=["GET"]) @login_required def get_service_by_id(id): service_record = Service_Service.query.get_or_404(id) service_schema = Service_Service_schema() return success_response({"service": service_schema.dump(service_record)}) @service.route("/delete/", methods=["DELETE"]) @login_required def delete_service_call(id): service_record = Service_Service.query.get_or_404(id) try: db.session.delete(service_record) db.session.commit() return success_response({"message": "Service deleted successfully"}) except Exception as e: db.session.rollback() return error_response(str(e), 500) @service.route("/parts/customer/", methods=["GET"]) @login_required def get_service_parts(customer_id): parts = Service_Parts.query.filter_by(customer_id=customer_id).first() if parts: parts_schema = Service_Parts_schema() return success_response({"parts": parts_schema.dump(parts)}) else: return success_response({"parts": { "customer_id": customer_id, "oil_filter": "", "oil_filter_2": "", "oil_nozzle": "", "oil_nozzle_2": "", "hot_water_tank": 0 }}) @service.route("/parts/update/", methods=["POST"]) @login_required def update_service_parts(customer_id): try: data = request.get_json() if not data: return error_response("No data provided", 400) get_customer = db.session.query(Customer_Customer).filter(Customer_Customer.id == customer_id).first() parts = Service_Parts.query.filter_by(customer_id=customer_id).first() if not parts: parts = Service_Parts(customer_id=customer_id) db.session.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.session.query(Auto_Delivery).filter(Auto_Delivery.customer_id == customer_id).first() if get_auto: get_auto.hot_water_summer = parts.hot_water_tank db.session.add(get_auto) db.session.commit() return success_response({"message": "Service parts updated successfully"}) except Exception as e: db.session.rollback() return error_response(str(e), 500) @service.route("/payment//", methods=["PUT"]) @login_required def process_service_payment(service_id, payment_type): service = db.session.query(Service_Service).filter(Service_Service.id == service_id).first() if not service: return error_response("Service not found", 404) # Set payment columns as specified service.payment_type = payment_type # e.g., 1 for Tiger service.payment_status = 2 # As specified # payment_card_id retains the selected card's ID if set in the service record try: db.session.commit() return success_response() except Exception as e: db.session.rollback() return error_response(str(e), 500)