from flask import render_template, redirect, request, session, jsonify, flash, url_for from flask_login import login_required, current_user, logout_user, login_user from datetime import datetime from app.models import Category def register_main_routes(app): """Register main web application routes""" @app.before_request def debug_authentication_state(): """Debug any automatic authentication that might be happening""" from flask_login import current_user from flask import request, session # Only log for web routes (not API routes) if not request.path.startswith('/api/'): print(f"🔍 Route: {request.path}") print(f"🔍 current_user.is_authenticated: {current_user.is_authenticated}") print(f"🔍 session keys: {list(session.keys())}") if current_user.is_authenticated: print(f"⚠️ User automatically authenticated: {current_user.email}") @app.route('/') def index(): # Serve the complete marketing page directly with open('app/templates/index.html', 'r') as f: return f.read() @app.route('/test') def test(): """Simple test route""" return "

✅ RateRight Test Route Working!

If you see this, the Flask server is responding correctly.

" @app.route('/applications') @login_required def my_applications(): """View user's job applications or received applications""" from app.models import Application from app.extensions import db if current_user.role == 'worker': applications = Application.query.filter_by(worker_id=current_user.id).order_by(Application.id.desc()).all() return render_template('applications/list.html', applications=applications) elif current_user.role == 'contractor': # Get all applications for contractor's jobs from app.models import Job applications = db.session.query(Application).join(Job).filter( Job.contractor_id == current_user.id ).order_by(Application.id.desc()).all() return render_template('applications/contractor_list.html', applications=applications) else: flash('Access denied.', 'error') return redirect(url_for('dashboard')) @app.route('/jobs') def browse_jobs(): """Browse available construction jobs""" if current_user.is_authenticated and current_user.role == 'contractor': flash('As a contractor, you can post jobs and review applications. Workers browse and apply for jobs.', 'info') return render_template('jobs/browse.html') @app.route('/jobs/post', methods=['GET', 'POST']) def post_job(): """Post a new construction job (requires login)""" from flask_login import current_user from app.models import Job, Category from app.extensions import db from decimal import Decimal if not current_user.is_authenticated: flash('Please login to post a job', 'warning') return redirect(url_for('login')) if current_user.role != 'contractor': flash('Only contractors can post jobs', 'error') return redirect(url_for('dashboard')) if request.method == 'POST': try: # Get form data title = request.form.get('title', '').strip() description = request.form.get('description', '').strip() category_id = request.form.get('category_id', type=int) location = request.form.get('location', '').strip() budget_min = request.form.get('budget_min') budget_max = request.form.get('budget_max') hourly_rate = request.form.get('hourly_rate') whs_requirements = request.form.get('whs_requirements', '').strip() white_card_required = 'white_card_required' in request.form insurance_required = 'insurance_required' in request.form # Validation errors = [] if not title: errors.append('Job title is required') if not description: errors.append('Job description is required') if not category_id: errors.append('Job category is required') if not location: errors.append('Job location is required') # Validate category exists if category_id: category = Category.query.get(category_id) if not category or not category.is_active: errors.append('Invalid job category selected') # Convert budget/rate to Decimal if provided budget_min_decimal = None budget_max_decimal = None hourly_rate_decimal = None try: if budget_min: budget_min_decimal = Decimal(str(budget_min)) if budget_min_decimal < 0: errors.append('Minimum budget cannot be negative') except (ValueError, TypeError): errors.append('Invalid minimum budget amount') try: if budget_max: budget_max_decimal = Decimal(str(budget_max)) if budget_max_decimal < 0: errors.append('Maximum budget cannot be negative') except (ValueError, TypeError): errors.append('Invalid maximum budget amount') try: if hourly_rate: hourly_rate_decimal = Decimal(str(hourly_rate)) if hourly_rate_decimal < 0: errors.append('Hourly rate cannot be negative') except (ValueError, TypeError): errors.append('Invalid hourly rate amount') # Validate budget range if budget_min_decimal and budget_max_decimal and budget_min_decimal > budget_max_decimal: errors.append('Minimum budget cannot be greater than maximum budget') if errors: for error in errors: flash(error, 'error') categories = Category.query.filter_by(is_active=True).order_by(Category.sort_order, Category.name).all() return render_template('jobs/post.html', categories=categories) # Create new job job = Job( title=title, description=description, contractor_id=current_user.id, category_id=category_id, location=location, budget_min=budget_min_decimal, budget_max=budget_max_decimal, hourly_rate=hourly_rate_decimal, whs_requirements=whs_requirements or f"Category: {category.name}. Risk Level: {category.whs_risk_level}.", white_card_required=white_card_required, insurance_required=insurance_required, status='open' ) db.session.add(job) db.session.commit() flash('Job posted successfully!', 'success') return redirect(url_for('job_details', job_id=job.id)) except Exception as e: db.session.rollback() flash(f'Error posting job: {str(e)}', 'error') categories = Category.query.filter_by(is_active=True).order_by(Category.sort_order, Category.name).all() return render_template('jobs/post.html', categories=categories) # GET request - show the form categories = Category.query.filter_by(is_active=True).order_by(Category.sort_order, Category.name).all() return render_template('jobs/post.html', categories=categories) @app.route('/jobs/') def job_details(job_id): """View specific job details""" from app.models import Job, Application job = Job.query.get_or_404(job_id) # Retrieve job from database, or return 404 # Check if current user has already applied to this job user_applied = False if current_user.is_authenticated and current_user.role == 'worker': existing_app = Application.query.filter_by( job_id=job_id, worker_id=current_user.id ).first() user_applied = existing_app is not None return render_template('jobs/details.html', job=job, user_applied=user_applied) @app.route('/jobs//apply', methods=['POST']) @login_required def apply_to_job(job_id): """Apply to a job""" if request.method == 'POST': from app.models import Application from app.extensions import db from app.models import Job # Import Job model if current_user.role != 'worker': flash('Only workers can apply to jobs.', 'error') return redirect(url_for('job_details', job_id=job_id)) job = Job.query.get_or_404(job_id) if job.status != 'open': flash('This job is no longer accepting applications.', 'error') return redirect(url_for('job_details', job_id=job_id)) # Check if already applied existing_app = Application.query.filter_by( job_id=job_id, worker_id=current_user.id ).first() if existing_app: flash('You have already applied to this job.', 'warning') return redirect(url_for('job_details', job_id=job_id)) # Get form data proposed_rate = request.form.get('proposed_rate') cover_letter = request.form.get('cover_letter', '').strip() # Validate proposed rate proposed_rate_decimal = None if proposed_rate: try: proposed_rate_decimal = float(proposed_rate) if proposed_rate_decimal < 0: flash('Proposed rate cannot be negative.', 'error') return redirect(url_for('job_details', job_id=job_id)) except (ValueError, TypeError): flash('Invalid proposed rate.', 'error') return redirect(url_for('job_details', job_id=job_id)) # Create application application = Application( job_id=job_id, worker_id=current_user.id, proposed_rate=proposed_rate_decimal, cover_letter=cover_letter, abn_verified=True, # Auto-verify for now insurance_verified=current_user.public_liability_insurance, status='pending' ) try: db.session.add(application) # Update job applications count job.applications_count = (job.applications_count or 0) + 1 db.session.commit() flash('Application submitted successfully!', 'success') return redirect(url_for('dashboard')) except Exception as e: db.session.rollback() flash(f'Error submitting application: {str(e)}', 'error') print(f"Database error: {e}") # For debugging return redirect(url_for('job_details', job_id=job_id)) @app.route('/login', methods=['GET', 'POST']) def login(): """User login page and web form handler""" from app.models import User if current_user.is_authenticated: return redirect(url_for('dashboard')) if request.method == 'POST': # Handle web form login (creates Flask-Login session) email = request.form.get('email') password = request.form.get('password') if not email or not password: flash('Email and password are required', 'error') return render_template('auth/login.html') user = User.query.filter_by(email=email).first() if user and user.check_password(password) and user.is_active: # Create Flask-Login session login_user(user, remember=request.form.get('remember')) flash('Login successful!', 'success') return redirect(url_for('dashboard')) else: flash('Invalid email or password', 'error') return render_template('auth/login.html') @app.route('/register') def register(): """User registration page""" if 'user_id' in session: return redirect(url_for('dashboard')) return render_template('auth/register.html') @app.route('/logout') def logout(): """Logout user and completely clear all sessions""" from flask import make_response logout_user() # Clear Flask-Login session session.clear() # Clear Flask session # Create response that clears everything response = make_response(''' Logging out...

Logging out...

''') # Clear all cookies by setting them to expire response.set_cookie('session', '', expires=0) response.set_cookie('remember_token', '', expires=0) return response @app.route('/dashboard') def dashboard(): """User dashboard - redirect based on role""" # Check if user is logged in via Flask-Login if not current_user.is_authenticated: return redirect(url_for('login')) # Redirect based on user role return redirect(url_for(f'dashboard_{current_user.role}')) @app.route('/dashboard/contractor') def dashboard_contractor(): """Contractor dashboard""" from app.models import Job, Application, Contract from app.extensions import db if not current_user.is_authenticated or current_user.role != 'contractor': return redirect(url_for('login')) # Fetch contractor's posted jobs posted_jobs = Job.query.filter_by(contractor_id=current_user.id).order_by(Job.date_posted.desc()).all() # Get all applications for contractor's jobs recent_applications = db.session.query(Application).join(Job).filter( Job.contractor_id == current_user.id ).order_by(Application.date_applied.desc()).all() # Get contractor's contracts (pending, signed, and active) active_contracts = Contract.query.filter( Contract.contractor_id == current_user.id, Contract.status.in_(['pending_agreement', 'contractor_signed', 'worker_signed', 'active']) ).order_by(Contract.created_at.desc()).all() total_applications = len(recent_applications) return render_template('dashboard/contractor.html', posted_jobs=posted_jobs, total_applications=total_applications, recent_applications=recent_applications, active_contracts=active_contracts) @app.route('/dashboard/worker') def dashboard_worker(): """Worker dashboard""" from app.models import Application, Contract if not current_user.is_authenticated or current_user.role != 'worker': return redirect(url_for('login')) # Get worker's applications applications = Application.query.filter_by(worker_id=current_user.id).order_by(Application.date_applied.desc()).all() # Get worker's contracts (pending agreement, signed, and active) active_contracts = Contract.query.filter( Contract.worker_id == current_user.id, Contract.status.in_(['pending_agreement', 'contractor_signed', 'worker_signed', 'active']) ).order_by(Contract.created_at.desc()).all() return render_template('dashboard/worker.html', applications=applications, active_contracts=active_contracts) @app.route('/profile') def profile(): """User profile page""" if not current_user.is_authenticated: flash('Please login to access your profile', 'warning') return redirect(url_for('login')) return render_template('auth/profile.html') @app.route('/health') def app_health(): """Simple health check for the web application""" return { "status": "healthy", "message": "RateRight web application is running", "timestamp": datetime.utcnow().isoformat() } # API info is handled in __init__.py @app.route('/applications//respond', methods=['GET', 'POST']) @login_required def respond_to_application(application_id): """Accept or reject job application (contractor only)""" from app.extensions import db from app.models import Application # Import Application model from app.models import Job # Import Job model if current_user.role != 'contractor': flash('Only contractors can respond to applications.', 'error') return redirect(url_for('dashboard')) application = Application.query.get_or_404(application_id) # Check if user owns the job if application.job.contractor_id != current_user.id: flash('You can only respond to applications for your own jobs.', 'error') return redirect(url_for('dashboard')) if request.method == 'GET': # Show application details for review return render_template('applications/respond.html', application=application) action = request.form.get('action') if action == 'accept': application.status = 'accepted' # Close the job and reject other applications application.job.status = 'assigned' # Reject all other pending applications for this job other_applications = Application.query.filter_by( job_id=application.job_id, status='pending' ).filter(Application.id != application_id).all() for other_app in other_applications: other_app.status = 'rejected' # Create Contract record from app.models import Contract from datetime import date, timedelta # Calculate end date (estimate 30 days from start) start_date = date.today() end_date = start_date + timedelta(days=30) # Debug rate transfer proposed_rate = application.proposed_rate fallback_rate = application.job.budget_max or application.job.hourly_rate or 0 final_rate = proposed_rate if proposed_rate is not None else fallback_rate print(f"💰 Contract Rate Debug:") print(f" Application proposed_rate: {proposed_rate}") print(f" Job budget_max: {application.job.budget_max}") print(f" Job hourly_rate: {application.job.hourly_rate}") print(f" Final agreed_rate: {final_rate}") # Determine if this is hourly or total rate based on job data rate_type = 'hourly' if application.job.hourly_rate else 'total' # Create contract with proper workflow status contract = Contract( job_id=application.job_id, contractor_id=application.job.contractor_id, worker_id=application.worker_id, agreed_rate=final_rate, rate_type=rate_type, # Set correct rate type start_date=start_date, end_date=end_date, scope_of_work=application.job.description or "Contract work as per job posting", status='pending_agreement' # Correct! Requires both parties to agree ) # Add and commit contract with proper error handling db.session.add(contract) try: db.session.commit() print(f"✅ Contract created successfully: ID {contract.id}") # Verify what was actually saved to database saved_contract = Contract.query.get(contract.id) print(f"🔍 VERIFICATION - Saved contract agreed_rate: {saved_contract.agreed_rate}") print(f"🔍 VERIFICATION - Saved contract type: {type(saved_contract.agreed_rate)}") # Verify contract was created with valid ID if contract.id: flash(f'Application accepted! Contract #{contract.id} created and ready for review.', 'success') return redirect(url_for('contracts_review', contract_id=contract.id)) else: flash('Contract created but ID assignment failed. Check your dashboard.', 'warning') return redirect(url_for('dashboard_contractor')) except Exception as e: db.session.rollback() flash(f'Error creating contract: {str(e)}', 'error') print(f"Contract creation error: {e}") # For debugging return redirect(url_for('dashboard_contractor')) elif action == 'reject': application.status = 'rejected' flash(f'Application from {application.worker.first_name} {application.worker.last_name} rejected.', 'info') try: db.session.commit() except Exception as e: db.session.rollback() flash('Error updating application. Please try again.', 'error') return redirect(url_for('dashboard_contractor')) @app.route('/contracts//review', methods=['GET', 'POST']) @login_required def contracts_review(contract_id): """Review contract details and sign""" from app.models import Contract from app.extensions import db contract = Contract.query.get_or_404(contract_id) # Check if user is party to this contract if current_user.id not in [contract.contractor_id, contract.worker_id]: flash('You do not have access to this contract.', 'error') return redirect(url_for('dashboard')) if request.method == 'POST': action = request.form.get('action') if action == 'contractor_review' and current_user.id == contract.contractor_id: contract.contractor_reviewed = True flash('Contract marked as reviewed by contractor.', 'success') elif action == 'worker_review' and current_user.id == contract.worker_id: contract.worker_reviewed = True flash('Contract marked as reviewed by worker.', 'success') else: flash('Invalid action or insufficient permissions.', 'error') return redirect(url_for('contracts_review', contract_id=contract_id)) db.session.commit() return redirect(url_for('contracts_review', contract_id=contract_id)) return render_template('contracts/review.html', contract=contract) @app.route('/contracts//sign', methods=['GET', 'POST']) @login_required def contracts_sign(contract_id): """Sign a contract - GET shows form, POST processes signing""" from app.models import Contract from app.extensions import db contract = Contract.query.get_or_404(contract_id) if not contract.can_sign(current_user.id): flash('You cannot sign this contract at this time.', 'error') return redirect(url_for('contracts_review', contract_id=contract_id)) # Auto-mark as reviewed when signing if current_user.id == contract.contractor_id: contract.contractor_reviewed = True elif current_user.id == contract.worker_id: contract.worker_reviewed = True if request.method == 'POST': success = contract.sign_contract(current_user.id) if success: db.session.commit() if contract.status == 'active': flash('Contract fully executed! Work can now begin.', 'success') # TODO: Send notifications to both parties else: flash('Contract signed successfully. Waiting for other party.', 'success') else: flash('Failed to sign contract.', 'error') return redirect(url_for('contracts_review', contract_id=contract_id)) # GET request - show signing confirmation page return render_template('contracts/review.html', contract=contract, show_sign_form=True) @app.route('/contracts//mark-complete', methods=['POST']) @login_required def mark_contract_complete(contract_id): """Worker marks contract work as complete""" from app.models import Contract from app.extensions import db contract = Contract.query.get_or_404(contract_id) # Only worker can mark as complete if contract.worker_id != current_user.id: flash('Only the worker can mark work as complete.', 'error') return redirect(url_for('contract_closeout', contract_id=contract_id)) if contract.status != 'active': flash('Contract must be active to mark complete.', 'error') return redirect(url_for('contract_closeout', contract_id=contract_id)) # Update contract status contract.status = 'pending_review' # Find associated payment if exists payment = contract.payments.filter_by(status='held_escrow').first() if payment: payment.status = 'pending_release' payment.release_conditions_met = True db.session.commit() flash('Work marked as complete - awaiting contractor review.', 'success') return redirect(url_for('contract_closeout', contract_id=contract_id)) @app.route('/contracts//approve-completion', methods=['POST']) @login_required def approve_contract_completion(contract_id): """Contractor approves work completion""" from app.models import Contract from app.extensions import db from datetime import datetime contract = Contract.query.get_or_404(contract_id) # Only contractor can approve completion if contract.contractor_id != current_user.id: flash('Only the contractor can approve completion.', 'error') return redirect(url_for('contract_closeout', contract_id=contract_id)) if contract.status != 'pending_review': flash('Contract is not pending completion review.', 'error') return redirect(url_for('contract_closeout', contract_id=contract_id)) # Find payment in escrow payment = contract.payments.filter_by(status='pending_release').first() if payment: payment.status = 'released' payment.date_released = datetime.utcnow() # Update contract status contract.status = 'completed' db.session.commit() flash('Work approved and payment released successfully!', 'success') return redirect(url_for('contract_closeout', contract_id=contract_id)) @app.route('/contracts//dispute-completion', methods=['POST']) @login_required def dispute_contract_completion(contract_id): """Contractor disputes work completion""" from app.models import Contract from app.extensions import db contract = Contract.query.get_or_404(contract_id) # Only contractor can dispute if contract.contractor_id != current_user.id: flash('Only the contractor can dispute completion.', 'error') return redirect(url_for('contract_closeout', contract_id=contract_id)) if contract.status != 'pending_review': flash('Contract is not pending completion review.', 'error') return redirect(url_for('contract_closeout', contract_id=contract_id)) dispute_reason = request.form.get('reason', 'Work does not meet requirements') # Update contract status contract.status = 'disputed' # Find payment and mark as disputed payment = contract.payments.filter_by(status='pending_release').first() if payment: payment.status = 'disputed' db.session.commit() flash(f'Completion disputed: {dispute_reason}', 'warning') return redirect(url_for('contract_closeout', contract_id=contract_id)) @app.route('/contracts', methods=['GET']) @login_required def contracts_list(): """List all contracts for current user""" from app.models import Contract if current_user.role == 'contractor': contracts = Contract.query.filter_by(contractor_id=current_user.id).order_by(Contract.created_at.desc()).all() else: contracts = Contract.query.filter_by(worker_id=current_user.id).order_by(Contract.created_at.desc()).all() return render_template('contracts/list.html', contracts=contracts) @app.route('/applications//accept', methods=['POST']) @login_required def accept_application(application_id): """Accept a job application and create contract""" from app.models import Application, Contract from datetime import date, timedelta if not current_user.is_authenticated or current_user.role != 'contractor': flash('Only contractors can accept applications', 'error') return redirect(url_for('login')) application = Application.query.get_or_404(application_id) # Check if contractor owns this job if application.job.contractor_id != current_user.id: flash('You can only accept applications for your own jobs', 'error') return redirect(url_for('applications_contractor_list')) if application.status != 'pending': flash('Application has already been processed', 'warning') return redirect(url_for('applications_contractor_list')) try: # Accept the application application.status = 'accepted' # Close the job application.job.status = 'assigned' # Create contract automatically start_date = date.today() + timedelta(days=7) # Start in 1 week end_date = start_date + timedelta(days=30) # 30 day contract contract = Contract( job_id=application.job_id, contractor_id=current_user.id, worker_id=application.worker_id, agreed_rate=application.proposed_rate if application.proposed_rate is not None else (application.job.budget_max or application.job.hourly_rate or 5000.00), rate_type='total', start_date=start_date, end_date=end_date, scope_of_work=application.job.description, payment_terms='completion', status='pending_agreement' ) db.session.add(contract) # Reject other pending applications for this job other_applications = Application.query.filter_by( job_id=application.job_id, status='pending' ).filter(Application.id != application_id).all() for other_app in other_applications: other_app.status = 'rejected' db.session.commit() flash(f'Application accepted! Contract #{contract.id} created and ready for review.', 'success') except Exception as e: db.session.rollback() flash(f'Error accepting application: {str(e)}', 'error') return redirect(url_for('applications_contractor_list')) @app.route('/contracts/') @login_required def contract_review(contract_id): """Review contract details and sign""" from app.models import Contract from app.extensions import db contract = Contract.query.get_or_404(contract_id) # Check authorization if current_user.id not in [contract.contractor_id, contract.worker_id]: flash('You are not authorized to view this contract.', 'danger') return redirect(url_for('dashboard')) return render_template('contracts/review.html', contract=contract) @app.route('/contracts//closeout') @login_required def contract_closeout(contract_id): """Contract close-out interface""" from app.models import Contract from app.extensions import db contract = Contract.query.get_or_404(contract_id) # Check authorization if current_user.id not in [contract.contractor_id, contract.worker_id]: flash('You are not authorized to view this contract.', 'danger') return redirect(url_for('dashboard')) return render_template('contracts/closeout.html', contract=contract)