Skip to content

M2 — Maintenance Management System

M2 is a Computerised Maintenance Management System (CMMS) built for the Inspiring Living Solutions (ILS) property management platform. It handles work orders, asset tracking, preventive maintenance scheduling, vendor management, inventory, purchase orders, and emergency response for villa properties.

  • Production URL: https://m2.inspiringlivingsolutions.com
  • Company: Inspiring Living Solutions / Inspiring Group (UAE)
  • Codebase: Next.js full-stack monolith (API + frontend in one app)

M2 Dashboard overview - Part 1

M2 Dashboard overview - Part 2

M2 Dashboard overview - Part 3


graph TD
Browser["Browser / PWA"]
NextApp["M2 Next.js App\nm2.inspiringlivingsolutions.com"]
AuthJS["NextAuth v5\nJWT Session"]
Authentik["Authentik SSO\nsso.inspiringroup.com"]
AzureAD["Microsoft Azure AD\nIdentity Provider"]
Bitrix["Bitrix24\nAlternate Auth"]
Postgres["PostgreSQL\nPrisma ORM"]
GMaps["Google Maps API\nGeocoding / Places"]
Sentry["Sentry\nsentry.dabz.me\nError Monitoring"]
Browser -->|HTTPS| NextApp
NextApp -->|JWT verify| AuthJS
AuthJS -->|OIDC| Authentik
Authentik -->|OAuth| AzureAD
AuthJS -->|OAuth| Bitrix
NextApp -->|Prisma| Postgres
NextApp -->|JS API Loader| GMaps
NextApp -->|SDK| Sentry
graph LR
OBV["OBV (M1)\nOnboarding Wizard\nobv.inspiringlivingsolutions.com"]
M2["M2\nMaintenance CMMS\nm2.inspiringlivingsolutions.com"]
M3["M3\nInventory\nm3.inspiringlivingsolutions.com"]
M4["M4\nReporting"]
M7["M7\nHardware Assets\nm7.inspiringlivingsolutions.com"]
SSO["Authentik SSO\nShared Identity Provider"]
AzureAD["Microsoft Azure AD"]
SSO -->|OIDC| OBV
SSO -->|OIDC| M2
SSO -->|OIDC| M3
SSO -->|OIDC| M4
SSO -->|OIDC| M7
AzureAD -->|IdP| SSO

All ILS microservices share the same Authentik SSO instance. M2 does not share its database with any other service — each module maintains its own PostgreSQL instance.


TechnologyVersionPurpose
Node.js22+Runtime
TypeScript5.9.3Language
Next.js16.1.6Full-stack framework (App Router)
React19UI library
ToolVersionPurpose
Yarn1.22.22Package manager
Vite7.3.0Build tooling / Vitest runner
PostCSS8.5.6CSS transformation
TechnologyVersionPurpose
PostgreSQL16Primary database
Prisma7.3.0ORM + migrations
pg8.16.3PostgreSQL client adapter
LibraryVersionPurpose
Tailwind CSS4Utility-first styling
shadcn/uiLatestComponent library (Sky/Zinc theme)
Radix UIVariousHeadless UI primitives
React Hook Form7.69.0Form state management
Zod4.3.4Schema validation
Zustand5.0.9Global state management
TanStack React Table8.21.3Data tables
React Big Calendar1.19.4PMP schedule calendar
Sonner2.0.7Toast notifications
next-intl4.6.1i18n (English + Thai)
next-themes0.4.6Dark/light mode
Lucide React0.562.0Icons
LibraryVersionPurpose
NextAuthv5 beta 30Auth framework
Authentik (OIDC)Primary SSO provider
Bitrix24 (OAuth)Secondary login provider
ToolVersionPurpose
Sentry (@sentry/nextjs)10.32.1Error monitoring
Vitest4.0.16Unit + integration tests
@testing-library/react16.3.1Component tests
@vitest/coverage-v84.0.18Test coverage

m2-main/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── api/ # All REST API route handlers
│ │ │ ├── assets/
│ │ │ ├── auth/ # NextAuth + Bitrix OAuth routes
│ │ │ ├── emergency-contacts/
│ │ │ ├── incidents/
│ │ │ ├── inventory/
│ │ │ ├── parts/
│ │ │ ├── pmp-schedules/
│ │ │ ├── pmp-templates/
│ │ │ ├── purchase-orders/
│ │ │ ├── requests/
│ │ │ ├── response-teams/
│ │ │ ├── staff/
│ │ │ ├── teams/
│ │ │ ├── vendors/
│ │ │ ├── villa-locations/
│ │ │ ├── villas/
│ │ │ ├── work-orders/
│ │ │ └── zones/
│ │ ├── assets/ # Asset management pages
│ │ ├── emergency/ # Emergency management pages
│ │ ├── inventory/ # Inventory pages
│ │ ├── locations/ # Villa location pages
│ │ ├── login/ # Login page
│ │ ├── parts/ # Parts management pages
│ │ ├── pmp/ # PMP calendar + templates
│ │ ├── purchase-orders/ # Purchase order pages
│ │ ├── reports/ # Analytics & reporting
│ │ ├── requests/ # Maintenance request pages
│ │ ├── settings/ # Settings page
│ │ ├── staff/ # Staff & team management
│ │ ├── vendors/ # Vendor management
│ │ ├── villas/ # Villa management + map view
│ │ ├── work-orders/ # Work order pages
│ │ ├── layout.tsx # Root layout (Sentry, theme, i18n)
│ │ └── page.tsx # Dashboard
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── analytics/ # Dashboard charts
│ │ ├── assets/ # Asset form, list, detail components
│ │ ├── emergency/ # Incident form, map, contacts
│ │ ├── locations/ # Location management
│ │ ├── maps/ # Google Maps wrappers
│ │ ├── parts/ # Parts & inventory components
│ │ ├── pmp/ # PMP calendar, templates
│ │ ├── purchase-orders/ # PO form, detail components
│ │ ├── requests/ # Request form, detail
│ │ ├── staff/ # Staff list, profile, team
│ │ ├── vendors/ # Vendor list, contracts, pricing
│ │ ├── villas/ # Villa form, map, detail
│ │ ├── work-orders/ # WO form, detail, tasks, checklist
│ │ ├── app-header.tsx # Top navigation bar
│ │ ├── app-sidebar.tsx # Side navigation
│ │ ├── cmms-dashboard.tsx # Main dashboard component
│ │ ├── data-table.tsx # Generic reusable table (TanStack)
│ │ ├── sidebar-layout.tsx # Page layout wrapper
│ │ └── status-badge.tsx # Status/priority badge
│ ├── stores/ # Zustand state stores
│ │ ├── work-order-store.ts
│ │ ├── asset-store.ts
│ │ ├── emergency-store.ts
│ │ ├── inventory-store.ts
│ │ ├── parts-store.ts
│ │ ├── purchase-orders-store.ts
│ │ ├── requests-store.ts
│ │ ├── vendor-store.ts
│ │ └── ui-store.ts
│ ├── lib/
│ │ ├── validations/ # Zod schemas per module
│ │ ├── api/ # Client-side API fetch helpers
│ │ ├── utils/ # Shared utilities (dates, labels)
│ │ ├── pmp/ # PMP recurrence helpers
│ │ ├── db.ts # Prisma client singleton
│ │ ├── auth-server.ts # Server-side auth helpers
│ │ ├── auth-client.ts # Client-side auth helpers
│ │ ├── bitrix-user.ts # Bitrix24 user sync
│ │ ├── password.ts # bcrypt helpers
│ │ ├── mock-data.ts # Development/seed data (71 KB)
│ │ └── maintenance-alert.ts # Alert dispatch logic
│ ├── contexts/
│ │ └── auth-context.tsx # React auth context
│ ├── types/
│ │ ├── villa.ts
│ │ └── zone.ts
│ ├── i18n/
│ │ ├── config.ts # Supported locales: en, th
│ │ └── request.ts
│ ├── test/
│ │ ├── setup.ts # Vitest global setup
│ │ └── integration/ # Integration test helpers
│ └── auth.ts # NextAuth config (providers, callbacks)
├── prisma/
│ └── schema.prisma # Full database schema (1327 lines)
├── scripts/ # E2E API test scripts
├── messages/ # i18n translation files (en, th)
├── nginx/ # Nginx reverse proxy config
├── Dockerfile # Production multi-stage build
├── Dockerfile.migrate # Migration-only container
├── docker-compose.yml # Local development
├── docker-compose.prod.yml # Production deployment
├── .env.example # Development env template
├── .env.production.example # Production env template
├── DEPLOYMENT.md # Deployment runbook
└── README.md # Authentik SSO setup guide

VariableExampleDescription
HOSTNAME0.0.0.0Bind address
PORT3000HTTP port
LOG_LEVELinfoLog verbosity (debug, info, warn, error)
NEXT_PUBLIC_SENTRY_DSN(empty)Sentry DSN for client-side error reporting
SENTRY_AUTH_TOKEN(empty)Sentry token for source map upload
AUTH_URLhttp://localhost:3000Full app URL (used by NextAuth)
AUTH_SECRET(generated)NextAuth secret — run openssl rand -base64 32
AUTH_TRUST_HOSTtrueTrust X-Forwarded-Host header
AUTH_AUTHENTIK_ID(empty)Authentik OAuth client ID
AUTH_AUTHENTIK_SECRET(empty)Authentik OAuth client secret
AUTH_AUTHENTIK_ISSUERhttps://sso.inspiringroup.com/application/o/m2/Authentik OIDC issuer URL
NEXT_PUBLIC_AUTH_AUTHENTIK_ISSUER(same)Issuer URL exposed to client
DATABASE_URLprisma+postgres://localhost:51213/?api_key=...Prisma Data Proxy URL for local dev
VariableExampleDescription
NODE_ENVproductionNode environment
POSTGRES_PASSWORDchange_me_strong_passwordPostgreSQL root password
DATABASE_URLpostgresql://postgres:PASSWORD@postgres:5432/m2_cmmsDirect PostgreSQL connection string
AUTH_URLhttps://your-domain.comPublic-facing app URL
AUTH_SECRET(generate)NextAuth secret — never share
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY(optional)Google Maps JS API key

sequenceDiagram
participant User
participant M2 as M2 App
participant NextAuth
participant Authentik
participant AzureAD
participant DB as PostgreSQL
User->>M2: GET /login
M2->>NextAuth: Initiate sign-in
NextAuth->>Authentik: OIDC Authorization Request
Authentik->>AzureAD: Federated login
AzureAD-->>Authentik: Identity token
Authentik-->>NextAuth: ID token + user info
NextAuth->>DB: Upsert user record
NextAuth-->>M2: JWT session cookie
M2-->>User: Redirect to dashboard
Note over M2,DB: Bitrix24 fallback: if no session,<br/>check cookie for Bitrix OAuth token
FunctionFilePurpose
getAuthenticatedUser()lib/auth-server.tsPrimary entry — checks session then Bitrix token
getCurrentUserServer()lib/auth-server.tsReads JWT from cookie
verifyPassword()lib/password.tsbcrypt comparison for credential provider

Note: Demo bypass credentials ([email protected]:123, [email protected]:123) auto-create accounts in the database. Remove these for production.


Every API route follows this structure:

src/app/api/[resource]/route.ts
export async function GET(request: NextRequest) {
const user = await getAuthenticatedUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
// Parse query params → build Prisma where clause → paginate
const data = await prisma.resource.findMany({ where, skip, take });
return NextResponse.json(data);
}
export async function POST(request: NextRequest) {
const user = await getAuthenticatedUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = await request.json();
const parsed = resourceSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: parsed.error.issues }, { status: 400 });
const record = await prisma.resource.create({ data: parsed.data });
return NextResponse.json(record, { status: 201 });
}
CodeMeaning
200Success (GET, PUT, PATCH)
201Created (POST)
400Validation error — body contains Zod issue array
401Unauthenticated — no valid session
404Resource not found
500Internal server error

One store per major domain. Each store holds:

  • Entity list + filter state
  • Selected item
  • Loading / error flags
  • CRUD action methods

Stores use persist middleware to cache filters to localStorage and devtools for debug inspection.

  • Zod schemas live in src/lib/validations/[module].ts
  • Types are derived with z.infer<typeof schema>
  • The same schema is used for server-side validation in API routes and client-side validation in forms

Complex multi-step writes use prisma.$transaction([...]) to ensure atomicity (e.g., creating a work order and its initial activity log entry together).


sequenceDiagram
participant Client
participant Route as Next.js API Route
participant Auth as getAuthenticatedUser()
participant Zod as Zod Schema
participant Prisma
participant DB as PostgreSQL
Client->>Route: HTTP Request
Route->>Auth: Validate session
Auth-->>Route: User | null
alt Unauthenticated
Route-->>Client: 401 Unauthorized
end
Route->>Zod: Parse request body
alt Invalid body
Route-->>Client: 400 { error: issues[] }
end
Route->>Prisma: Query / Mutation
Prisma->>DB: SQL
DB-->>Prisma: Result
Prisma-->>Route: Typed result
Route-->>Client: 200/201 JSON

graph TD
M2["M2 App"]
Authentik["Authentik OIDC\nsso.inspiringroup.com"]
AzureAD["Microsoft Azure AD"]
Bitrix24["Bitrix24 OAuth"]
GMaps["Google Maps API\nPlaces + Geocoding"]
Sentry["Sentry (self-hosted)\nsentry.dabz.me"]
M2 -->|OIDC auth flow| Authentik
Authentik -->|identity federation| AzureAD
M2 -->|fallback login| Bitrix24
M2 -->|villa map + place search| GMaps
M2 -->|errors + source maps| Sentry
IntegrationPackageUsage
Authentiknext-auth/providersPrimary SSO — all users login via Authentik
Microsoft Azure AD(via Authentik)Identity source
Bitrix24lib/bitrix-user.tsAlternative OAuth login, user profile sync
Google Maps@googlemaps/js-api-loader, @react-google-maps/apiVilla map view, address autocomplete, geocoding
Sentry@sentry/nextjsClient + server error capture, source map upload

Note: No email delivery service (SendGrid, SES, etc.) is currently wired up. Alert notifications use in-app toasts only. Email integration requires manual implementation.

Note: File/image uploads accept URL strings in the schema. No S3/CDN/blob storage is currently integrated — the upload endpoints are placeholders.


sequenceDiagram
participant Reporter
participant Manager
participant Technician
participant M2 as M2 API
participant DB
Reporter->>M2: POST /api/work-orders
M2->>DB: Create WorkOrder (status: PENDING)
M2->>DB: Log WorkOrderActivity (created)
Manager->>M2: PATCH /work-orders/:id/status (ASSIGNED)
M2->>DB: Update assignedToId, status
Technician->>M2: PATCH /work-orders/:id/status (IN_PROGRESS)
Technician->>M2: POST /work-orders/:id/comments
Technician->>M2: PATCH /work-orders/:id/tasks/:taskId (COMPLETED)
Technician->>M2: PATCH /work-orders/:id/status (COMPLETED)
M2->>DB: Set completedAt, log activity
Manager->>M2: Verify and close
sequenceDiagram
participant User
participant M2 as M2 API
participant DB
User->>M2: POST /api/requests (status: PENDING)
DB-->>M2: Request created (REQ-XXXX)
Manager->>M2: PATCH /api/requests/:id/convert
M2->>DB: Create WorkOrder linked to requestId
M2->>DB: Update Request status → IN_PROGRESS
M2-->>Manager: { workOrderId, workOrderNumber }
sequenceDiagram
participant Admin
participant M2 as M2 API
participant DB
participant Scheduler
Admin->>M2: POST /api/pmp-templates (checklist + frequency)
Admin->>M2: POST /api/pmp-schedules (template + villa/asset)
M2->>DB: Create PMPSchedule (nextDueDate calculated)
Scheduler->>M2: POST /api/pmp-schedules/generate-recurrence
M2->>DB: Insert future PMPSchedule instances
M2-->>Technician: Schedules visible on PMP calendar

RoleAccess Level
ADMINFull access — configuration, user management, all modules
MANAGERAll properties, work order assignment, reporting
TECHNICIANAssigned work orders, field notes, timesheets
VENDORAssigned external work orders only

  • Node.js 22+
  • Yarn 1.22.x
  • Docker (for PostgreSQL)
  • Access to Authentik SSO credentials (or use demo bypass accounts)
Terminal window
# Install dependencies
yarn install
# Copy environment file and fill in values
cp .env.example .env
# Generate Prisma client
npx prisma generate
# Run database migrations
npx prisma migrate dev
# Seed with mock data (optional)
npx prisma db seed
# Start development server
yarn dev
Terminal window
yarn test # Unit tests in watch mode
yarn test:run # Unit tests once
yarn test:integration # Integration tests
yarn test:coverage # Coverage report
Terminal window
docker-compose up -d # Starts app + PostgreSQL

M2 is deployed as a Docker container on Digital Ocean via Coolify.

graph LR
GitLab["GitLab CI\n.gitlab-ci.yml"]
Docker["Docker Build\nmulti-stage"]
Coolify["Coolify\nDeploy Host"]
App["M2 Container\nNode 22 Alpine"]
PG["PostgreSQL\nContainer"]
Nginx["Nginx\nReverse Proxy"]
GitLab -->|build image| Docker
Docker -->|push| Coolify
Coolify -->|run| App
App --- PG
Nginx -->|proxy| App
  • Build: Multi-stage Dockerfile — deps → build (Prisma generate + next build) → runner (standalone output)
  • Migrations: Separate Dockerfile.migrate runs prisma migrate deploy before app start
  • Config: Secrets injected via Coolify environment variables
  • Output: next build with output: 'standalone' for minimal container size

See DEPLOYMENT.md in the repo root for the full deployment runbook.


LocationIssue
api/purchase-orders/route.ts:54TODO: Get actual user ID from session — currently falls back to first user in DB
components/vendors/new-contract-modal.tsx:51TODO: Wire to API once service contracts endpoint is ready
File uploadsfileUrl fields accept strings only — no storage backend (S3/CDN) integrated
Email notificationsNo email service configured — alerts are in-app toasts only
Real-time updatesNo WebSocket/SSE — polling only
QR code scanningqrCode field exists on Asset model but no scanning UI
Bitrix24 syncOnly OAuth login implemented — work order sync not built
Mobile appFieldNote and Timesheet models exist but no dedicated mobile UI

M2 is internationalised using next-intl. Supported locales:

CodeLanguage
enEnglish (default)
thThai (ภาษาไทย)

Translation message files are in messages/. Switch locale via the user menu.