fix(api): close v2 mobile contract gaps

This commit is contained in:
zouantchaw
2026-03-17 22:37:45 +01:00
parent afcd896b47
commit 008dd7efb1
14 changed files with 1315 additions and 54 deletions

View File

@@ -171,17 +171,32 @@ export async function listRecentReorders(actorUid, limit) {
o.id,
o.title,
o.starts_at AS "date",
COALESCE(cp.label, o.location_name) AS "hubName",
COALESCE(COUNT(sr.id), 0)::INTEGER AS "positionCount",
COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType"
MAX(COALESCE(cp.label, o.location_name)) AS "hubName",
MAX(b.business_name) AS "clientName",
COALESCE(SUM(sr.workers_needed), 0)::INTEGER AS "positionCount",
COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType",
COALESCE(ROUND(AVG(sr.bill_rate_cents))::INTEGER, 0) AS "hourlyRateCents",
COALESCE(
SUM(
sr.bill_rate_cents
* sr.workers_needed
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
),
0
)::BIGINT AS "totalPriceCents",
COALESCE(
SUM(GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)),
0
)::NUMERIC(12,2) AS hours
FROM orders o
JOIN businesses b ON b.id = o.business_id
LEFT JOIN shifts s ON s.order_id = o.id
LEFT JOIN shift_roles sr ON sr.shift_id = s.id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
WHERE o.tenant_id = $1
AND o.business_id = $2
AND o.status IN ('COMPLETED', 'ACTIVE', 'FILLED')
GROUP BY o.id, cp.label
GROUP BY o.id
ORDER BY o.starts_at DESC NULLS LAST
LIMIT $3
`,
@@ -520,15 +535,33 @@ export async function listOrderItemsByDateRange(actorUid, { startDate, endDate }
sr.id AS "itemId",
o.id AS "orderId",
COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType",
o.title AS "eventName",
b.business_name AS "clientName",
sr.role_name AS title,
sr.role_name AS "roleName",
s.starts_at AS date,
to_char(s.starts_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS date,
s.starts_at AS "startsAt",
s.ends_at AS "endsAt",
to_char(s.starts_at AT TIME ZONE 'UTC', 'HH24:MI') AS "startTime",
to_char(s.ends_at AT TIME ZONE 'UTC', 'HH24:MI') AS "endTime",
sr.workers_needed AS "requiredWorkerCount",
sr.assigned_count AS "filledCount",
sr.bill_rate_cents AS "hourlyRateCents",
ROUND(COALESCE(sr.bill_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)::NUMERIC(12,2) AS hours,
(sr.bill_rate_cents * sr.workers_needed)::BIGINT AS "totalCostCents",
ROUND(
(
sr.bill_rate_cents
* sr.workers_needed
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)::numeric / 100,
2
) AS "totalValue",
COALESCE(cp.label, s.location_name) AS "locationName",
COALESCE(s.location_address, cp.address) AS "locationAddress",
hm.business_membership_id AS "hubManagerId",
COALESCE(u.display_name, u.email) AS "hubManagerName",
s.status,
COALESCE(
json_agg(
@@ -544,14 +577,34 @@ export async function listOrderItemsByDateRange(actorUid, { startDate, endDate }
FROM shift_roles sr
JOIN shifts s ON s.id = sr.shift_id
JOIN orders o ON o.id = s.order_id
JOIN businesses b ON b.id = o.business_id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
LEFT JOIN assignments a ON a.shift_role_id = sr.id
LEFT JOIN staffs st ON st.id = a.staff_id
LEFT JOIN LATERAL (
SELECT business_membership_id
FROM hub_managers
WHERE tenant_id = o.tenant_id
AND hub_id = s.clock_point_id
ORDER BY created_at ASC
LIMIT 1
) hm ON TRUE
LEFT JOIN business_memberships bm ON bm.id = hm.business_membership_id
LEFT JOIN users u ON u.id = bm.user_id
WHERE o.tenant_id = $1
AND o.business_id = $2
AND s.starts_at >= $3::timestamptz
AND s.starts_at <= $4::timestamptz
GROUP BY sr.id, o.id, s.id, cp.label
GROUP BY
sr.id,
o.id,
s.id,
cp.label,
cp.address,
b.business_name,
hm.business_membership_id,
u.display_name,
u.email
ORDER BY s.starts_at ASC, sr.role_name ASC
`,
[context.tenant.tenantId, context.business.businessId, range.start, range.end]
@@ -633,6 +686,23 @@ export async function listTodayShifts(actorUid) {
COALESCE(s.title, sr.role_name || ' shift') AS title,
b.business_name AS "clientName",
ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
COALESCE(sr.pay_rate_cents, 0)::INTEGER AS "hourlyRateCents",
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)::numeric / 100,
2
) AS "totalRate",
COALESCE(
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)
)::INTEGER,
0
) AS "totalRateCents",
sr.role_name AS "roleName",
COALESCE(cp.label, s.location_name) AS location,
COALESCE(s.location_address, cp.address) AS "locationAddress",
@@ -656,7 +726,7 @@ export async function listTodayShifts(actorUid) {
AND a.staff_id = $2
AND s.starts_at >= $3::timestamptz
AND s.starts_at < $4::timestamptz
AND a.status IN ('ASSIGNED', 'ACCEPTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED')
AND a.status IN ('ASSIGNED', 'ACCEPTED', 'SWAP_REQUESTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED')
ORDER BY ABS(EXTRACT(EPOCH FROM (s.starts_at - NOW()))) ASC
`,
[context.tenant.tenantId, context.staff.staffId, from, to]
@@ -767,24 +837,43 @@ export async function listAssignedShifts(actorUid, { startDate, endDate }) {
SELECT
a.id AS "assignmentId",
s.id AS "shiftId",
b.business_name AS "clientName",
sr.role_name AS "roleName",
COALESCE(cp.label, s.location_name) AS location,
s.starts_at AS date,
s.starts_at AS "startTime",
s.ends_at AS "endTime",
sr.pay_rate_cents AS "hourlyRateCents",
ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
COALESCE(
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)
)::INTEGER,
0
) AS "totalRateCents",
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)::numeric / 100,
2
) AS "totalRate",
COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType",
a.status
FROM assignments a
JOIN shifts s ON s.id = a.shift_id
JOIN shift_roles sr ON sr.id = a.shift_role_id
JOIN orders o ON o.id = s.order_id
JOIN businesses b ON b.id = s.business_id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
WHERE a.tenant_id = $1
AND a.staff_id = $2
AND s.starts_at >= $3::timestamptz
AND s.starts_at <= $4::timestamptz
AND a.status IN ('ASSIGNED', 'ACCEPTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED')
AND a.status IN ('ASSIGNED', 'ACCEPTED', 'SWAP_REQUESTED', 'CHECKED_IN', 'CHECKED_OUT', 'COMPLETED')
ORDER BY s.starts_at ASC
`,
[context.tenant.tenantId, context.staff.staffId, range.start, range.end]
@@ -800,18 +889,37 @@ export async function listOpenShifts(actorUid, { limit, search } = {}) {
SELECT
s.id AS "shiftId",
sr.id AS "roleId",
b.business_name AS "clientName",
sr.role_name AS "roleName",
COALESCE(cp.label, s.location_name) AS location,
s.starts_at AS date,
s.starts_at AS "startTime",
s.ends_at AS "endTime",
sr.pay_rate_cents AS "hourlyRateCents",
ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
COALESCE(
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)
)::INTEGER,
0
) AS "totalRateCents",
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)::numeric / 100,
2
) AS "totalRate",
COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType",
FALSE AS "instantBook",
sr.workers_needed AS "requiredWorkerCount"
FROM shifts s
JOIN shift_roles sr ON sr.shift_id = s.id
JOIN orders o ON o.id = s.order_id
JOIN businesses b ON b.id = s.business_id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
WHERE s.tenant_id = $1
AND s.status = 'OPEN'
@@ -829,12 +937,30 @@ export async function listOpenShifts(actorUid, { limit, search } = {}) {
SELECT
s.id AS "shiftId",
sr.id AS "roleId",
b.business_name AS "clientName",
sr.role_name AS "roleName",
COALESCE(cp.label, s.location_name) AS location,
s.starts_at AS date,
s.starts_at AS "startTime",
s.ends_at AS "endTime",
sr.pay_rate_cents AS "hourlyRateCents",
ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
COALESCE(
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)
)::INTEGER,
0
) AS "totalRateCents",
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)::numeric / 100,
2
) AS "totalRate",
COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType",
FALSE AS "instantBook",
1::INTEGER AS "requiredWorkerCount"
@@ -842,6 +968,7 @@ export async function listOpenShifts(actorUid, { limit, search } = {}) {
JOIN shifts s ON s.id = a.shift_id
JOIN shift_roles sr ON sr.id = a.shift_role_id
JOIN orders o ON o.id = s.order_id
JOIN businesses b ON b.id = s.business_id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
WHERE a.tenant_id = $1
AND a.status = 'SWAP_REQUESTED'
@@ -911,8 +1038,11 @@ export async function getStaffShiftDetail(actorUid, shiftId) {
s.id AS "shiftId",
s.title,
o.description,
b.business_name AS "clientName",
COALESCE(cp.label, s.location_name) AS location,
s.location_address AS address,
COALESCE(s.location_address, cp.address) AS address,
COALESCE(s.latitude, cp.latitude) AS latitude,
COALESCE(s.longitude, cp.longitude) AS longitude,
s.starts_at AS date,
s.starts_at AS "startTime",
s.ends_at AS "endTime",
@@ -923,6 +1053,23 @@ export async function getStaffShiftDetail(actorUid, shiftId) {
sr.id AS "roleId",
sr.role_name AS "roleName",
sr.pay_rate_cents AS "hourlyRateCents",
ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
COALESCE(
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)
)::INTEGER,
0
) AS "totalRateCents",
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)::numeric / 100,
2
) AS "totalRate",
COALESCE(o.metadata->>'orderType', 'ONE_TIME') AS "orderType",
sr.workers_needed AS "requiredCount",
sr.assigned_count AS "confirmedCount",
@@ -930,6 +1077,7 @@ export async function getStaffShiftDetail(actorUid, shiftId) {
app.status AS "applicationStatus"
FROM shifts s
JOIN orders o ON o.id = s.order_id
JOIN businesses b ON b.id = s.business_id
JOIN shift_roles sr ON sr.shift_id = s.id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
LEFT JOIN assignments a ON a.shift_role_id = sr.id AND a.staff_id = $3
@@ -981,12 +1129,41 @@ export async function listCompletedShifts(actorUid) {
a.id AS "assignmentId",
s.id AS "shiftId",
s.title,
b.business_name AS "clientName",
COALESCE(cp.label, s.location_name) AS location,
s.starts_at AS date,
to_char(s.starts_at AT TIME ZONE 'UTC', 'YYYY-MM-DD') AS date,
s.starts_at AS "startTime",
s.ends_at AS "endTime",
COALESCE(sr.pay_rate_cents, 0)::INTEGER AS "hourlyRateCents",
ROUND(COALESCE(sr.pay_rate_cents, 0)::numeric / 100, 2) AS "hourlyRate",
COALESCE(ts.status, 'PENDING') AS "timesheetStatus",
COALESCE(ts.regular_minutes + ts.overtime_minutes, 0) AS "minutesWorked",
COALESCE(
ts.gross_pay_cents,
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)
)::BIGINT
) AS "totalRateCents",
ROUND(
COALESCE(
ts.gross_pay_cents,
ROUND(
(
COALESCE(sr.pay_rate_cents, 0)
* GREATEST(EXTRACT(EPOCH FROM (s.ends_at - s.starts_at)) / 3600, 0)
)
)::BIGINT
)::numeric / 100,
2
) AS "totalRate",
COALESCE(rp.status, 'PENDING') AS "paymentStatus"
FROM assignments a
JOIN shifts s ON s.id = a.shift_id
JOIN businesses b ON b.id = s.business_id
LEFT JOIN shift_roles sr ON sr.id = a.shift_role_id
LEFT JOIN clock_points cp ON cp.id = s.clock_point_id
LEFT JOIN timesheets ts ON ts.assignment_id = a.id
LEFT JOIN recent_payments rp ON rp.assignment_id = a.id
@@ -1003,19 +1180,22 @@ export async function listCompletedShifts(actorUid) {
export async function getProfileSectionsStatus(actorUid) {
const context = await requireStaffContext(actorUid);
const completion = getProfileCompletionFromMetadata(context.staff);
const [documents, certificates, benefits] = await Promise.all([
const [documents, certificates, benefits, attire, taxForms] = await Promise.all([
listProfileDocuments(actorUid),
listCertificates(actorUid),
listStaffBenefits(actorUid),
listAttireChecklist(actorUid),
listTaxForms(actorUid),
]);
return {
personalInfoCompleted: completion.fields.firstName && completion.fields.lastName && completion.fields.email && completion.fields.phone && completion.fields.preferredLocations,
emergencyContactCompleted: completion.fields.emergencyContact,
experienceCompleted: completion.fields.skills && completion.fields.industries,
attireCompleted: documents.filter((item) => item.documentType === 'ATTIRE').every((item) => item.status === 'VERIFIED'),
taxFormsCompleted: documents.filter((item) => item.documentType === 'TAX_FORM').every((item) => item.status === 'VERIFIED'),
attireCompleted: attire.every((item) => item.status === 'VERIFIED'),
taxFormsCompleted: taxForms.every((item) => item.status === 'VERIFIED' || item.status === 'SUBMITTED'),
benefitsConfigured: benefits.length > 0,
certificateCount: certificates.length,
documentCount: documents.length,
};
}
@@ -1054,6 +1234,7 @@ export async function listProfileDocuments(actorUid) {
d.id AS "documentId",
d.document_type AS "documentType",
d.name,
COALESCE(d.metadata->>'description', '') AS description,
sd.id AS "staffDocumentId",
sd.file_uri AS "fileUri",
COALESCE(sd.status, 'NOT_UPLOADED') AS status,
@@ -1065,7 +1246,7 @@ export async function listProfileDocuments(actorUid) {
AND sd.tenant_id = d.tenant_id
AND sd.staff_id = $2
WHERE d.tenant_id = $1
AND d.document_type IN ('DOCUMENT', 'GOVERNMENT_ID', 'ATTIRE', 'TAX_FORM')
AND d.document_type IN ('DOCUMENT', 'GOVERNMENT_ID')
ORDER BY d.name ASC
`,
[context.tenant.tenantId, context.staff.staffId]
@@ -1645,9 +1826,12 @@ export async function listTaxForms(actorUid) {
SELECT
d.id AS "documentId",
d.name AS "formType",
COALESCE(d.metadata->>'description', '') AS description,
sd.id AS "staffDocumentId",
sd.file_uri AS "fileUri",
COALESCE(sd.metadata->>'formStatus', 'NOT_STARTED') AS status,
COALESCE(sd.metadata->'fields', '{}'::jsonb) AS fields
COALESCE(sd.metadata->'fields', '{}'::jsonb) AS fields,
sd.expires_at AS "expiresAt"
FROM documents d
LEFT JOIN staff_documents sd
ON sd.document_id = d.id