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.user 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', methods=['GET', 'POST'])
def register():
"""User registration page and handler"""
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
if request.method == 'POST':
from app.models.user import User
from app.extensions import db
# Get form data
email = request.form.get('email', '').strip().lower()
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
first_name = request.form.get('first_name', '').strip()
last_name = request.form.get('last_name', '').strip()
role = request.form.get('role')
phone_number = request.form.get('phone_number', '').strip()
location = request.form.get('location', '').strip()
abn_number = request.form.get('abn_number', '').strip()
# Validation
errors = []
if not email:
errors.append('Email is required')
elif User.query.filter_by(email=email).first():
errors.append('Email already registered')
if not password or len(password) < 6:
errors.append('Password must be at least 6 characters')
elif password != confirm_password:
errors.append('Passwords do not match')
if not first_name:
errors.append('First name is required')
if not last_name:
errors.append('Last name is required')
if role not in ['contractor', 'worker']:
errors.append('Please select a valid role')
if not phone_number:
errors.append('Phone number is required')
if not location:
errors.append('Location is required')
if not abn_number or len(abn_number) != 11:
errors.append('Valid 11-digit ABN is required')
if errors:
for error in errors:
flash(error, 'error')
return render_template('auth/register.html')
try:
# Create new user
user = User(
email=email,
first_name=first_name,
last_name=last_name,
role=role,
phone_number=phone_number,
location=location,
abn_number=abn_number,
privacy_consent=True,
terms_accepted=True,
terms_accepted_date=datetime.utcnow()
)
user.set_password(password)
db.session.add(user)
db.session.commit()
login_user(user)
flash('Registration successful! Welcome to RateRight!', 'success')
return redirect(url_for('dashboard'))
except Exception as e:
db.session.rollback()
flash(f'Registration failed: {str(e)}', 'error')
return render_template('auth/register.html')
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'))
# Count of posted jobs (active only)
job_count = Job.query.filter_by(
contractor_id=current_user.id,
status='open'
).count()
# Pending applications that need review
pending_applications = db.session.query(Application).join(Job).filter(
Job.contractor_id == current_user.id,
Application.status == 'pending'
).order_by(Application.date_applied.desc()).limit(5).all()
# Recently processed applications (history)
processed_applications = db.session.query(Application).join(Job).filter(
Job.contractor_id == current_user.id,
Application.status.in_(['accepted', 'rejected'])
).order_by(Application.date_applied.desc()).limit(5).all()
# Calculate metrics
total_applications = len(pending_applications) + len(processed_applications)
recent_applications = pending_applications
# Active contracts only
active_contracts = Contract.query.filter(
Contract.contractor_id == current_user.id,
Contract.status.in_(['active', 'pending_review'])
).order_by(Contract.created_at.desc()).all()
# Calculate total pending payouts from active contracts
pending_payouts = sum(float(contract.agreed_rate) for contract in active_contracts if contract.agreed_rate)
# Get Michael's posted jobs (ADD THIS)
posted_jobs = Job.query.filter_by(
contractor_id=current_user.id
).order_by(Job.date_posted.desc()).all()
return render_template('dashboard/contractor.html',
posted_jobs=posted_jobs,
recent_applications=recent_applications,
processed_applications=processed_applications,
pending_applications=pending_applications,
active_contracts=active_contracts,
pending_payouts=pending_payouts,
job_count=len(posted_jobs))
@app.route('/dashboard/worker')
def dashboard_worker():
"""Worker dashboard"""
from app.models import Contract
from app.extensions import db
if not current_user.is_authenticated or current_user.role != 'worker':
return redirect(url_for('login'))
# Get active contracts only
active_contracts = Contract.query.filter(
Contract.worker_id == current_user.id,
Contract.status.in_(['active', 'pending_review', 'pending_rating'])
).order_by(Contract.created_at.desc()).all()
# Calculate pending money from active contracts
pending_money = sum(float(contract.agreed_rate) for contract in active_contracts if contract.agreed_rate)
# Get average rating from completed contracts with reviews
completed_contracts = Contract.query.filter(
Contract.worker_id == current_user.id,
Contract.status == 'completed'
).all()
# Calculate average rating (assuming reviews have rating fields)
total_rating = 0
rating_count = 0
for contract in completed_contracts:
if hasattr(contract, 'reviews') and contract.reviews:
for review in contract.reviews:
if hasattr(review, 'rating') and review.rating:
total_rating += review.rating
rating_count += 1
avg_rating = round(total_rating / rating_count, 1) if rating_count > 0 else 0.0
# Count active jobs (contracts that need action)
active_job_count = len(active_contracts)
return render_template('dashboard/worker.html',
active_contracts=active_contracts,
pending_money=pending_money,
avg_rating=avg_rating,
active_job_count=active_job_count)
@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_completion(contract_id):
"""Contractor approves work completion"""
from app.models.contract import Contract
from app.extensions import db
from datetime import datetime
contract = Contract.query.get_or_404(contract_id)
# Only contractor can approve
if current_user.id != contract.contractor_id:
flash('Only the contractor can approve completion.', 'error')
return redirect(url_for('contracts_review', contract_id=contract_id))
# Update status
contract.status = 'pending_rating'
contract.contractor_approval_date = datetime.utcnow()
db.session.commit()
flash('Work approved! You can now rate each other.', 'success')
return redirect(url_for('contracts_review', contract_id=contract_id))
@app.route('/contracts//dispute-completion', methods=['POST'])
@login_required
def dispute_completion(contract_id):
"""Contractor disputes work completion"""
from app.models.contract import Contract
from app.extensions import db
contract = Contract.query.get_or_404(contract_id)
if current_user.id != contract.contractor_id:
flash('Only the contractor can dispute completion.', 'error')
return redirect(url_for('contracts_review', contract_id=contract_id))
contract.status = 'disputed'
db.session.commit()
flash('Completion disputed. Please contact the worker to resolve.', 'warning')
return redirect(url_for('contracts_review', 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)
@app.route('/contracts//rate', methods=['GET', 'POST'])
@login_required
def rate_contract(contract_id):
"""Rate contract performance (both parties can rate)"""
from app.models.contract import Contract
from app.models.safety import Review
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 rate this contract.', 'error')
return redirect(url_for('dashboard'))
# Check if contract is ready for rating
if contract.status != 'pending_rating':
flash('This contract is not ready for rating.', 'warning')
return redirect(url_for('contracts_review', contract_id=contract_id))
# Check if user already rated
existing_review = Review.query.filter_by(
contract_id=contract_id,
reviewer_id=current_user.id
).first()
if existing_review:
flash('You have already rated this contract.', 'info')
return redirect(url_for('contracts_review', contract_id=contract_id))
if request.method == 'POST':
try:
# Get form data
overall_rating = int(request.form.get('overall_rating'))
quality_rating = int(request.form.get('quality_rating'))
communication_rating = int(request.form.get('communication_rating'))
safety_rating = int(request.form.get('safety_rating'))
comment = request.form.get('comment', '').strip()
would_work_again = bool(request.form.get('would_work_again'))
# Validate ratings
for rating in [overall_rating, quality_rating, communication_rating, safety_rating]:
if not (1 <= rating <= 5):
flash('All ratings must be between 1 and 5.', 'error')
return render_template('contracts/rate.html', contract=contract)
# Determine who is being rated
reviewee_id = contract.worker_id if current_user.id == contract.contractor_id else contract.contractor_id
# Create review
review = Review(
reviewer_id=current_user.id,
reviewee_id=reviewee_id,
job_id=contract.job_id,
contract_id=contract_id,
overall_rating=overall_rating,
quality_rating=quality_rating,
communication_rating=communication_rating,
safety_rating=safety_rating,
comment=comment,
would_work_again=would_work_again,
verified_completion=True
)
db.session.add(review)
# Update contract rating status
if current_user.id == contract.contractor_id:
contract.contractor_rated = True
else:
contract.worker_rated = True
# Check if both parties have rated
if contract.contractor_rated and contract.worker_rated:
contract.status = 'completed'
contract.mutual_rating_completed_date = datetime.utcnow()
flash('Rating submitted! Contract is now complete.', 'success')
else:
flash('Rating submitted! Waiting for the other party to rate.', 'success')
db.session.commit()
return redirect(url_for('contracts_review', contract_id=contract_id))
except (ValueError, TypeError) as e:
flash('Invalid rating values provided.', 'error')
return render_template('contracts/rate.html', contract=contract)
except Exception as e:
db.session.rollback()
flash(f'Error submitting rating: {str(e)}', 'error')
return render_template('contracts/rate.html', contract=contract)
# GET request - show rating form
return render_template('contracts/rate.html', contract=contract)
@app.route('/contracts//ratings')
@login_required
def view_ratings(contract_id):
"""View all ratings for a contract"""
from app.models.contract import Contract
from app.models.safety import Review
contract = Contract.query.get_or_404(contract_id)
# Authorization check
if current_user.id not in [contract.contractor_id, contract.worker_id]:
flash('Not authorized to view these ratings.', 'error')
return redirect(url_for('dashboard'))
# Get all reviews for this contract
reviews = Review.query.filter_by(contract_id=contract_id).all()
return render_template('contracts/ratings.html',
contract=contract,
reviews=reviews)