commit 892ac3d23b5bfc7f8bc6a8a6ae5b3970e820d156 Author: pnannery Date: Mon Jan 19 21:57:25 2026 -0500 Initial commit - Fire alarm management application Co-Authored-By: Claude Opus 4.5 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 0000000..a23856c Binary files /dev/null and b/instance/fire_alarm.db differ 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 0000000..933f293 Binary files /dev/null and b/schedule_updated.xlsm differ