Initial commit - Fire alarm management application

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 21:57:25 -05:00
commit 892ac3d23b
24 changed files with 4183 additions and 0 deletions

337
app/routes.py Normal file
View File

@@ -0,0 +1,337 @@
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/<int:job_id>')
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/<int:job_id>/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/<int:job_id>', 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/<int:job_id>', 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/<int:job_id>', 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/<int:job_id>/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/<int:job_id>/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/<int:phase_id>', 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/<int:phase_id>', 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/<int:job_id>/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/<int:job_id>/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/<int:material_id>', 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/<int:material_id>', methods=['DELETE'])
def delete_material(material_id):
material = Material.query.get_or_404(material_id)
db.session.delete(material)
db.session.commit()
return '', 204