Files
Krow-workspace/backend/query-api/src/services/query-service.js

286 lines
7.7 KiB
JavaScript

import { AppError } from '../lib/errors.js';
import { query } from './db.js';
function parseLimit(value, fallback = 20, max = 100) {
const parsed = Number.parseInt(`${value || fallback}`, 10);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
return Math.min(parsed, max);
}
function parseOffset(value) {
const parsed = Number.parseInt(`${value || 0}`, 10);
if (!Number.isFinite(parsed) || parsed < 0) return 0;
return parsed;
}
export async function listOrders({ tenantId, businessId, status, limit, offset }) {
const result = await query(
`
SELECT
o.id,
o.order_number AS "orderNumber",
o.title,
o.status,
o.service_type AS "serviceType",
o.starts_at AS "startsAt",
o.ends_at AS "endsAt",
o.location_name AS "locationName",
o.location_address AS "locationAddress",
o.created_at AS "createdAt",
b.id AS "businessId",
b.business_name AS "businessName",
v.id AS "vendorId",
v.company_name AS "vendorName",
COALESCE(COUNT(s.id), 0)::INTEGER AS "shiftCount",
COALESCE(SUM(s.required_workers), 0)::INTEGER AS "requiredWorkers",
COALESCE(SUM(s.assigned_workers), 0)::INTEGER AS "assignedWorkers"
FROM orders o
JOIN businesses b ON b.id = o.business_id
LEFT JOIN vendors v ON v.id = o.vendor_id
LEFT JOIN shifts s ON s.order_id = o.id
WHERE o.tenant_id = $1
AND ($2::uuid IS NULL OR o.business_id = $2::uuid)
AND ($3::text IS NULL OR o.status = $3::text)
GROUP BY o.id, b.id, v.id
ORDER BY o.created_at DESC
LIMIT $4 OFFSET $5
`,
[
tenantId,
businessId || null,
status || null,
parseLimit(limit),
parseOffset(offset),
]
);
return result.rows;
}
export async function getOrderDetail({ tenantId, orderId }) {
const orderResult = await query(
`
SELECT
o.id,
o.order_number AS "orderNumber",
o.title,
o.description,
o.status,
o.service_type AS "serviceType",
o.starts_at AS "startsAt",
o.ends_at AS "endsAt",
o.location_name AS "locationName",
o.location_address AS "locationAddress",
o.latitude,
o.longitude,
o.notes,
o.created_at AS "createdAt",
b.id AS "businessId",
b.business_name AS "businessName",
v.id AS "vendorId",
v.company_name AS "vendorName"
FROM orders o
JOIN businesses b ON b.id = o.business_id
LEFT JOIN vendors v ON v.id = o.vendor_id
WHERE o.tenant_id = $1
AND o.id = $2
`,
[tenantId, orderId]
);
if (orderResult.rowCount === 0) {
throw new AppError('NOT_FOUND', 'Order not found', 404, { tenantId, orderId });
}
const shiftsResult = await query(
`
SELECT
s.id,
s.shift_code AS "shiftCode",
s.title,
s.status,
s.starts_at AS "startsAt",
s.ends_at AS "endsAt",
s.timezone,
s.location_name AS "locationName",
s.location_address AS "locationAddress",
s.required_workers AS "requiredWorkers",
s.assigned_workers AS "assignedWorkers",
cp.id AS "clockPointId",
cp.label AS "clockPointLabel"
FROM shifts s
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
WHERE s.tenant_id = $1
AND s.order_id = $2
ORDER BY s.starts_at ASC
`,
[tenantId, orderId]
);
const shiftIds = shiftsResult.rows.map((row) => row.id);
let rolesByShiftId = new Map();
if (shiftIds.length > 0) {
const rolesResult = await query(
`
SELECT
sr.id,
sr.shift_id AS "shiftId",
sr.role_code AS "roleCode",
sr.role_name AS "roleName",
sr.workers_needed AS "workersNeeded",
sr.assigned_count AS "assignedCount",
sr.pay_rate_cents AS "payRateCents",
sr.bill_rate_cents AS "billRateCents"
FROM shift_roles sr
WHERE sr.shift_id = ANY($1::uuid[])
ORDER BY sr.role_name ASC
`,
[shiftIds]
);
rolesByShiftId = rolesResult.rows.reduce((map, row) => {
const list = map.get(row.shiftId) || [];
list.push(row);
map.set(row.shiftId, list);
return map;
}, new Map());
}
return {
...orderResult.rows[0],
shifts: shiftsResult.rows.map((shift) => ({
...shift,
roles: rolesByShiftId.get(shift.id) || [],
})),
};
}
export async function listFavoriteStaff({ tenantId, businessId, limit, offset }) {
const result = await query(
`
SELECT
sf.id AS "favoriteId",
sf.created_at AS "favoritedAt",
s.id AS "staffId",
s.full_name AS "fullName",
s.primary_role AS "primaryRole",
s.average_rating AS "averageRating",
s.rating_count AS "ratingCount",
s.status
FROM staff_favorites sf
JOIN staffs s ON s.id = sf.staff_id
WHERE sf.tenant_id = $1
AND sf.business_id = $2
ORDER BY sf.created_at DESC
LIMIT $3 OFFSET $4
`,
[tenantId, businessId, parseLimit(limit), parseOffset(offset)]
);
return result.rows;
}
export async function getStaffReviewSummary({ tenantId, staffId, limit }) {
const staffResult = await query(
`
SELECT
id AS "staffId",
full_name AS "fullName",
average_rating AS "averageRating",
rating_count AS "ratingCount",
primary_role AS "primaryRole",
status
FROM staffs
WHERE tenant_id = $1
AND id = $2
`,
[tenantId, staffId]
);
if (staffResult.rowCount === 0) {
throw new AppError('NOT_FOUND', 'Staff not found', 404, { tenantId, staffId });
}
const reviewsResult = await query(
`
SELECT
sr.id AS "reviewId",
sr.rating,
sr.review_text AS "reviewText",
sr.tags,
sr.created_at AS "createdAt",
b.id AS "businessId",
b.business_name AS "businessName",
sr.assignment_id AS "assignmentId"
FROM staff_reviews sr
JOIN businesses b ON b.id = sr.business_id
WHERE sr.tenant_id = $1
AND sr.staff_id = $2
ORDER BY sr.created_at DESC
LIMIT $3
`,
[tenantId, staffId, parseLimit(limit, 10, 50)]
);
return {
...staffResult.rows[0],
reviews: reviewsResult.rows,
};
}
export async function getAssignmentAttendance({ tenantId, assignmentId }) {
const assignmentResult = await query(
`
SELECT
a.id AS "assignmentId",
a.status,
a.shift_id AS "shiftId",
a.staff_id AS "staffId",
s.title AS "shiftTitle",
s.starts_at AS "shiftStartsAt",
s.ends_at AS "shiftEndsAt",
attendance_sessions.id AS "sessionId",
attendance_sessions.status AS "sessionStatus",
attendance_sessions.check_in_at AS "checkInAt",
attendance_sessions.check_out_at AS "checkOutAt",
attendance_sessions.worked_minutes AS "workedMinutes"
FROM assignments a
JOIN shifts s ON s.id = a.shift_id
LEFT JOIN attendance_sessions ON attendance_sessions.assignment_id = a.id
WHERE a.id = $1
AND a.tenant_id = $2
`,
[assignmentId, tenantId]
);
if (assignmentResult.rowCount === 0) {
throw new AppError('NOT_FOUND', 'Assignment not found', 404, { tenantId, assignmentId });
}
const eventsResult = await query(
`
SELECT
id AS "attendanceEventId",
event_type AS "eventType",
source_type AS "sourceType",
source_reference AS "sourceReference",
nfc_tag_uid AS "nfcTagUid",
latitude,
longitude,
distance_to_clock_point_meters AS "distanceToClockPointMeters",
within_geofence AS "withinGeofence",
validation_status AS "validationStatus",
validation_reason AS "validationReason",
captured_at AS "capturedAt"
FROM attendance_events
WHERE assignment_id = $1
ORDER BY captured_at ASC
`,
[assignmentId]
);
return {
...assignmentResult.rows[0],
events: eventsResult.rows,
};
}