Skip to content

Repository files navigation

PropCare — Property Maintenance Management System

A full-stack web application for managing property maintenance requests across three user roles: Tenants, Property Managers, and Technicians.


Demo Credentials

Role Email Password
Property Manager manager@propcare.demo Demo@123
Tenant tenant1@propcare.demo Demo@123
Tenant tenant2@propcare.demo Demo@123
Technician tech1@propcare.demo Demo@123
Technician tech2@propcare.demo Demo@123

Run npm run seed in the server directory to populate demo data.


Problem Overview

Property managers handle dozens of maintenance requests daily — leaking pipes, broken ACs, faulty wiring. Without a system, requests get lost in text messages, tenants don't know the status, and technicians lack clear assignments.

PropCare solves this by providing a structured workflow where tenants submit tickets, managers triage and assign them, and technicians update progress — all with full audit trails and notifications.


Scope

This system was scoped as a focused development effort. The priority is correctness, clarity, and architectural soundness — not feature sprawl. Every feature included is functional end-to-end, from database schema to API to UI.


Architecture Overview

┌──────────────────┐       ┌──────────────────┐       ┌──────────────────┐
│   React Client   │──────▶│  Express API     │──────▶│   PostgreSQL     │
│   (Vite, :5173)  │ HTTP  │  (Node.js, :3001)│Prisma │   (Port 5432)    │
└──────────────────┘       └──────────────────┘       └──────────────────┘
  • Client → Server: REST API calls over HTTP with JWT in Authorization header
  • Server → DB: Prisma ORM with type-safe queries, migrations, and seed scripts
  • File uploads: Stored on disk via Multer, served as static assets at /uploads/
  • Auth flow: POST /api/auth/login → JWT issued → all subsequent requests carry the token

Project Structure

PropCare/
├── server/                     # Backend API
│   ├── prisma/
│   │   ├── schema.prisma       # Database schema (5 models, 4 enums)
│   │   └── seed.ts             # Demo data seeder
│   └── src/
│       ├── config/             # Environment variables, CORS config
│       ├── middleware/         # auth, role, upload, validate, errorHandler
│       ├── modules/
│       │   ├── auth/           # Register, login, JWT issuance
│       │   ├── user/           # User listing, technician lookup
│       │   ├── ticket/         # CRUD, status transitions, assignment
│       │   ├── activity/       # Per-ticket audit trail
│       │   └── notification/   # In-app notification system
│       ├── utils/              # JWT helpers, password hashing, response format
│       └── index.ts            # Express app entry point
├── client/                     # Frontend SPA
│   └── src/
│       ├── api/                # Axios instance + endpoint wrappers
│       ├── context/            # AuthContext (login state management)
│       ├── guards/             # ProtectedRoute (redirect if unauthenticated)
│       ├── components/
│       │   ├── layout/         # AppShell (sidebar, mobile nav, notification badge)
│       │   └── tickets/        # TicketCard
│       ├── pages/              # Login, Dashboard, Tickets, NewTicket, TicketDetail, Notifications
│       └── styles/             # Global CSS (dark theme design system)
├── docker-compose.yml
├── Dockerfile.server
└── Dockerfile.client

Tech Stack

Layer Technology Version Purpose
Runtime Node.js 18+ Server runtime
Backend Framework Express 5.x REST API routing and middleware
Language TypeScript 5.9 Type safety across the backend
ORM Prisma 6.16 Schema management, migrations, type-safe DB queries
Database PostgreSQL 15+ Relational data storage
Auth jsonwebtoken 9.x JWT token issuance and verification
Password Hashing bcryptjs 3.x Secure password storage
Validation Zod 4.x Request body/params validation
File Upload Multer 2.x Disk-based image upload handling
Frontend React 19.x Component-based UI
Build Tool Vite 7.x Fast HMR and production bundling
HTTP Client Axios 1.x API calls with interceptors
Routing React Router 7.x Client-side navigation
Icons Lucide React 0.563 Consistent icon set
CSS Vanilla CSS Custom dark theme design system (no framework)

Role-Based Access Control (RBAC)

Every API route is protected by two middleware layers:

  1. authenticate — Verifies the JWT token from the Authorization: Bearer <token> header. Attaches { userId, role } to req.user.
  2. authorize(...roles) — Checks if req.user.role is in the allowed list. Returns 403 if not.

Permission Matrix

Action Tenant Manager Technician
Submit ticket
View own tickets
View all tickets
View assigned tickets
Assign technician
Change priority
Update ticket status
View dashboard stats Own tickets All tickets Assigned tasks
Receive notifications Ticket updates New tickets, status changes New assignments

How it works in code

// Routes are protected by chaining middleware:
router.post(
  "/",
  authenticate,
  authorize("TENANT"),
  upload.array("images", 5),
  validate(createTicketSchema),
  create,
);
router.patch(
  "/:id/assign",
  authenticate,
  authorize("MANAGER"),
  validate(assignSchema),
  assign,
);

Ticket Workflow

Tickets follow a strict state machine. Invalid transitions are rejected server-side.

┌──────┐    assign    ┌──────────┐   start work   ┌─────────────┐   complete   ┌──────┐
│ OPEN │─────────────▶│ ASSIGNED │───────────────▶│ IN_PROGRESS │────────────▶│ DONE │
└──────┘              └──────────┘                └─────────────┘             └──────┘
                         │    ▲                        │    ▲
                         │    │      reassign          │    │
                         │    └────────────────────────┘    │
                         │         unassign                  │
                         └──────────────────────────────────┘
                              (back to OPEN)

Valid transitions enforced in ticket.service.ts:

const VALID_TRANSITIONS: Record<TicketStatus, TicketStatus[]> = {
  OPEN: ["ASSIGNED"],
  ASSIGNED: ["IN_PROGRESS", "OPEN"],
  IN_PROGRESS: ["DONE", "ASSIGNED"],
  DONE: [],
};

What happens on each transition:

  • OPEN → ASSIGNED: Manager assigns a technician. Technician receives notification.
  • ASSIGNED → IN_PROGRESS: Technician starts work. Tenant + managers notified.
  • IN_PROGRESS → DONE: Technician marks complete. Tenant + managers notified.
  • ASSIGNED → OPEN / IN_PROGRESS → ASSIGNED: Reassignment/unassignment paths.

Every transition creates an Activity record for the audit trail.


Key Features

Tenant

  • Submit maintenance tickets with title, description, category, building, unit, priority
  • Upload up to 5 images per ticket (JPEG, PNG, WebP — max 5MB each)
  • Track ticket status in real-time
  • Receive notifications when status changes

Property Manager

  • Dashboard with ticket counts by status (Open, Assigned, In Progress, Done)
  • Filter tickets by status and priority
  • Assign technicians to tickets
  • Change ticket priority (Low / Medium / High / Urgent)
  • View full activity timeline per ticket

Technician

  • View only assigned tasks
  • Update status: Start Work (→ In Progress), Mark Complete (→ Done)
  • Receive notifications on new assignments

System-Wide

  • JWT authentication with 24-hour token expiry
  • In-app notification bell with unread count (polls every 30 seconds)
  • Activity log per ticket (who did what, when)
  • Mobile-first responsive UI with dark theme
  • Role-specific accent colors (Blue: Tenant, Purple: Manager, Green: Technician)
  • Demo account quick-fill on the login page

Database Design

Entity Relationship

User (1) ──────▶ (N) Ticket (as creator)
User (1) ──────▶ (N) Ticket (as assignee, optional)
User (1) ──────▶ (N) Activity (as performer)
User (1) ──────▶ (N) Notification

Ticket (1) ────▶ (N) TicketImage
Ticket (1) ────▶ (N) Activity

Models

Model Key Fields Notes
User email (unique), password (hashed), name, role, phone? Indexed on role
Ticket title, description, status, priority, category?, building?, unit? Indexed on status, creatorId, assigneeId
TicketImage url, filename, mimeType, size Cascades on ticket delete
Activity type, message, metadata (JSON) Audit trail — cascades on ticket delete
Notification title, message, read, link? Indexed on [userId, read] for fast unread queries

Design Decisions

  • UUIDs as primary keys — avoids sequential ID guessing, works well in distributed setups
  • Composite index [userId, read] on notifications — optimizes the most frequent query (fetching unread notifications for a user)
  • JSON metadata on activities — flexible key-value storage for transition details without adding columns
  • Cascade deletes on images, activities, notifications — when a ticket is deleted, all related records are cleaned up

Setup Instructions

Prerequisites

  • Node.js 18+
  • PostgreSQL 15+ (running locally or via Docker)

1. Database Setup

# Option A: Local PostgreSQL
psql -U postgres -c "CREATE USER propcare WITH PASSWORD 'propcare123' CREATEDB;"
psql -U postgres -c "CREATE DATABASE propcare_db OWNER propcare;"

# Option B: Docker
docker run -d --name propcare-db \
  -e POSTGRES_USER=propcare \
  -e POSTGRES_PASSWORD=propcare123 \
  -e POSTGRES_DB=propcare_db \
  -p 5432:5432 postgres:15-alpine

2. Backend

cd server
cp .env.example .env          # Edit DATABASE_URL if needed
npm install
npx prisma migrate dev --name init
npm run seed                   # Populates demo users + tickets
npm run dev                    # Starts on http://localhost:3001

3. Frontend

cd client
npm install
npm run dev                    # Starts on http://localhost:5173

4. Open Browser

Navigate to http://localhost:5173 and log in with a demo account.

Docker (Full Stack)

docker-compose up --build
# Frontend: http://localhost:5173
# Backend:  http://localhost:3001

Environment Variables

Variable Default Description
DATABASE_URL postgresql://propcare:propcare123@localhost:5432/propcare_db PostgreSQL connection string
JWT_SECRET propcare-super-secret-... JWT signing secret
PORT 3001 API server port
UPLOAD_DIR ./uploads Directory for uploaded images
MAX_FILE_SIZE 5242880 Max upload size in bytes (5MB)
CORS_ORIGIN http://localhost:5173 Allowed CORS origin

Testing

Automated Tests

The backend includes integration tests covering auth, CRUD, the full ticket workflow, authorization, and notifications:

cd server
npm test        # Runs 19 integration tests via Jest + Supertest

Test coverage:

Suite Tests What's verified
Auth 5 Login (valid/invalid), registration forced to TENANT, /me auth + unauth
Tickets 5 Tenant creates, manager can't create, tenant sees only own, manager sees all, tech can't see unassigned
Workflow 7 Assign → In Progress → Done, invalid transition rejected, DONE is terminal, audit trail, priority change
Notifications 2 Fetch notifications, mark all as read

API Verification

# Health check
curl http://localhost:3001/api/health

# Login
curl -X POST http://localhost:3001/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"manager@propcare.demo","password":"Demo@123"}'

# List tickets (use token from login response)
curl http://localhost:3001/api/tickets \
  -H "Authorization: Bearer <token>"

# Dashboard stats
curl http://localhost:3001/api/tickets/dashboard/stats \
  -H "Authorization: Bearer <token>"

Build Verification

# Frontend
cd client && npm run build     # Vite production build

# Backend
cd server && npx tsc --noEmit  # TypeScript type checking

Design Decisions

Decision Rationale
Express 5 over Nest/Fastify Minimal overhead, direct control, widely understood — appropriate for the scope
Prisma over raw SQL/Sequelize Type-safe queries generated from schema, auto-migrations, clean relation syntax
Zod for validation Composable schemas, TypeScript-native inference, better error messages than Joi
JWT over sessions Stateless auth — no server-side session store needed, works cleanly with REST API
Multer with local disk Simple file handling for the current scale; easily swappable to S3 via storage engine
Vanilla CSS over Tailwind Full control over the dark theme design system, no build-time dependency
Polling for notifications Simpler than WebSockets for the current scope; 30s interval is acceptable latency
State machine for tickets Prevents invalid transitions (e.g., OPEN → DONE), enforced server-side
Modular folder structure Each module (auth, ticket, etc.) is self-contained with its own routes, controller, service, and schema

Future Improvements

  • Real-time updates: Replace notification polling with WebSocket/SSE for instant pushes
  • Comments on tickets: Allow tenants and technicians to communicate directly on a ticket
  • Email/SMS notifications: Integrate Nodemailer or Twilio for external notifications
  • Image storage on S3: Replace local disk uploads with AWS S3 or Cloudinary
  • Pagination: Add cursor-based pagination for ticket lists at scale
  • Search: Full-text search on ticket title/description using PostgreSQL tsvector
  • Analytics dashboard: Avg resolution time, tickets per building, technician workload
  • Mobile app: React Native client reusing the same API layer
  • CI/CD pipeline: GitHub Actions for lint, test, build, and deploy

Alignment with Evaluation Criteria

This implementation focuses on:

  • Backend architecture through modular layering and service-driven logic
  • Database integrity via explicit schema constraints, indexes, transactions, and cascade rules
  • Auth & role management with strict middleware enforcement, rate limiting on auth routes, and registration restricted to tenants
  • Workflow logic via a validated state machine with atomic transitions and audit trails
  • Clean, usable UI optimized for role-specific workflows
  • Code quality with TypeScript, Zod validation, and 19 integration tests covering auth, CRUD, workflow, and notifications
  • Clear deployment and documentation practices with Docker, healthchecks, and environment isolation

Why This Approach

The system is intentionally built with a thin, modular Express API rather than a monolithic framework. Each domain concept (tickets, auth, notifications) is isolated in its own module with a clear service → controller → route boundary. This makes it straightforward to:

  • Test in isolation — each service function can be unit tested without HTTP concerns
  • Swap implementations — replacing Multer's local storage with S3 requires changing one file
  • Onboard developers — the folder structure maps directly to the domain language

The state machine pattern for ticket status is a deliberate choice over a simple status update endpoint. By defining valid transitions explicitly, the system prevents impossible states (a ticket can't go from OPEN to DONE without assignment and work) — this is enforced at the service layer, not just the UI.

The notification system uses database-backed polling rather than WebSockets. For a property management context where latency of a few seconds is acceptable, this avoids the complexity of persistent connections and works reliably behind load balancers and proxies.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages