from flask import request, jsonify from flask_login import login_required, current_user from datetime import datetime from . import marketplace_bp from ...extensions import db from ...models import Job, Application, User, Category from ...models.enums import JobStatus, ApplicationStatus @marketplace_bp.route('/jobs', methods=['GET']) def get_jobs(): """Get all available jobs with filtering""" try: # Get query parameters category_id = request.args.get('category_id', type=int) location = request.args.get('location') min_budget = request.args.get('min_budget', type=float) max_budget = request.args.get('max_budget', type=float) page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) # Build query query = Job.query.filter_by(status=JobStatus.OPEN) if category_id: query = query.filter_by(category_id=category_id) if location: query = query.filter(Job.location.ilike(f'%{location}%')) if min_budget: query = query.filter(Job.budget_min >= min_budget) if max_budget: query = query.filter(Job.budget_max <= max_budget) # Order by date posted (newest first) query = query.order_by(Job.date_posted.desc()) # Paginate jobs = query.paginate(page=page, per_page=per_page, error_out=False) return jsonify({ 'jobs': [{ 'id': job.id, 'title': job.title, 'description': job.description, 'location': job.location, 'budget_min': float(job.budget_min) if job.budget_min else None, 'budget_max': float(job.budget_max) if job.budget_max else None, 'hourly_rate': float(job.hourly_rate) if job.hourly_rate else None, 'duration': job.duration, 'workers_needed': job.workers_needed, 'start_datetime': job.start_datetime.isoformat() if job.start_datetime else None, 'own_tools_required': job.own_tools_required, 'own_transport_required': job.own_transport_required, 'payment_terms': job.payment_terms, 'category': job.category.name, 'contractor_name': f"{job.contractor.first_name} {job.contractor.last_name}", 'whs_requirements': job.whs_requirements, 'insurance_required': job.insurance_required, 'white_card_required': job.white_card_required, 'date_posted': job.date_posted.isoformat(), 'deadline': job.deadline.isoformat() if job.deadline else None, 'applications_count': job.applications_count, 'user_has_applied': ( Application.query.filter_by( job_id=job.id, worker_id=current_user.id ).first() is not None ) if current_user.is_authenticated and current_user.role == 'worker' else False } for job in jobs.items], 'pagination': { 'page': jobs.page, 'per_page': jobs.per_page, 'total': jobs.total, 'pages': jobs.pages, 'has_next': jobs.has_next, 'has_prev': jobs.has_prev } }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/jobs/', methods=['GET']) def get_job(job_id): """Get single job details""" try: job = Job.query.get_or_404(job_id) return jsonify({ 'job': { 'id': job.id, 'title': job.title, 'description': job.description, 'location': job.location, 'budget_min': float(job.budget_min) if job.budget_min else None, 'budget_max': float(job.budget_max) if job.budget_max else None, 'category': { 'id': job.category.id, 'name': job.category.name, 'whs_risk_level': job.category.whs_risk_level }, 'contractor': { 'id': job.contractor.id, 'name': f"{job.contractor.first_name} {job.contractor.last_name}", 'business_name': job.contractor.business_name, 'average_rating': float(job.contractor.average_rating) if job.contractor.average_rating else 0.0, 'jobs_completed': job.contractor.jobs_completed }, 'whs_requirements': job.whs_requirements, 'insurance_required': job.insurance_required, 'white_card_required': job.white_card_required, 'date_posted': job.date_posted.isoformat(), 'deadline': job.deadline.isoformat() if job.deadline else None, 'applications_count': job.applications_count, 'status': job.status } }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/jobs//apply', methods=['POST']) @login_required def apply_to_job(job_id): """Apply to a job (workers only)""" try: user = current_user if not user.is_authenticated: return jsonify({'error': 'Please login to apply'}), 401 if user.role != 'worker': return jsonify({'error': 'Only workers can apply to jobs'}), 403 job = Job.query.get_or_404(job_id) if job.status != JobStatus.OPEN: return jsonify({'error': 'Job is not open for applications'}), 400 # Check if user already applied existing_application = Application.query.filter_by( job_id=job_id, worker_id=user.id ).first() if existing_application: return jsonify({'error': 'You have already applied to this job'}), 400 data = request.get_json() # Create application application = Application( job_id=job_id, worker_id=user.id, proposed_rate=data.get('proposed_rate'), cover_letter=data.get('cover_letter'), abn_verified=True, # We assume ABN is verified during registration insurance_verified=user.public_liability_insurance ) db.session.add(application) # Update job applications count job.applications_count += 1 db.session.commit() return jsonify({ 'message': 'Application submitted successfully', 'application_id': application.id }), 201 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/categories', methods=['GET']) def get_categories(): """Get all active job categories""" try: categories = Category.query.filter_by(is_active=True).order_by(Category.sort_order, Category.name).all() return jsonify({ 'categories': [{ 'id': category.id, 'name': category.name, 'description': category.description, 'whs_risk_level': category.whs_risk_level, 'insurance_minimum': category.get_insurance_minimum(), 'license_required': category.license_required, 'white_card_required': category.white_card_required } for category in categories] }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/my-applications', methods=['GET']) @login_required def get_my_applications(): """Get current user's job applications""" try: user = current_user if not user or user.role != 'worker': return jsonify({'error': 'Only workers can view applications'}), 403 applications = Application.query.filter_by(worker_id=user.id).order_by(Application.date_applied.desc()).all() return jsonify({ 'applications': [{ 'id': app.id, 'job': { 'id': app.job.id, 'title': app.job.title, 'location': app.job.location, 'contractor_name': f"{app.job.contractor.first_name} {app.job.contractor.last_name}" }, 'status': app.status, 'proposed_rate': float(app.proposed_rate) if app.proposed_rate else None, 'date_applied': app.date_applied.isoformat(), 'abn_verified': app.abn_verified, 'insurance_verified': app.insurance_verified } for app in applications] }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/jobs', methods=['POST']) @login_required def create_job(): """Create a new job (contractors only)""" try: user = current_user if not user: return jsonify({'error': 'User not found'}), 404 if user.role != 'contractor': return jsonify({'error': 'Only contractors can post jobs'}), 403 # Check contractor compliance compliance_valid, compliance_issues = user.is_compliance_valid() if not compliance_valid: return jsonify({ 'error': 'Contractor compliance required to post jobs', 'issues': compliance_issues }), 400 data = request.get_json() # Validate required fields required_fields = ['title', 'description', 'category_id', 'location'] for field in required_fields: if not data.get(field): return jsonify({'error': f'{field} is required'}), 400 # Validate category exists category = Category.query.get(data['category_id']) if not category or not category.is_active: return jsonify({'error': 'Invalid or inactive category'}), 400 # Create job job = Job( title=data['title'], description=data['description'], contractor_id=user_id, category_id=data['category_id'], location=data['location'], budget_min=data.get('budget_min'), budget_max=data.get('budget_max'), whs_requirements=data.get('whs_requirements') or f"Category: {category.name}. Risk Level: {category.whs_risk_level}. {category.insurance_requirements or ''}", insurance_required=data.get('insurance_required', True), white_card_required=data.get('white_card_required', category.white_card_required), deadline=datetime.fromisoformat(data['deadline']) if data.get('deadline') else None ) db.session.add(job) db.session.commit() # Award points for posting job from ...utils.gamification import award_points award_points(user.id, 'job_posted', 50) return jsonify({ 'message': 'Job posted successfully', 'job_id': job.id, 'job': { 'id': job.id, 'title': job.title, 'location': job.location, 'status': job.status, 'date_posted': job.date_posted.isoformat() } }), 201 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/jobs/', methods=['PUT']) @login_required def update_job(job_id): """Update job details (job owner only)""" try: user = current_user job = Job.query.get_or_404(job_id) # Check ownership if job.contractor_id != user.id: return jsonify({'error': 'You can only update your own jobs'}), 403 # Don't allow updates if job has applications if job.applications_count > 0: return jsonify({'error': 'Cannot update job with existing applications'}), 400 data = request.get_json() # Update allowed fields updatable_fields = ['title', 'description', 'location', 'budget_min', 'budget_max', 'whs_requirements', 'deadline'] for field in updatable_fields: if field in data: if field == 'deadline' and data[field]: setattr(job, field, datetime.fromisoformat(data[field])) else: setattr(job, field, data[field]) db.session.commit() return jsonify({ 'message': 'Job updated successfully', 'job': { 'id': job.id, 'title': job.title, 'description': job.description, 'location': job.location, 'budget_min': float(job.budget_min) if job.budget_min else None, 'budget_max': float(job.budget_max) if job.budget_max else None, 'status': job.status } }), 200 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/my-jobs', methods=['GET']) @login_required def get_my_jobs(): """Get contractor's posted jobs""" try: user = current_user if not user or user.role != 'contractor': return jsonify({'error': 'Only contractors can view posted jobs'}), 403 status = request.args.get('status', 'all') page = request.args.get('page', 1, type=int) per_page = min(request.args.get('per_page', 10, type=int), 100) # Build query query = Job.query.filter_by(contractor_id=user.id) if status != 'all': query = query.filter_by(status=status) query = query.order_by(Job.date_posted.desc()) # Paginate jobs = query.paginate(page=page, per_page=per_page, error_out=False) return jsonify({ 'jobs': [{ 'id': job.id, 'title': job.title, 'location': job.location, 'status': job.status, 'budget_min': float(job.budget_min) if job.budget_min else None, 'budget_max': float(job.budget_max) if job.budget_max else None, 'applications_count': job.applications_count, 'date_posted': job.date_posted.isoformat(), 'deadline': job.deadline.isoformat() if job.deadline else None, 'category': job.category.name } for job in jobs.items], 'pagination': { 'page': jobs.page, 'per_page': jobs.per_page, 'total': jobs.total, 'pages': jobs.pages } }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/jobs//applications', methods=['GET']) @login_required def get_job_applications(job_id): """Get applications for a specific job (job owner only)""" try: user = current_user job = Job.query.get_or_404(job_id) # Check ownership if job.contractor_id != user.id: return jsonify({'error': 'You can only view applications for your own jobs'}), 403 applications = Application.query.filter_by(job_id=job_id).order_by(Application.date_applied.desc()).all() return jsonify({ 'job': { 'id': job.id, 'title': job.title, 'applications_count': job.applications_count }, 'applications': [{ 'id': app.id, 'worker': { 'id': app.worker.id, 'name': f"{app.worker.first_name} {app.worker.last_name}", 'primary_trade': app.worker.primary_trade, 'average_rating': float(app.worker.average_rating) if app.worker.average_rating else 0.0, 'jobs_completed': app.worker.jobs_completed, 'location': app.worker.location }, 'status': app.status, 'proposed_rate': float(app.proposed_rate) if app.proposed_rate else None, 'cover_letter': app.cover_letter, 'date_applied': app.date_applied.isoformat(), 'abn_verified': app.abn_verified, 'insurance_verified': app.insurance_verified, 'compliance_valid': app.worker.is_compliance_valid()[0] } for app in applications] }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/applications/', methods=['PUT']) @login_required def update_application_status(application_id): """Accept or reject job application (job owner only)""" try: user = current_user application = Application.query.get_or_404(application_id) # Check if user owns the job if application.job.contractor_id != user.id: return jsonify({'error': 'You can only manage applications for your own jobs'}), 403 data = request.get_json() new_status = data.get('status') if new_status not in [ApplicationStatus.ACCEPTED, ApplicationStatus.REJECTED]: return jsonify({'error': 'Status must be "accepted" or "rejected"'}), 400 if application.status != ApplicationStatus.PENDING: return jsonify({'error': 'Can only update pending applications'}), 400 application.status = new_status # If accepted, close the job and reject other applications if new_status == ApplicationStatus.ACCEPTED: application.job.status = JobStatus.ASSIGNED # Reject all other pending applications for this job other_applications = Application.query.filter_by( job_id=application.job_id, status=ApplicationStatus.PENDING ).filter(Application.id != application_id).all() for other_app in other_applications: other_app.status = ApplicationStatus.REJECTED # Award points to both contractor and worker from ...utils.gamification import award_points award_points(application.job.contractor_id, 'application_accepted', 25) award_points(application.worker_id, 'application_accepted', 75) db.session.commit() return jsonify({ 'message': f'Application {new_status} successfully', 'application': { 'id': application.id, 'status': application.status, 'worker_name': f"{application.worker.first_name} {application.worker.last_name}", 'job_title': application.job.title } }), 200 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @marketplace_bp.route('/jobs//close', methods=['PUT']) @login_required def close_job(job_id): """Close a job posting (job owner only)""" try: user = current_user job = Job.query.get_or_404(job_id) # Check ownership if job.contractor_id != user.id: return jsonify({'error': 'You can only close your own jobs'}), 403 if job.status not in [JobStatus.OPEN, JobStatus.ASSIGNED]: return jsonify({'error': 'Job is already closed'}), 400 job.status = JobStatus.CANCELLED # Reject all pending applications pending_applications = Application.query.filter_by( job_id=job_id, status=ApplicationStatus.PENDING ).all() for app in pending_applications: app.status = ApplicationStatus.REJECTED db.session.commit() return jsonify({ 'message': 'Job closed successfully', 'job': { 'id': job.id, 'title': job.title, 'status': job.status } }), 200 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500