From 892ac3d23b5bfc7f8bc6a8a6ae5b3970e820d156 Mon Sep 17 00:00:00 2001 From: pnannery Date: Mon, 19 Jan 2026 21:57:25 -0500 Subject: [PATCH] Initial commit - Fire alarm management application Co-Authored-By: Claude Opus 4.5 --- API.md | 722 +++++++++++++++++++++++ DEVELOPMENT.md | 417 +++++++++++++ README.md | 159 +++++ Raleigh jobs FIRE ALARM INFORMATION.xlsx | Bin 0 -> 21228 bytes app/__init__.py | 25 + app/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1511 bytes app/__pycache__/models.cpython-311.pyc | Bin 0 -> 11809 bytes app/__pycache__/routes.cpython-311.pyc | Bin 0 -> 20866 bytes app/models.py | 182 ++++++ app/routes.py | 337 +++++++++++ app/static/css/style.css | 108 ++++ app/static/js/app.js | 110 ++++ app/templates/base.html | 57 ++ app/templates/dashboard.html | 350 +++++++++++ app/templates/job_detail.html | 436 ++++++++++++++ app/templates/job_form.html | 316 ++++++++++ app/templates/jobs.html | 237 ++++++++ app/templates/materials.html | 256 ++++++++ app/templates/schedule.html | 182 ++++++ import_data.py | 265 +++++++++ instance/fire_alarm.db | Bin 0 -> 20480 bytes requirements.txt | 4 + run.py | 20 + schedule_updated.xlsm | Bin 0 -> 39881 bytes 24 files changed, 4183 insertions(+) create mode 100644 API.md create mode 100644 DEVELOPMENT.md create mode 100644 README.md create mode 100644 Raleigh jobs FIRE ALARM INFORMATION.xlsx create mode 100644 app/__init__.py create mode 100644 app/__pycache__/__init__.cpython-311.pyc create mode 100644 app/__pycache__/models.cpython-311.pyc create mode 100644 app/__pycache__/routes.cpython-311.pyc create mode 100644 app/models.py create mode 100644 app/routes.py create mode 100644 app/static/css/style.css create mode 100644 app/static/js/app.js create mode 100644 app/templates/base.html create mode 100644 app/templates/dashboard.html create mode 100644 app/templates/job_detail.html create mode 100644 app/templates/job_form.html create mode 100644 app/templates/jobs.html create mode 100644 app/templates/materials.html create mode 100644 app/templates/schedule.html create mode 100644 import_data.py create mode 100644 instance/fire_alarm.db create mode 100644 requirements.txt create mode 100644 run.py create mode 100644 schedule_updated.xlsm diff --git a/API.md b/API.md new file mode 100644 index 0000000..e9cd255 --- /dev/null +++ b/API.md @@ -0,0 +1,722 @@ +# API Documentation + +The Romanoff Fire Alarm Management System provides a RESTful API for programmatic access to all data. All API endpoints are prefixed with `/api`. + +## Base URL + +``` +http://localhost:5000/api +``` + +## Response Format + +All API responses are JSON. Successful responses return the requested data directly. Error responses include appropriate HTTP status codes. + +## Endpoints + +### Dashboard Statistics + +#### Get Dashboard Stats + +``` +GET /api/stats +``` + +Returns aggregated statistics for the dashboard. + +**Response** + +```json +{ + "total_jobs": 25, + "total_budget": 1500000.00, + "total_labor": 450000.00, + "total_material": 350000.00, + "avg_completion": 65.5, + "completed_jobs": 8, + "in_progress_jobs": 15, + "not_started_jobs": 2, + "vendor_budgets": { + "Vendor A": 750000.00, + "Vendor B": 500000.00, + "Unknown": 250000.00 + }, + "pm_jobs": { + "John Smith": { + "count": 10, + "budget": 600000.00 + }, + "Jane Doe": { + "count": 8, + "budget": 500000.00 + } + }, + "jobs_completion": [ + { + "id": 1, + "job_number": "JOB-001", + "name": "Project Alpha", + "completion": 75.0, + "budget": 50000.00, + "pm_assigned": "John Smith" + } + ] +} +``` + +--- + +### Jobs + +#### List All Jobs + +``` +GET /api/jobs +``` + +Returns a list of all jobs. + +**Response** + +```json +[ + { + "id": 1, + "job_number": "JOB-001", + "job_name": "Project Alpha", + "location": "123 Main St, Raleigh, NC", + "percent_complete": 0.75, + "est_starting_qtr": "Q1 2025", + "fire_alarm_budget": 50000.00, + "labor_estimate": 20000.00, + "material_estimate": 15000.00, + "amount_left_on_contract": 15000.00, + "pm_assigned": "John Smith", + "aor": "Region A", + "fire_vendor": "Vendor A", + "install_partner": "Partner Inc", + "ps_or_install": "Install", + "subcontractor": "Sub Co", + "pci": "Yes", + "voip_or_phone": "VoIP", + "plans": "Approved", + "notes": "Project notes here", + "issues": "No major issues", + "milestone_1": "Permits obtained", + "milestone_2": "Rough-in complete", + "milestone_3": null, + "milestone_4": null, + "milestone_5": null, + "milestone_6": null, + "milestone_7": null, + "elevator_final": "2025-03-15", + "pretest": "2025-04-01", + "final_date": "2025-04-15", + "co_drop_dead_date": "2025-04-30", + "number_of_units": 150, + "sep_club_house": "Yes", + "created_at": "2025-01-01T00:00:00", + "updated_at": "2025-01-15T12:30:00" + } +] +``` + +#### Get Single Job + +``` +GET /api/jobs/ +``` + +Returns a single job by ID. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| id | integer | Job ID (path parameter) | + +**Response** + +Returns a job object (same structure as list item above). + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Job not found | + +#### Create Job + +``` +POST /api/jobs +``` + +Creates a new job. + +**Request Body** + +```json +{ + "job_number": "JOB-002", + "job_name": "Project Beta", + "location": "456 Oak Ave, Raleigh, NC", + "percent_complete": 0, + "est_starting_qtr": "Q2 2025", + "fire_alarm_budget": 75000.00, + "labor_estimate": 30000.00, + "material_estimate": 25000.00, + "amount_left_on_contract": 75000.00, + "pm_assigned": "Jane Doe", + "aor": "Region B", + "fire_vendor": "Vendor B", + "install_partner": "Partner Corp", + "ps_or_install": "PS", + "subcontractor": "Sub LLC", + "pci": "No", + "voip_or_phone": "Phone", + "plans": "Pending", + "notes": "New project", + "issues": "", + "milestone_1": "Site survey", + "number_of_units": 200, + "sep_club_house": "No", + "elevator_final": "2025-06-15", + "pretest": "2025-07-01", + "final_date": "2025-07-15", + "co_drop_dead_date": "2025-07-31" +} +``` + +**Required Fields** + +| Field | Type | Description | +|-------|------|-------------| +| job_number | string | Unique job identifier | +| job_name | string | Job name/title | + +**Optional Fields** + +| Field | Type | Description | +|-------|------|-------------| +| location | string | Job location address | +| percent_complete | float | Completion percentage (0.0 - 1.0) | +| est_starting_qtr | string | Estimated starting quarter | +| fire_alarm_budget | float | Total fire alarm budget | +| labor_estimate | float | Labor cost estimate | +| material_estimate | float | Material cost estimate | +| amount_left_on_contract | float | Remaining contract amount | +| pm_assigned | string | Project manager name | +| aor | string | Area of responsibility | +| fire_vendor | string | Fire alarm vendor | +| install_partner | string | Installation partner | +| ps_or_install | string | PS or Install designation | +| subcontractor | string | Subcontractor name | +| pci | string | PCI status | +| voip_or_phone | string | Communication type | +| plans | string | Plans status | +| notes | string | General notes | +| issues | string | Known issues | +| milestone_1 - milestone_7 | string | Project milestones | +| elevator_final | date | Elevator final date (YYYY-MM-DD) | +| pretest | date | Pretest date (YYYY-MM-DD) | +| final_date | date | Final date (YYYY-MM-DD) | +| co_drop_dead_date | date | CO drop dead date (YYYY-MM-DD) | +| number_of_units | integer | Number of units | +| sep_club_house | string | Separate club house indicator | + +**Response** + +Returns the created job object with status `201 Created`. + +#### Update Job + +``` +PUT /api/jobs/ +``` + +Updates an existing job. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| id | integer | Job ID (path parameter) | + +**Request Body** + +Include only the fields you want to update. Same fields as create. + +```json +{ + "percent_complete": 0.85, + "notes": "Updated project notes" +} +``` + +**Response** + +Returns the updated job object. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Job not found | + +#### Delete Job + +``` +DELETE /api/jobs/ +``` + +Deletes a job and all associated phases and materials. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| id | integer | Job ID (path parameter) | + +**Response** + +Returns `204 No Content` on success. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Job not found | + +--- + +### Phases + +#### List Phases for Job + +``` +GET /api/jobs//phases +``` + +Returns all phases for a specific job. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| job_id | integer | Job ID (path parameter) | + +**Response** + +```json +[ + { + "id": 1, + "job_id": 1, + "phase_type": "rough_in", + "phase_number": 1, + "points": 500, + "mobilization_date": "2025-02-01", + "start_date": "2025-02-05", + "due_date": "2025-02-28", + "completion_date": "2025-02-25", + "men_on_site": 5, + "completed": true, + "due_date_met": true, + "pci_man_hours": 120.5, + "rer_fire_mgmt_hours": 40.0, + "rer_fire_mgmt_hours_avl": 50.0 + } +] +``` + +#### Create Phase + +``` +POST /api/jobs//phases +``` + +Creates a new phase for a job. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| job_id | integer | Job ID (path parameter) | + +**Request Body** + +```json +{ + "phase_type": "trim", + "phase_number": 2, + "points": 300, + "mobilization_date": "2025-03-01", + "start_date": "2025-03-05", + "due_date": "2025-03-31", + "men_on_site": 4, + "completed": false, + "pci_man_hours": 0, + "rer_fire_mgmt_hours": 0, + "rer_fire_mgmt_hours_avl": 30.0 +} +``` + +**Fields** + +| Field | Type | Description | +|-------|------|-------------| +| phase_type | string | Phase type: `rough_in`, `trim`, `commissioning`, `final`, `turnover` | +| phase_number | integer | Phase sequence number | +| points | integer | Points assigned to phase | +| mobilization_date | date | Mobilization date (YYYY-MM-DD) | +| start_date | date | Start date (YYYY-MM-DD) | +| due_date | date | Due date (YYYY-MM-DD) | +| completion_date | date | Completion date (YYYY-MM-DD) | +| men_on_site | integer | Number of workers on site | +| completed | boolean | Whether phase is complete | +| due_date_met | boolean | Whether due date was met | +| pci_man_hours | float | PCI man hours | +| rer_fire_mgmt_hours | float | RER fire management hours | +| rer_fire_mgmt_hours_avl | float | RER fire management hours available | + +**Response** + +Returns the created phase object with status `201 Created`. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Job not found | + +#### Update Phase + +``` +PUT /api/phases/ +``` + +Updates an existing phase. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| phase_id | integer | Phase ID (path parameter) | + +**Request Body** + +Include only the fields you want to update. + +```json +{ + "completed": true, + "completion_date": "2025-03-28", + "due_date_met": true +} +``` + +**Response** + +Returns the updated phase object. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Phase not found | + +#### Delete Phase + +``` +DELETE /api/phases/ +``` + +Deletes a phase. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| phase_id | integer | Phase ID (path parameter) | + +**Response** + +Returns `204 No Content` on success. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Phase not found | + +--- + +### Materials + +#### List All Materials + +``` +GET /api/materials +``` + +Returns all materials across all jobs. + +**Response** + +```json +[ + { + "id": 1, + "job_id": 1, + "part_number": "FA-SENSOR-001", + "quantity": 50, + "ordered": true, + "received": true, + "received_qty": 50, + "received_by": "John Doe", + "delivered_qty": 45, + "delivered_to": "Site A - Building 1" + } +] +``` + +#### List Materials for Job + +``` +GET /api/jobs//materials +``` + +Returns all materials for a specific job. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| job_id | integer | Job ID (path parameter) | + +**Response** + +Returns an array of material objects (same structure as above). + +#### Create Material + +``` +POST /api/jobs//materials +``` + +Creates a new material record for a job. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| job_id | integer | Job ID (path parameter) | + +**Request Body** + +```json +{ + "part_number": "FA-PANEL-002", + "quantity": 10, + "ordered": false, + "received": false, + "received_qty": 0, + "received_by": "", + "delivered_qty": 0, + "delivered_to": "" +} +``` + +**Fields** + +| Field | Type | Description | +|-------|------|-------------| +| part_number | string | Part/item number | +| quantity | integer | Quantity needed | +| ordered | boolean | Whether order has been placed | +| received | boolean | Whether items have been received | +| received_qty | integer | Quantity received | +| received_by | string | Person who received items | +| delivered_qty | integer | Quantity delivered to site | +| delivered_to | string | Delivery location | + +**Response** + +Returns the created material object with status `201 Created`. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Job not found | + +#### Update Material + +``` +PUT /api/materials/ +``` + +Updates an existing material record. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| material_id | integer | Material ID (path parameter) | + +**Request Body** + +Include only the fields you want to update. + +```json +{ + "ordered": true, + "received": true, + "received_qty": 10, + "received_by": "Jane Smith" +} +``` + +**Response** + +Returns the updated material object. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Material not found | + +#### Delete Material + +``` +DELETE /api/materials/ +``` + +Deletes a material record. + +**Parameters** + +| Name | Type | Description | +|------|------|-------------| +| material_id | integer | Material ID (path parameter) | + +**Response** + +Returns `204 No Content` on success. + +**Errors** + +| Status | Description | +|--------|-------------| +| 404 | Material not found | + +--- + +## Data Types + +### Date Format + +All dates use ISO 8601 format: `YYYY-MM-DD` + +Examples: +- `2025-01-15` +- `2025-12-31` + +### DateTime Format + +All timestamps use ISO 8601 format: `YYYY-MM-DDTHH:MM:SS` + +Examples: +- `2025-01-15T14:30:00` +- `2025-12-31T23:59:59` + +### Phase Types + +Valid values for `phase_type`: +- `rough_in` - Initial rough-in phase +- `trim` - Trim phase +- `commissioning` - System commissioning +- `final` - Final inspection +- `turnover` - Project turnover + +--- + +## Error Handling + +The API uses standard HTTP status codes: + +| Status | Description | +|--------|-------------| +| 200 | Success | +| 201 | Created | +| 204 | No Content (successful deletion) | +| 400 | Bad Request (invalid input) | +| 404 | Not Found | +| 500 | Internal Server Error | + +--- + +## Examples + +### cURL Examples + +**Get all jobs:** +```bash +curl http://localhost:5000/api/jobs +``` + +**Create a job:** +```bash +curl -X POST http://localhost:5000/api/jobs \ + -H "Content-Type: application/json" \ + -d '{"job_number": "JOB-003", "job_name": "New Project"}' +``` + +**Update a job:** +```bash +curl -X PUT http://localhost:5000/api/jobs/1 \ + -H "Content-Type: application/json" \ + -d '{"percent_complete": 0.5}' +``` + +**Delete a job:** +```bash +curl -X DELETE http://localhost:5000/api/jobs/1 +``` + +### JavaScript Fetch Examples + +**Get all jobs:** +```javascript +const response = await fetch('/api/jobs'); +const jobs = await response.json(); +``` + +**Create a job:** +```javascript +const response = await fetch('/api/jobs', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + job_number: 'JOB-003', + job_name: 'New Project' + }) +}); +const newJob = await response.json(); +``` + +**Update a job:** +```javascript +const response = await fetch('/api/jobs/1', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ percent_complete: 0.5 }) +}); +const updatedJob = await response.json(); +``` + +**Delete a job:** +```javascript +await fetch('/api/jobs/1', { method: 'DELETE' }); +``` diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..9a66e52 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,417 @@ +# Development Guide + +This guide covers the architecture, code organization, and contribution guidelines for the Romanoff Fire Alarm Management System. + +## Architecture Overview + +### Application Structure + +``` +romanoff/ +├── run.py # Entry point - starts Flask dev server +├── import_data.py # Data import utility +├── requirements.txt # Dependencies +├── app/ +│ ├── __init__.py # Flask app factory +│ ├── models.py # SQLAlchemy models +│ ├── routes.py # Web routes & API endpoints +│ ├── templates/ # Jinja2 templates +│ └── static/ # CSS, JavaScript assets +└── instance/ # SQLite database (auto-created) +``` + +### Design Patterns + +**Flask App Factory** + +The application uses the factory pattern in `app/__init__.py`: + +```python +def create_app(): + app = Flask(__name__) + # Configure app + db.init_app(app) + # Register blueprints + app.register_blueprint(main_bp) + app.register_blueprint(api_bp, url_prefix='/api') + return app +``` + +**Blueprint Organization** + +Routes are organized into two blueprints: +- `main_bp` - Web routes serving HTML pages +- `api_bp` - REST API endpoints (prefixed with `/api`) + +**Model-View Separation** + +- Models (`models.py`) handle data persistence +- Routes (`routes.py`) handle request/response logic +- Templates (`templates/`) handle presentation + +### Data Flow + +``` +User Browser + ↓ +Flask Routes (routes.py) + ↓ +SQLAlchemy Models (models.py) + ↓ +SQLite Database (instance/romanoff.db) +``` + +## Database Schema + +### Entity Relationship + +``` +┌─────────────┐ +│ Job │ +├─────────────┤ +│ id (PK) │ +│ job_number │───────┐ +│ job_name │ │ +│ ... │ │ +└─────────────┘ │ + │ │ + │ 1:N │ 1:N + ↓ ↓ +┌─────────────┐ ┌─────────────┐ +│ Phase │ │ Material │ +├─────────────┤ ├─────────────┤ +│ id (PK) │ │ id (PK) │ +│ job_id (FK) │ │ job_id (FK) │ +│ phase_type │ │ part_number │ +│ ... │ │ ... │ +└─────────────┘ └─────────────┘ +``` + +### Models + +**Job** - Core project entity +- Contains budget, team assignments, milestones, dates +- Has many phases and materials (cascade delete) + +**Phase** - Project schedule phases +- Types: rough_in, trim, commissioning, final, turnover +- Tracks dates, completion status, man hours + +**Material** - Inventory tracking +- Tracks ordering, receipt, and delivery + +## Frontend Architecture + +### Template Hierarchy + +``` +base.html +├── dashboard.html - Charts and statistics +├── jobs.html - Job list with filters +├── job_detail.html - Single job view +├── job_form.html - Create/edit job +├── schedule.html - Phase management +└── materials.html - Material tracking +``` + +### JavaScript Strategy + +**Shared Utilities** (`static/js/app.js`) + +```javascript +// API helpers +async function apiGet(url) { ... } +async function apiPost(url, data) { ... } +async function apiPut(url, data) { ... } +async function apiDelete(url) { ... } + +// Formatting +function formatCurrency(value) { ... } +function formatDate(dateString) { ... } + +// UI helpers +function showToast(message, type) { ... } +function getProgressClass(percent) { ... } +function debounce(func, wait) { ... } +function confirmAction(message) { ... } +``` + +**Page-Specific Scripts** + +Each template contains embedded ` + + {% block head %}{% endblock %} + + + + +
+ {% block content %}{% endblock %} +
+ + + + {% block scripts %}{% endblock %} + + diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..bf11b41 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,350 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - Fire Alarm Management{% endblock %} + +{% block content %} +
+
+

Dashboard

+
+
+ + +
+
+
+
+
Total Jobs
+

-

+
+
+
+
+
+
+
Total Budget
+

-

+
+
+
+
+
+
+
Avg Completion
+

-

+
+
+
+
+
+
+
Labor + Material
+

-

+
+
+
+
+ + +
+
+
+
+ Jobs by Status +
+
+ +
+
+
+
+
+
+ Budget by Vendor +
+
+ +
+
+
+
+ + +
+
+
+
+ Job Completion Progress +
+
+ +
+
+
+
+ + +
+
+
+
+ Jobs by Project Manager +
+
+ +
+
+
+
+
+
+ Budget vs Estimates +
+
+ +
+
+
+
+ + +
+
+
+
+ Recent Jobs + View All +
+
+
+ + + + + + + + + + + + + + + + +
Job #NamePMBudgetProgressActions
Loading...
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/job_detail.html b/app/templates/job_detail.html new file mode 100644 index 0000000..007db60 --- /dev/null +++ b/app/templates/job_detail.html @@ -0,0 +1,436 @@ +{% extends "base.html" %} + +{% block title %}{{ job.job_name }} - Fire Alarm Management{% endblock %} + +{% block content %} +
+
+ +

+ {{ job.job_name }} + #{{ job.job_number }} +

+
+ +
+ + +
+
+
+
+
Overall Progress: {{ (job.percent_complete * 100)|round(0) }}%
+
+
+ {{ (job.percent_complete * 100)|round(0) }}% +
+
+
+
+
+
+ +
+ +
+
+
Basic Information
+
+ + + + + +
Location:{{ job.location or '-' }}
Est. Starting Qtr:{{ job.est_starting_qtr or '-' }}
Number of Units:{{ job.number_of_units or '-' }}
Sep Club House:{{ job.sep_club_house or '-' }}
+
+
+
+ + +
+
+
Budget & Estimates
+
+ + + + + +
Fire Alarm Budget:${{ '{:,.2f}'.format(job.fire_alarm_budget or 0) }}
Labor Estimate:${{ '{:,.2f}'.format(job.labor_estimate or 0) }}
Material Estimate:${{ '{:,.2f}'.format(job.material_estimate or 0) }}
Amount Left:${{ '{:,.2f}'.format(job.amount_left_on_contract or 0) }}
+
+
+
+ + +
+
+
Assignments
+
+ + + + + + + + +
PM Assigned:{{ job.pm_assigned or '-' }}
AOR:{{ job.aor or '-' }}
Fire Vendor:{{ job.fire_vendor or '-' }}
Install Partner:{{ job.install_partner or '-' }}
P/S or Install:{{ job.ps_or_install or '-' }}
Subcontractor:{{ job.subcontractor or '-' }}
PCI:{{ job.pci or '-' }}
+
+
+
+ + +
+
+
Communication & Plans
+
+ + + +
VOIP/Phone:{{ job.voip_or_phone or '-' }}
Plans:{{ job.plans or '-' }}
+
+
+
+ + +
+
+
Key Dates
+
+ + + + + +
Elevator Final:{{ job.elevator_final or '-' }}
Pretest:{{ job.pretest or '-' }}
Final:{{ job.final_date or '-' }}
C/O Drop Dead:{{ job.co_drop_dead_date or '-' }}
+
+
+
+ + +
+
+
Milestones
+
+ + {% for i in range(1, 8) %} + {% set milestone = job['milestone_' ~ i] %} + {% if milestone %} + + {% endif %} + {% endfor %} +
{{ i }}{{ 'st' if i == 1 else ('nd' if i == 2 else ('rd' if i == 3 else 'th')) }}:{{ milestone }}
+
+
+
+ + +
+
+
Notes
+
+

{{ job.notes or 'No notes' }}

+
+
+
+
+
+
Issues
+
+

{{ job.issues or 'No issues' }}

+
+
+
+
+ + +
+
+
+
+ Phases + +
+
+
+ + + + + + + + + + + + + + + + +
TypePhase #PointsStart DateDue DateMen on SiteStatusActions
Loading...
+
+
+
+
+
+ + +
+
+
+
+ Materials + +
+
+
+ + + + + + + + + + + + + + + +
Part #QtyOrderedReceivedReceived QtyDelivered QtyActions
Loading...
+
+
+
+
+
+ + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/job_form.html b/app/templates/job_form.html new file mode 100644 index 0000000..877dac8 --- /dev/null +++ b/app/templates/job_form.html @@ -0,0 +1,316 @@ +{% extends "base.html" %} + +{% block title %}{{ 'Edit' if job else 'New' }} Job - Fire Alarm Management{% endblock %} + +{% block content %} +
+
+

+ + {{ 'Edit Job: ' + job.job_name if job else 'New Job' }} +

+
+
+ +
+
+ +
+
+
Basic Information
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
Budget & Estimates
+
+
+
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+
+ +
+ $ + +
+
+
+ +
+ + 0-1 (e.g., 0.5 = 50%) +
+
+
+
+
+
+ + +
+
+
Assignments
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
Communication & Plans
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
Key Dates
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + +
+
+
Milestones
+
+
+ {% for i in range(1, 8) %} +
+ + +
+ {% endfor %} +
+
+
+
+ + +
+
+
Notes & Issues
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+
+ + + Cancel + +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/jobs.html b/app/templates/jobs.html new file mode 100644 index 0000000..0431f10 --- /dev/null +++ b/app/templates/jobs.html @@ -0,0 +1,237 @@ +{% extends "base.html" %} + +{% block title %}Jobs - Fire Alarm Management{% endblock %} + +{% block content %} +
+
+

Jobs

+
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + +
Job #NameLocationPMVendorBudgetProgressUnitsActions
Loading...
+
+
+
+ + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/materials.html b/app/templates/materials.html new file mode 100644 index 0000000..f19ee75 --- /dev/null +++ b/app/templates/materials.html @@ -0,0 +1,256 @@ +{% extends "base.html" %} + +{% block title %}Materials - Fire Alarm Management{% endblock %} + +{% block content %} +
+
+

Materials Tracking

+
+
+ + +
+
+
+
+
Total Items
+

-

+
+
+
+
+
+
+
Pending Order
+

-

+
+
+
+
+
+
+
Ordered
+

-

+
+
+
+
+
+
+
Received
+

-

+
+
+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
JobPart #Qty NeededOrderedReceivedReceived QtyReceived ByDelivered QtyDelivered ToActions
Loading...
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/schedule.html b/app/templates/schedule.html new file mode 100644 index 0000000..a975e92 --- /dev/null +++ b/app/templates/schedule.html @@ -0,0 +1,182 @@ +{% extends "base.html" %} + +{% block title %}Schedule - Fire Alarm Management{% endblock %} + +{% block content %} +
+
+

Schedule Overview

+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ Loading... +
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/import_data.py b/import_data.py new file mode 100644 index 0000000..d6d2d20 --- /dev/null +++ b/import_data.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +Import data from Excel spreadsheets into the Fire Alarm Management database. +""" + +import pandas as pd +from datetime import datetime +import sys +import os + +# Add the app directory to the path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app import create_app +from app.models import db, Job, Phase, Material + + +def parse_date(value): + """Parse date from various formats.""" + if pd.isna(value) or value is None: + return None + if isinstance(value, datetime): + return value.date() + if isinstance(value, str): + try: + return datetime.strptime(value, '%Y-%m-%d').date() + except ValueError: + try: + return datetime.strptime(value, '%m/%d/%Y').date() + except ValueError: + return None + return None + + +def parse_float(value, default=0.0): + """Parse float from various formats.""" + if pd.isna(value) or value is None: + return default + try: + return float(value) + except (ValueError, TypeError): + return default + + +def parse_int(value, default=None): + """Parse integer from various formats.""" + if pd.isna(value) or value is None: + return default + try: + return int(float(value)) + except (ValueError, TypeError): + return default + + +def parse_str(value): + """Parse string, returning None for NaN values.""" + if pd.isna(value) or value is None: + return None + return str(value).strip() if str(value).strip() else None + + +def import_fire_alarm_jobs(app, filepath): + """Import jobs from the Fire Alarm Information spreadsheet.""" + print(f"\nImporting jobs from: {filepath}") + + try: + df = pd.read_excel(filepath, sheet_name='Sheet1') + print(f"Found {len(df)} rows") + except Exception as e: + print(f"Error reading file: {e}") + return + + with app.app_context(): + imported = 0 + for idx, row in df.iterrows(): + job_number = parse_str(row.get('JOB NUMBER')) + if not job_number: + print(f" Skipping row {idx}: No job number") + continue + + # Check if job already exists + existing = Job.query.filter_by(job_number=job_number).first() + if existing: + print(f" Skipping {job_number}: Already exists") + continue + + job = Job( + job_number=job_number, + job_name=parse_str(row.get('JOB NAME')) or f"Job {job_number}", + location=parse_str(row.get('LOCATION')), + percent_complete=parse_float(row.get('% COMPLETE'), 0.0), + est_starting_qtr=parse_str(row.get('Est. Starting Qtr')), + fire_alarm_budget=parse_float(row.get('FIRE ALARM BUDGET - VENDOR BID APPROVAL')), + labor_estimate=parse_float(row.get('LABOR ESTIMATE')), + material_estimate=parse_float(row.get('MATERIAL ESTIMATE')), + amount_left_on_contract=parse_float(row.get('AMOUNT LEFT ON CONTRACT')), + pm_assigned=parse_str(row.get('PM ASSIGNED')), + aor=parse_str(row.get('AOR')), + fire_vendor=parse_str(row.get('FIRE VENDOR')), + install_partner=parse_str(row.get('INSTALL PARTNER TEAM UP WITH RER')), + ps_or_install=parse_str(row.get('P/S OR INSTALL')), + voip_or_phone=parse_str(row.get('VOIP OR PHONE LINE')), + plans=parse_str(row.get('Plans')), + notes=parse_str(row.get('NOTES')), + issues=parse_str(row.get('ISSUES')), + milestone_1=parse_str(row.get('1ST MILESTONE')), + milestone_2=parse_str(row.get('2ND MILESTONE')), + milestone_3=parse_str(row.get('3RD MILESTONE')), + milestone_4=parse_str(row.get('4TH MILESTONE')), + milestone_5=parse_str(row.get('5TH MILESTONE')), + milestone_6=parse_str(row.get('6TH MILESTONE')), + milestone_7=parse_str(row.get('7TH MILESTONE')), + elevator_final=parse_date(row.get('ELEVATOR FINAL')), + pretest=parse_date(row.get('PRETEST')), + final_date=parse_date(row.get('FINAL')), + co_drop_dead_date=parse_date(row.get('C/O DROP DEAD DATE')), + number_of_units=parse_int(row.get('NUMBER OF UNITS')), + sep_club_house=parse_str(row.get('SEP CLUB HOUSE - FIRE ALARM')), + ) + + db.session.add(job) + imported += 1 + print(f" Imported: {job_number} - {job.job_name}") + + db.session.commit() + print(f"\nImported {imported} jobs") + + +def import_schedule_data(app, filepath): + """Import schedule/phase data from the schedule spreadsheet.""" + print(f"\nImporting schedule data from: {filepath}") + + try: + xl = pd.ExcelFile(filepath) + print(f"Found sheets: {xl.sheet_names}") + except Exception as e: + print(f"Error reading file: {e}") + return + + with app.app_context(): + # Read Schedule sheet + try: + schedule_df = pd.read_excel(xl, sheet_name='Sechdule') + print(f"\nProcessing Schedule sheet with {len(schedule_df)} rows") + + # The first row contains job info, remaining rows contain phase details + if len(schedule_df) > 0: + # Get job info from first row + first_row = schedule_df.iloc[0] + job_number = parse_str(first_row.get('Job #')) + + if job_number: + job = Job.query.filter_by(job_number=job_number).first() + if not job: + # Create the job if it doesn't exist + job = Job( + job_number=job_number, + job_name=parse_str(first_row.get('Job Name')) or f"Job {job_number}", + pm_assigned=parse_str(first_row.get('PM RAR')), + subcontractor=parse_str(first_row.get('Subcontractor')), + pci=parse_str(first_row.get('PCI')), + ) + db.session.add(job) + db.session.commit() + print(f" Created job: {job_number}") + else: + # Update job info + if not job.subcontractor: + job.subcontractor = parse_str(first_row.get('Subcontractor')) + if not job.pci: + job.pci = parse_str(first_row.get('PCI')) + db.session.commit() + print(f" Updated job: {job_number}") + + # Import phases from the schedule + phase_types = ['Rough-in', 'Trim', 'Commissioning', 'Final', 'Turnover'] + phase_type_map = { + 'Rough-in': 'rough_in', + 'Trim': 'trim', + 'Commissioning': 'commissioning', + 'Final': 'final', + 'Turnover': 'turnover' + } + + phases_imported = 0 + for phase_type in phase_types: + for phase_num in range(1, 51): + col_name = f"{phase_type} Phase {phase_num}" + if col_name in schedule_df.columns: + # Check if we have points data for this phase (row index 1) + if len(schedule_df) > 1: + points_row = schedule_df.iloc[1] + points = parse_int(points_row.get(col_name)) + + if points and points > 0: + # Check if phase already exists + existing = Phase.query.filter_by( + job_id=job.id, + phase_type=phase_type_map[phase_type], + phase_number=phase_num + ).first() + + if not existing: + phase = Phase( + job_id=job.id, + phase_type=phase_type_map[phase_type], + phase_number=phase_num, + points=points, + ) + + # Try to get start/due dates from other rows + if len(schedule_df) > 3: + start_row = schedule_df.iloc[3] + phase.start_date = parse_date(start_row.get(col_name)) + if len(schedule_df) > 4: + due_row = schedule_df.iloc[4] + phase.due_date = parse_date(due_row.get(col_name)) + if len(schedule_df) > 5: + men_row = schedule_df.iloc[5] + phase.men_on_site = parse_int(men_row.get(col_name)) + if len(schedule_df) > 6: + completed_row = schedule_df.iloc[6] + completed_val = completed_row.get(col_name) + phase.completed = completed_val == True or str(completed_val).lower() == 'true' + + db.session.add(phase) + phases_imported += 1 + + db.session.commit() + print(f" Imported {phases_imported} phases for job {job_number}") + + except Exception as e: + print(f"Error processing Schedule sheet: {e}") + + +def main(): + print("="*50) + print("Fire Alarm Data Import") + print("="*50) + + app = create_app() + + # File paths + fire_alarm_file = '/root/code/romanoff/Raleigh jobs FIRE ALARM INFORMATION.xlsx' + schedule_file = '/root/code/romanoff/schedule_updated.xlsm' + + # Import jobs from fire alarm spreadsheet + if os.path.exists(fire_alarm_file): + import_fire_alarm_jobs(app, fire_alarm_file) + else: + print(f"File not found: {fire_alarm_file}") + + # Import schedule data + if os.path.exists(schedule_file): + import_schedule_data(app, schedule_file) + else: + print(f"File not found: {schedule_file}") + + print("\n" + "="*50) + print("Import complete!") + print("="*50) + + +if __name__ == '__main__': + main() diff --git a/instance/fire_alarm.db b/instance/fire_alarm.db new file mode 100644 index 0000000000000000000000000000000000000000..a23856cc89d65a7a9043bcba9c3e3982642d74b6 GIT binary patch literal 20480 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3393e1d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask>=3.0.0 +flask-sqlalchemy>=3.1.0 +pandas>=2.0.0 +openpyxl>=3.1.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000..133191d --- /dev/null +++ b/run.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +Fire Alarm Management Application +Run this file to start the web server. +""" + +from app import create_app + +app = create_app() + +if __name__ == '__main__': + print("\n" + "="*50) + print("Fire Alarm Management System") + print("="*50) + print("\nStarting server...") + print("Open http://localhost:5000 in your browser") + print("\nPress Ctrl+C to stop the server") + print("="*50 + "\n") + + app.run(debug=True, host='0.0.0.0', port=5000) diff --git a/schedule_updated.xlsm b/schedule_updated.xlsm new file mode 100644 index 0000000000000000000000000000000000000000..933f293c0a3bce09774e05265bff5194c9ed56e5 GIT binary patch literal 39881 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 =yk=9|#XQ+ZJ{H_d&Y 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