from flask import Blueprint, render_template, request, jsonify, redirect, url_for from .models import db, Job, Phase, Material from datetime import datetime from sqlalchemy import func main_bp = Blueprint('main', __name__) api_bp = Blueprint('api', __name__) # ==================== WEB ROUTES ==================== @main_bp.route('/') def dashboard(): return render_template('dashboard.html') @main_bp.route('/jobs') def jobs_list(): return render_template('jobs.html') @main_bp.route('/jobs/') def job_detail(job_id): job = Job.query.get_or_404(job_id) return render_template('job_detail.html', job=job) @main_bp.route('/jobs/new') def job_new(): return render_template('job_form.html', job=None) @main_bp.route('/jobs//edit') def job_edit(job_id): job = Job.query.get_or_404(job_id) return render_template('job_form.html', job=job) @main_bp.route('/schedule') def schedule(): return render_template('schedule.html') @main_bp.route('/materials') def materials(): return render_template('materials.html') # ==================== API ROUTES ==================== # Dashboard Stats @api_bp.route('/stats') def get_stats(): jobs = Job.query.all() total_jobs = len(jobs) total_budget = sum(j.fire_alarm_budget or 0 for j in jobs) total_labor = sum(j.labor_estimate or 0 for j in jobs) total_material = sum(j.material_estimate or 0 for j in jobs) # Calculate average completion avg_completion = sum(j.percent_complete or 0 for j in jobs) / total_jobs if total_jobs > 0 else 0 # Jobs by status completed_jobs = len([j for j in jobs if (j.percent_complete or 0) >= 1.0]) in_progress_jobs = len([j for j in jobs if 0 < (j.percent_complete or 0) < 1.0]) not_started_jobs = len([j for j in jobs if (j.percent_complete or 0) == 0]) # Budget by vendor vendor_budgets = {} for job in jobs: vendor = job.fire_vendor or 'Unknown' if vendor not in vendor_budgets: vendor_budgets[vendor] = 0 vendor_budgets[vendor] += job.fire_alarm_budget or 0 # Jobs by PM pm_jobs = {} for job in jobs: pm = job.pm_assigned or 'Unassigned' if pm not in pm_jobs: pm_jobs[pm] = {'count': 0, 'budget': 0} pm_jobs[pm]['count'] += 1 pm_jobs[pm]['budget'] += job.fire_alarm_budget or 0 return jsonify({ 'total_jobs': total_jobs, 'total_budget': total_budget, 'total_labor': total_labor, 'total_material': total_material, 'avg_completion': avg_completion * 100, 'completed_jobs': completed_jobs, 'in_progress_jobs': in_progress_jobs, 'not_started_jobs': not_started_jobs, 'vendor_budgets': vendor_budgets, 'pm_jobs': pm_jobs, 'jobs_completion': [ { 'id': j.id, 'job_number': j.job_number, 'name': j.job_name, 'completion': (j.percent_complete or 0) * 100, 'budget': j.fire_alarm_budget or 0, 'pm_assigned': j.pm_assigned } for j in jobs ] }) # Jobs CRUD @api_bp.route('/jobs', methods=['GET']) def get_jobs(): jobs = Job.query.all() return jsonify([job.to_dict() for job in jobs]) @api_bp.route('/jobs/', methods=['GET']) def get_job(job_id): job = Job.query.get_or_404(job_id) return jsonify(job.to_dict()) @api_bp.route('/jobs', methods=['POST']) def create_job(): data = request.json job = Job( job_number=data.get('job_number'), job_name=data.get('job_name'), location=data.get('location'), percent_complete=data.get('percent_complete', 0), est_starting_qtr=data.get('est_starting_qtr'), fire_alarm_budget=data.get('fire_alarm_budget', 0), labor_estimate=data.get('labor_estimate', 0), material_estimate=data.get('material_estimate', 0), amount_left_on_contract=data.get('amount_left_on_contract', 0), pm_assigned=data.get('pm_assigned'), aor=data.get('aor'), fire_vendor=data.get('fire_vendor'), install_partner=data.get('install_partner'), ps_or_install=data.get('ps_or_install'), subcontractor=data.get('subcontractor'), pci=data.get('pci'), voip_or_phone=data.get('voip_or_phone'), plans=data.get('plans'), notes=data.get('notes'), issues=data.get('issues'), milestone_1=data.get('milestone_1'), milestone_2=data.get('milestone_2'), milestone_3=data.get('milestone_3'), milestone_4=data.get('milestone_4'), milestone_5=data.get('milestone_5'), milestone_6=data.get('milestone_6'), milestone_7=data.get('milestone_7'), number_of_units=data.get('number_of_units'), sep_club_house=data.get('sep_club_house'), ) # Parse dates for date_field in ['elevator_final', 'pretest', 'final_date', 'co_drop_dead_date']: if data.get(date_field): try: setattr(job, date_field, datetime.strptime(data[date_field], '%Y-%m-%d').date()) except ValueError: pass db.session.add(job) db.session.commit() return jsonify(job.to_dict()), 201 @api_bp.route('/jobs/', methods=['PUT']) def update_job(job_id): job = Job.query.get_or_404(job_id) data = request.json # Update fields for field in ['job_number', 'job_name', 'location', 'percent_complete', 'est_starting_qtr', 'fire_alarm_budget', 'labor_estimate', 'material_estimate', 'amount_left_on_contract', 'pm_assigned', 'aor', 'fire_vendor', 'install_partner', 'ps_or_install', 'subcontractor', 'pci', 'voip_or_phone', 'plans', 'notes', 'issues', 'milestone_1', 'milestone_2', 'milestone_3', 'milestone_4', 'milestone_5', 'milestone_6', 'milestone_7', 'number_of_units', 'sep_club_house']: if field in data: setattr(job, field, data[field]) # Parse dates for date_field in ['elevator_final', 'pretest', 'final_date', 'co_drop_dead_date']: if date_field in data: if data[date_field]: try: setattr(job, date_field, datetime.strptime(data[date_field], '%Y-%m-%d').date()) except ValueError: pass else: setattr(job, date_field, None) db.session.commit() return jsonify(job.to_dict()) @api_bp.route('/jobs/', methods=['DELETE']) def delete_job(job_id): job = Job.query.get_or_404(job_id) db.session.delete(job) db.session.commit() return '', 204 # Phases CRUD @api_bp.route('/jobs//phases', methods=['GET']) def get_phases(job_id): phases = Phase.query.filter_by(job_id=job_id).order_by(Phase.phase_type, Phase.phase_number).all() return jsonify([p.to_dict() for p in phases]) @api_bp.route('/jobs//phases', methods=['POST']) def create_phase(job_id): Job.query.get_or_404(job_id) data = request.json phase = Phase( job_id=job_id, phase_type=data.get('phase_type'), phase_number=data.get('phase_number'), points=data.get('points', 0), men_on_site=data.get('men_on_site', 0), completed=data.get('completed', False), due_date_met=data.get('due_date_met'), pci_man_hours=data.get('pci_man_hours', 0), rer_fire_mgmt_hours=data.get('rer_fire_mgmt_hours', 0), rer_fire_mgmt_hours_avl=data.get('rer_fire_mgmt_hours_avl', 0), ) for date_field in ['mobilization_date', 'start_date', 'due_date', 'completion_date']: if data.get(date_field): try: setattr(phase, date_field, datetime.strptime(data[date_field], '%Y-%m-%d').date()) except ValueError: pass db.session.add(phase) db.session.commit() return jsonify(phase.to_dict()), 201 @api_bp.route('/phases/', methods=['PUT']) def update_phase(phase_id): phase = Phase.query.get_or_404(phase_id) data = request.json for field in ['phase_type', 'phase_number', 'points', 'men_on_site', 'completed', 'due_date_met', 'pci_man_hours', 'rer_fire_mgmt_hours', 'rer_fire_mgmt_hours_avl']: if field in data: setattr(phase, field, data[field]) for date_field in ['mobilization_date', 'start_date', 'due_date', 'completion_date']: if date_field in data: if data[date_field]: try: setattr(phase, date_field, datetime.strptime(data[date_field], '%Y-%m-%d').date()) except ValueError: pass else: setattr(phase, date_field, None) db.session.commit() return jsonify(phase.to_dict()) @api_bp.route('/phases/', methods=['DELETE']) def delete_phase(phase_id): phase = Phase.query.get_or_404(phase_id) db.session.delete(phase) db.session.commit() return '', 204 # Materials CRUD @api_bp.route('/jobs//materials', methods=['GET']) def get_materials(job_id): materials = Material.query.filter_by(job_id=job_id).all() return jsonify([m.to_dict() for m in materials]) @api_bp.route('/materials', methods=['GET']) def get_all_materials(): materials = Material.query.all() return jsonify([m.to_dict() for m in materials]) @api_bp.route('/jobs//materials', methods=['POST']) def create_material(job_id): Job.query.get_or_404(job_id) data = request.json material = Material( job_id=job_id, part_number=data.get('part_number'), quantity=data.get('quantity', 0), ordered=data.get('ordered', False), received=data.get('received', False), received_qty=data.get('received_qty', 0), received_by=data.get('received_by'), delivered_qty=data.get('delivered_qty', 0), delivered_to=data.get('delivered_to'), ) db.session.add(material) db.session.commit() return jsonify(material.to_dict()), 201 @api_bp.route('/materials/', methods=['PUT']) def update_material(material_id): material = Material.query.get_or_404(material_id) data = request.json for field in ['part_number', 'quantity', 'ordered', 'received', 'received_qty', 'received_by', 'delivered_qty', 'delivered_to']: if field in data: setattr(material, field, data[field]) db.session.commit() return jsonify(material.to_dict()) @api_bp.route('/materials/', methods=['DELETE']) def delete_material(material_id): material = Material.query.get_or_404(material_id) db.session.delete(material) db.session.commit() return '', 204