"""Dashboard defaults API: visitor-IP -> business-unit mapping. CRUD for the mappings plus a resolve endpoint a kiosk/lobby dashboard calls to auto-select its business unit from the display PC's IP. """ from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import DashboardDefault, BusinessUnit, AuditLog from shopdb.utils.responses import success_response, error_response, ErrorCodes dashboarddefaults_bp = Blueprint('dashboarddefaults', __name__) def _request_ip(): """Caller IP, honoring a single proxy hop via X-Forwarded-For.""" forwarded = request.headers.get('X-Forwarded-For') if forwarded: return forwarded.split(',')[0].strip() return request.remote_addr def _serialize(default): data = default.to_dict() data['businessunit'] = default.businessunit.businessunit \ if default.businessunit else None return data @dashboarddefaults_bp.route('/visitor-location', methods=['GET']) def visitor_location(): """Resolve the business unit for the calling display by its IP. Unauthenticated: kiosks/lobby displays hit this. Returns the mapped business unit, or a null businessunitid when the IP is not mapped. """ ipaddress = request.args.get('ipaddress') or _request_ip() default = DashboardDefault.query.filter_by( ipaddress=ipaddress, isactive=True).first() if not default: return success_response({ 'ipaddress': ipaddress, 'businessunitid': None, 'businessunit': None, }) return success_response({ 'ipaddress': ipaddress, 'businessunitid': default.businessunitid, 'businessunit': default.businessunit.businessunit if default.businessunit else None, }) @dashboarddefaults_bp.route('', methods=['GET']) @jwt_required(optional=True) def list_defaults(): """List all visitor-IP -> business-unit mappings.""" defaults = DashboardDefault.query.filter_by(isactive=True).order_by( DashboardDefault.ipaddress).all() return success_response([_serialize(d) for d in defaults]) @dashboarddefaults_bp.route('', methods=['POST']) @jwt_required() def create_default(): """Create a visitor-IP -> business-unit mapping.""" data = request.get_json() or {} ipaddress = (data.get('ipaddress') or '').strip() businessunitid = data.get('businessunitid') if not ipaddress: return error_response(ErrorCodes.VALIDATION_ERROR, 'ipaddress is required') if not businessunitid: return error_response(ErrorCodes.VALIDATION_ERROR, 'businessunitid is required') if not BusinessUnit.query.get(businessunitid): return error_response(ErrorCodes.NOT_FOUND, 'Business unit not found', http_code=404) if DashboardDefault.query.filter_by(ipaddress=ipaddress, isactive=True).first(): return error_response(ErrorCodes.CONFLICT, f"IP {ipaddress} is already mapped", http_code=409) default = DashboardDefault(ipaddress=ipaddress, businessunitid=businessunitid, description=data.get('description')) db.session.add(default) db.session.flush() AuditLog.log('created', 'DashboardDefault', entityid=default.dashboarddefaultid, entityname=ipaddress) db.session.commit() return success_response(_serialize(default), message='Mapping created', http_code=201) @dashboarddefaults_bp.route('/', methods=['PUT']) @jwt_required() def update_default(default_id): """Update a mapping.""" default = DashboardDefault.query.get(default_id) if not default: return error_response(ErrorCodes.NOT_FOUND, 'Mapping not found', http_code=404) data = request.get_json() or {} if 'businessunitid' in data: if not BusinessUnit.query.get(data['businessunitid']): return error_response(ErrorCodes.NOT_FOUND, 'Business unit not found', http_code=404) default.businessunitid = data['businessunitid'] if 'ipaddress' in data and data['ipaddress']: default.ipaddress = data['ipaddress'].strip() if 'description' in data: default.description = data['description'] db.session.commit() return success_response(_serialize(default), message='Mapping updated') @dashboarddefaults_bp.route('/', methods=['DELETE']) @jwt_required() def delete_default(default_id): """Delete (deactivate) a mapping.""" default = DashboardDefault.query.get(default_id) if not default: return error_response(ErrorCodes.NOT_FOUND, 'Mapping not found', http_code=404) default.isactive = False db.session.commit() return success_response(message='Mapping deleted')