363 lines
11 KiB
TypeScript
363 lines
11 KiB
TypeScript
// services/maintenanceRequestService.ts
|
|
// Handle maintenance requests, tracking, and resolution workflow
|
|
|
|
import { eq } from "drizzle-orm";
|
|
import { db } from "../db/db.ts";
|
|
import { maintenance_requests, properties } from "../db/schema.ts";
|
|
import { getUserById } from "./userService.ts";
|
|
import { getPropertyById } from "./propertyService.ts";
|
|
|
|
export interface MaintenanceRequest {
|
|
id: string;
|
|
property_id?: string | null;
|
|
tenant_id?: string | null;
|
|
request_type?: string | null; // 'plumbing', 'electrical', 'hvac', 'general'
|
|
description?: string | null; // Encrypted request details
|
|
status?: string | null; // 'open', 'in_progress', 'completed', 'cancelled'
|
|
priority?: string | null; // 'low', 'medium', 'high', 'urgent'
|
|
created_on?: number | null;
|
|
resolved_on?: number | null;
|
|
}
|
|
|
|
// Extended interface for dashboard maintenance requests with UI properties
|
|
export interface DashboardMaintenanceRequest extends MaintenanceRequest {
|
|
tenant_name?: string;
|
|
tenant_email?: string;
|
|
property_address?: string;
|
|
created_at?: number | null; // UI-friendly timestamp (maps to created_on)
|
|
resolved_at?: number | null; // UI-friendly timestamp (maps to resolved_on)
|
|
days_open?: number; // Calculated field for dashboard display
|
|
}
|
|
|
|
// Custom interface that combines maintenance request with user and property data
|
|
export interface MaintenanceData {
|
|
id: string;
|
|
property_id: string;
|
|
tenant_id: string;
|
|
request_type: string | null;
|
|
description: string | null;
|
|
status: string | null;
|
|
priority: string | null;
|
|
created_on: number;
|
|
resolved_on: number | null;
|
|
// Enhanced data from related tables
|
|
tenant_name: string;
|
|
tenant_email: string;
|
|
tenant_phone: string;
|
|
property_address: string;
|
|
property_city: string;
|
|
property_state: string;
|
|
property_zip: string;
|
|
property_title: string;
|
|
}
|
|
|
|
// Create new maintenance request
|
|
export async function createMaintenanceRequest(
|
|
property_id: string,
|
|
tenant_id: string,
|
|
request_type: string,
|
|
description: string,
|
|
priority: string = "medium",
|
|
) {
|
|
const now = Date.now();
|
|
return await db.insert(maintenance_requests)
|
|
.values({
|
|
id: crypto.randomUUID(),
|
|
property_id,
|
|
tenant_id,
|
|
request_type,
|
|
description,
|
|
status: "open",
|
|
priority,
|
|
created_on: now,
|
|
resolved_on: null,
|
|
})
|
|
.returning();
|
|
}
|
|
|
|
// Get maintenance requests by property
|
|
export async function getRequestsByProperty(property_id: string) {
|
|
return await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.property_id, property_id))
|
|
.orderBy(maintenance_requests.created_on);
|
|
}
|
|
|
|
// Get maintenance requests by tenant
|
|
export async function getRequestsByTenant(tenant_id: string) {
|
|
return await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.tenant_id, tenant_id))
|
|
.orderBy(maintenance_requests.created_on);
|
|
}
|
|
|
|
// Get requests by status
|
|
export async function getRequestsByStatus(
|
|
status: string,
|
|
property_ids?: string[],
|
|
) {
|
|
if (property_ids && property_ids.length > 0) {
|
|
// In a real implementation, you'd use an IN clause
|
|
// For now, filtering by status only
|
|
return await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.status, status))
|
|
.orderBy(maintenance_requests.created_on);
|
|
}
|
|
|
|
return await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.status, status))
|
|
.orderBy(maintenance_requests.created_on);
|
|
}
|
|
|
|
// Get open requests
|
|
export async function getOpenRequests(property_ids?: string[]) {
|
|
return await getRequestsByStatus("open", property_ids);
|
|
}
|
|
|
|
// Get requests by priority
|
|
export async function getRequestsByPriority(
|
|
priority: string,
|
|
property_ids?: string[],
|
|
) {
|
|
if (property_ids && property_ids.length > 0) {
|
|
return await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.priority, priority))
|
|
.orderBy(maintenance_requests.created_on);
|
|
}
|
|
|
|
return await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.priority, priority))
|
|
.orderBy(maintenance_requests.created_on);
|
|
}
|
|
|
|
// Get urgent requests
|
|
export async function getUrgentRequests(property_ids?: string[]) {
|
|
return await getRequestsByPriority("urgent", property_ids);
|
|
}
|
|
|
|
// Update request status
|
|
export async function updateRequestStatus(
|
|
id: string,
|
|
status: "open" | "in_progress" | "completed" | "cancelled",
|
|
) {
|
|
const updates: { status: string; resolved_on?: number } = { status };
|
|
|
|
// Set resolved_on timestamp when completing or cancelling
|
|
if (status === "completed" || status === "cancelled") {
|
|
updates.resolved_on = Date.now();
|
|
}
|
|
|
|
return await db.update(maintenance_requests)
|
|
.set(updates)
|
|
.where(eq(maintenance_requests.id, id))
|
|
.returning();
|
|
}
|
|
|
|
// Update request priority
|
|
export async function updateRequestPriority(
|
|
id: string,
|
|
priority: "low" | "medium" | "high" | "urgent",
|
|
) {
|
|
return await db.update(maintenance_requests)
|
|
.set({ priority })
|
|
.where(eq(maintenance_requests.id, id))
|
|
.returning();
|
|
}
|
|
|
|
// Get request by ID
|
|
export async function getRequestById(id: string) {
|
|
const result = await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.id, id));
|
|
return result[0] || null;
|
|
}
|
|
|
|
// Delete request
|
|
export async function deleteMaintenanceRequest(id: string) {
|
|
return await db.delete(maintenance_requests)
|
|
.where(eq(maintenance_requests.id, id))
|
|
.returning();
|
|
}
|
|
|
|
// Get request statistics for a property
|
|
export async function getRequestStatsByProperty(property_id: string) {
|
|
const requests = await getRequestsByProperty(property_id);
|
|
|
|
return {
|
|
total: requests.length,
|
|
open: requests.filter((r) => r.status === "open").length,
|
|
in_progress: requests.filter((r) => r.status === "in_progress").length,
|
|
completed: requests.filter((r) => r.status === "completed").length,
|
|
cancelled: requests.filter((r) => r.status === "cancelled").length,
|
|
urgent: requests.filter((r) => r.priority === "urgent").length,
|
|
high: requests.filter((r) => r.priority === "high").length,
|
|
medium: requests.filter((r) => r.priority === "medium").length,
|
|
low: requests.filter((r) => r.priority === "low").length,
|
|
};
|
|
}
|
|
|
|
// Get request statistics for a tenant
|
|
export async function getRequestStatsByTenant(tenant_id: string) {
|
|
const requests = await db.select()
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.tenant_id, tenant_id));
|
|
|
|
return {
|
|
total: requests.length,
|
|
open: requests.filter((r) => r.status === "open").length,
|
|
in_progress: requests.filter((r) => r.status === "in_progress").length,
|
|
completed: requests.filter((r) => r.status === "completed").length,
|
|
};
|
|
}
|
|
|
|
// Get maintenance requests by landlord (via properties)
|
|
export async function getMaintenanceRequestsByLandlord(landlord_id: string) {
|
|
return await db.select({
|
|
id: maintenance_requests.id,
|
|
property_id: maintenance_requests.property_id,
|
|
tenant_id: maintenance_requests.tenant_id,
|
|
request_type: maintenance_requests.request_type,
|
|
description: maintenance_requests.description,
|
|
status: maintenance_requests.status,
|
|
priority: maintenance_requests.priority,
|
|
created_on: maintenance_requests.created_on,
|
|
resolved_on: maintenance_requests.resolved_on,
|
|
})
|
|
.from(maintenance_requests)
|
|
.innerJoin(properties, eq(maintenance_requests.property_id, properties.id))
|
|
.where(eq(properties.owner_id, landlord_id))
|
|
.orderBy(maintenance_requests.created_on);
|
|
}
|
|
|
|
// Get maintenance requests by user (tenant)
|
|
export async function getMaintenanceRequestsByUser(
|
|
user_id: string,
|
|
options: { limit?: number } = {},
|
|
) {
|
|
const query = db.select({
|
|
id: maintenance_requests.id,
|
|
property_id: maintenance_requests.property_id,
|
|
tenant_id: maintenance_requests.tenant_id,
|
|
title: maintenance_requests.request_type,
|
|
description: maintenance_requests.description,
|
|
status: maintenance_requests.status,
|
|
priority: maintenance_requests.priority,
|
|
category: maintenance_requests.request_type,
|
|
created_on: maintenance_requests.created_on,
|
|
resolved_on: maintenance_requests.resolved_on,
|
|
})
|
|
.from(maintenance_requests)
|
|
.where(eq(maintenance_requests.tenant_id, user_id))
|
|
.orderBy(maintenance_requests.created_on);
|
|
|
|
if (options.limit) {
|
|
return await query.limit(options.limit);
|
|
}
|
|
|
|
return await query;
|
|
}
|
|
|
|
// Update maintenance request status (alias for compatibility)
|
|
export async function updateMaintenanceRequestStatus(
|
|
id: string,
|
|
updates: { status?: string; priority?: string },
|
|
) {
|
|
const updateData: {
|
|
status?: string;
|
|
priority?: string;
|
|
resolved_on?: number;
|
|
} = {};
|
|
|
|
if (updates.status) {
|
|
updateData.status = updates.status;
|
|
if (updates.status === "completed") {
|
|
updateData.resolved_on = Date.now();
|
|
}
|
|
}
|
|
|
|
if (updates.priority) {
|
|
updateData.priority = updates.priority;
|
|
}
|
|
|
|
return await db.update(maintenance_requests)
|
|
.set(updateData)
|
|
.where(eq(maintenance_requests.id, id))
|
|
.returning();
|
|
}
|
|
|
|
// Get maintenance requests with enhanced data (user names and property addresses)
|
|
export async function getMaintenanceDataByLandlord(
|
|
landlord_id: string,
|
|
): Promise<MaintenanceData[]> {
|
|
try {
|
|
// First get all maintenance requests for the landlord
|
|
const maintenanceRequests = await getMaintenanceRequestsByLandlord(
|
|
landlord_id,
|
|
);
|
|
|
|
// Build the enhanced data array
|
|
const maintenanceData: MaintenanceData[] = [];
|
|
|
|
for (const request of maintenanceRequests) {
|
|
try {
|
|
// Get tenant user data
|
|
const tenant = await getUserById(request.tenant_id || "");
|
|
if (!tenant || !request.tenant_id) {
|
|
console.warn(`Tenant not found for ID: ${request.tenant_id}`);
|
|
continue;
|
|
}
|
|
|
|
// Get property data
|
|
const property = await getPropertyById(request.property_id || "");
|
|
if (!property || !request.property_id) {
|
|
console.warn(`Property not found for ID: ${request.property_id}`);
|
|
continue;
|
|
}
|
|
|
|
// Build the enhanced maintenance data object
|
|
const enhancedData: MaintenanceData = {
|
|
id: request.id,
|
|
property_id: request.property_id || "",
|
|
tenant_id: request.tenant_id || "",
|
|
request_type: request.request_type || null,
|
|
description: request.description || null,
|
|
status: request.status || null,
|
|
priority: request.priority || null,
|
|
created_on: request.created_on || Date.now(),
|
|
resolved_on: request.resolved_on || null,
|
|
// Enhanced data from user table
|
|
tenant_name: tenant.name || "Unknown",
|
|
tenant_email: tenant.email || "",
|
|
tenant_phone: tenant.phone || "",
|
|
// Enhanced data from property table
|
|
property_address:
|
|
`${property.address_1 || ""} ${property.address_2 || ""}`.trim() ||
|
|
"No address",
|
|
property_city: property.city || "",
|
|
property_state: property.state || "",
|
|
property_zip: property.zip || "",
|
|
property_title: property.title || "Untitled Property",
|
|
};
|
|
|
|
maintenanceData.push(enhancedData);
|
|
} catch (error) {
|
|
console.error(
|
|
`Error processing maintenance request ${request.id}:`,
|
|
error,
|
|
);
|
|
// Continue processing other requests even if one fails
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return maintenanceData;
|
|
} catch (error) {
|
|
console.error("Error in getMaintenanceDataByLandlord:", error);
|
|
throw error;
|
|
}
|
|
}
|