# 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 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= 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= ``` > **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.