293 lines
8.1 KiB
Markdown
293 lines
8.1 KiB
Markdown
# JQC — Janitorial Quality Control System
|
||
|
||
A production-grade web application for managing janitorial service contracts, facility inspections, issue tracking, and client reporting.
|
||
|
||
---
|
||
|
||
## Features
|
||
|
||
- **Inspection Management** — Execute structured inspections against configurable templates with a drag-and-drop form builder supporting ratings, pass/fail, photos, signatures, and free-form fields
|
||
- **Issue Tracking** — Full lifecycle management (open → in-progress → pending verification → resolved) with SLA enforcement, follower subscriptions, and resolution photo uploads
|
||
- **Customer Portal** — Scoped facility visibility for client accounts with invitation-based onboarding (email link, 72-hour token)
|
||
- **Notification System** — In-app + email notifications driven by an admin-controlled routing matrix; per-user preferences and digest mode
|
||
- **Reports** — On-demand PDF/CSV scorecards and scheduled recurring email reports (daily/weekly/monthly)
|
||
- **Audit Trail** — Immutable log of every create, update, and delete action with actor and IP capture
|
||
- **Mobile API** — JWT-authenticated REST API for the companion React Native / Expo mobile application
|
||
- **Project Hierarchy** — Facilities grouped into Projects with optional Project Manager assignment and per-project customer access control
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
|
||
| Layer | Technology |
|
||
|---|---|
|
||
| Backend | Python / Flask |
|
||
| Database | MySQL |
|
||
| ORM / Migrations | SQLAlchemy + Alembic |
|
||
| Auth (web) | Flask-Login + Flask-WTF (CSRF) |
|
||
| Auth (API) | JWT + opaque refresh tokens |
|
||
| Email | Flask-Mail (SMTP) |
|
||
| PDF | ReportLab |
|
||
| Frontend | Bootstrap 5, Chart.js, Jinja2 |
|
||
| Server | Gunicorn + Nginx |
|
||
| Mobile | React Native + Expo |
|
||
|
||
---
|
||
|
||
## Prerequisites
|
||
|
||
- Python 3.11+
|
||
- MySQL 8.x
|
||
- A configured SMTP server (port 465 or 587)
|
||
|
||
---
|
||
|
||
## Installation
|
||
|
||
### 1. Clone and create a virtual environment
|
||
|
||
```bash
|
||
git clone <repo-url> jqc
|
||
cd jqc
|
||
python -m venv venv
|
||
source venv/bin/activate # Linux / macOS
|
||
# venv\Scripts\activate # Windows
|
||
```
|
||
|
||
### 2. Install dependencies
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
### 3. Create the database
|
||
|
||
```sql
|
||
CREATE DATABASE jqc CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
CREATE USER 'jqc'@'localhost' IDENTIFIED BY 'your_password';
|
||
GRANT ALL PRIVILEGES ON jqc.* TO 'jqc'@'localhost';
|
||
FLUSH PRIVILEGES;
|
||
```
|
||
|
||
### 4. Configure environment variables
|
||
|
||
Create a `.env` file in the project root (never commit this file):
|
||
|
||
```dotenv
|
||
SECRET_KEY=<long-random-string>
|
||
DATABASE_URL=mysql+pymysql://jqc:your_password@localhost/jqc
|
||
|
||
# Email
|
||
MAIL_SERVER=smtp.example.com
|
||
MAIL_PORT=587
|
||
MAIL_USERNAME=noreply@example.com
|
||
MAIL_PASSWORD=smtp_password
|
||
MAIL_DEFAULT_SENDER=noreply@example.com
|
||
|
||
# Application
|
||
APP_BASE_URL=https://your-domain.com
|
||
DIGEST_SECRET=<random-secret-for-cron-auth>
|
||
```
|
||
|
||
> **Email SSL:** Port 465 uses implicit SSL (`MAIL_USE_SSL=True`). Port 587 uses STARTTLS (`MAIL_USE_TLS=True`). The application auto-detects based on `MAIL_PORT` — never set both flags to True.
|
||
|
||
### 5. Run database migrations
|
||
|
||
```bash
|
||
flask db upgrade
|
||
```
|
||
|
||
### 6. Start the development server
|
||
|
||
```bash
|
||
python run.py
|
||
```
|
||
|
||
The application will be available at `http://localhost:5000`.
|
||
|
||
---
|
||
|
||
## Production Deployment
|
||
|
||
### Gunicorn
|
||
|
||
```bash
|
||
gunicorn -c gunicorn_config.py wsgi:app
|
||
```
|
||
|
||
The included `gunicorn_config.py` binds to `127.0.0.1:8000` with sync workers. Log files are written to `/home/jqc/logs/`.
|
||
|
||
### Nginx (recommended configuration)
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl;
|
||
server_name your-domain.com;
|
||
|
||
client_max_body_size 50M; # must match MAX_CONTENT_LENGTH in config.py
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8000;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
|
||
location /static/ {
|
||
alias /path/to/jqc/app/static/;
|
||
expires 30d;
|
||
}
|
||
}
|
||
```
|
||
|
||
### Systemd service (example)
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=JQC Gunicorn
|
||
After=network.target mysql.service
|
||
|
||
[Service]
|
||
User=jqc
|
||
WorkingDirectory=/home/jqc/lt_janitorial_quality_control
|
||
EnvironmentFile=/home/jqc/.env
|
||
ExecStart=/home/jqc/venv/bin/gunicorn -c gunicorn_config.py wsgi:app
|
||
Restart=on-failure
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
---
|
||
|
||
## Cron Jobs
|
||
|
||
Two background tasks require scheduled execution:
|
||
|
||
### SLA Alerts
|
||
|
||
Checks all open issues against their SLA window and dispatches notifications:
|
||
|
||
```bash
|
||
# Run every 30 minutes
|
||
*/30 * * * * /home/jqc/venv/bin/python -c "
|
||
from app import create_app
|
||
from app.utils.sla import send_sla_alerts
|
||
app = create_app('production')
|
||
with app.app_context():
|
||
send_sla_alerts()
|
||
"
|
||
```
|
||
|
||
### Digest Emails and Scheduled Reports
|
||
|
||
```bash
|
||
# Daily digest — run at 7:00 AM
|
||
0 7 * * * curl -s -X POST "https://your-domain.com/notifications/send-digest?frequency=daily&secret=YOUR_DIGEST_SECRET"
|
||
|
||
# Scheduled reports
|
||
0 8 * * * curl -s -X POST "https://your-domain.com/scheduled-reports/run?secret=YOUR_DIGEST_SECRET"
|
||
```
|
||
|
||
---
|
||
|
||
## User Roles
|
||
|
||
| Role | Description |
|
||
|---|---|
|
||
| **admin** | Full access to all features including Audit Trail and Notification Matrix |
|
||
| **director** | Broad access equivalent to admin, excluding Audit Trail and Notification Matrix |
|
||
| **project_manager** | Manages projects, facilities, and reports; cannot manage users or system settings |
|
||
| **inspector** | Executes inspections and manages assigned issues |
|
||
| **customer** | Read-only portal scoped to assigned facilities; receives notifications on their facilities |
|
||
|
||
### Creating the First Admin Account
|
||
|
||
```bash
|
||
flask shell
|
||
>>> from app import db
|
||
>>> from app.models.user import User
|
||
>>> u = User(username='admin', email='admin@example.com', role='admin')
|
||
>>> u.set_password('your-secure-password')
|
||
>>> db.session.add(u)
|
||
>>> db.session.commit()
|
||
```
|
||
|
||
---
|
||
|
||
## Customer Onboarding
|
||
|
||
1. Admin navigates to **Customers → Invite Customer**
|
||
2. Enter the customer's name and email address
|
||
3. The system auto-generates a username and sends an invitation email with a 72-hour setup link
|
||
4. Customer clicks the link, sets their password, and gains access to their scoped portal
|
||
5. Admin assigns the customer to one or more Projects/Facilities via **Customers → Manage Assignments**
|
||
|
||
---
|
||
|
||
## Mobile API
|
||
|
||
The REST API is available at `/api/v1/` and uses JWT Bearer token authentication.
|
||
|
||
### Authentication Endpoints
|
||
|
||
| Method | Endpoint | Description |
|
||
|---|---|---|
|
||
| POST | `/api/v1/auth/login` | Login; returns access + refresh tokens |
|
||
| POST | `/api/v1/auth/refresh` | Rotate refresh token; returns new access token |
|
||
| POST | `/api/v1/auth/logout` | Revoke refresh token |
|
||
| GET | `/api/v1/auth/me` | Return current user profile |
|
||
| POST | `/api/v1/devices/register` | Register APNs device token for push notifications |
|
||
|
||
Access tokens expire after 60 minutes. Refresh tokens are valid for 30 days and rotate on every use.
|
||
|
||
---
|
||
|
||
## Database Migrations
|
||
|
||
```bash
|
||
# Apply all pending migrations
|
||
flask db upgrade
|
||
|
||
# Create a new migration after model changes
|
||
flask db migrate -m "description of change"
|
||
|
||
# Roll back one migration
|
||
flask db downgrade
|
||
```
|
||
|
||
> **MySQL ENUM changes require three steps:** expand the ENUM to include both values, migrate existing data, then contract the ENUM. See `Claude.md` for the full protocol.
|
||
|
||
---
|
||
|
||
## Application Logs
|
||
|
||
| Log File | Contents |
|
||
|---|---|
|
||
| `logs/jqc.log` | Application log (rotating, 5 × 5 MB) |
|
||
| `/home/jqc/logs/gunicorn-error.log` | Gunicorn worker errors |
|
||
| `/home/jqc/logs/gunicorn-access.log` | HTTP access log |
|
||
|
||
---
|
||
|
||
## Configuration Reference
|
||
|
||
| Variable | Default | Description |
|
||
|---|---|---|
|
||
| `SECRET_KEY` | — *required* | Flask session signing key |
|
||
| `DATABASE_URL` | — *required* | SQLAlchemy connection URI |
|
||
| `MAIL_SERVER` | — | SMTP hostname |
|
||
| `MAIL_PORT` | `587` | SMTP port (465 = SSL, 587 = STARTTLS) |
|
||
| `MAIL_USERNAME` | — | SMTP username |
|
||
| `MAIL_PASSWORD` | — | SMTP password |
|
||
| `MAIL_DEFAULT_SENDER` | `noreply@janitorialqc.local` | From address |
|
||
| `APP_BASE_URL` | `""` | Base URL for links in emails |
|
||
| `DIGEST_SECRET` | — | Token for authenticating cron requests |
|
||
| `MAX_CONTENT_LENGTH` | `50MB` | Maximum upload size per request |
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
See `LICENSE` for terms. |