Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions src/modules/employees/employees.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const employeesRepository = {

let q = orgTable(request, "employees")
.select(EMPLOYEE_COLS, { count: "exact" })
.order("name", { ascending: true })
.order("employee_code", { ascending: true })
.range(offset, offset + limit - 1);

if (active !== undefined) {
Expand Down Expand Up @@ -154,11 +154,12 @@ export const employeesRepository = {
last_latitude: number | null;
last_longitude: number | null;
last_location_at: string | null;
activity_status: "ACTIVE" | "RECENT" | "INACTIVE";
})[];
total: number;
source: "snapshot" | "employees";
}> {
const { page, limit, active, search } = query;
const { page, limit, active, search, segment } = query;
const offset = (page - 1) * limit;

let q = supabase
Expand All @@ -172,7 +173,7 @@ export const employeesRepository = {
{ count: "exact" },
)
.eq("organization_id", request.organizationId)
.order("name", { ascending: true })
.order("employee_code", { ascending: true })
.range(offset, offset + limit - 1);

if (active !== undefined) {
Expand All @@ -199,6 +200,7 @@ export const employeesRepository = {
last_latitude: null,
last_longitude: null,
last_location_at: null,
activity_status: "INACTIVE" as const,
})),
source: "employees",
};
Expand All @@ -217,18 +219,35 @@ export const employeesRepository = {

const enriched = ((data ?? []) as unknown as EmployeeWithState[]).map((row) => {
const { employee_last_state: state, ...employee } = row;
const isCheckedIn = state?.is_checked_in ?? false;
let activityStatus: "ACTIVE" | "RECENT" | "INACTIVE" = "INACTIVE";
if (isCheckedIn) {
activityStatus = "ACTIVE";
} else if (state?.last_check_out_at) {
const ageMs = Date.now() - new Date(state.last_check_out_at).getTime();
activityStatus = ageMs < 86_400_000 ? "RECENT" : "INACTIVE";
}
return {
...(employee as Employee),
is_checked_in: state?.is_checked_in ?? false,
is_checked_in: isCheckedIn,
last_check_in_at: state?.last_check_in_at ?? null,
last_check_out_at: state?.last_check_out_at ?? null,
last_latitude: state?.last_latitude ?? null,
last_longitude: state?.last_longitude ?? null,
last_location_at: state?.last_location_at ?? null,
activity_status: activityStatus,
};
});

return { data: enriched, total: count ?? 0, source: "snapshot" as const };
// Apply segment filter in-memory (segment is derived from snapshot state, not a DB column)
const filtered = segment
? enriched.filter((e) => e.activity_status === segment.toUpperCase())
: enriched;

// When filtering by segment, total reflects the filtered count
const effectiveTotal = segment ? filtered.length : (count ?? 0);

return { data: filtered, total: effectiveTotal, source: "snapshot" as const };
},

/**
Expand Down
4 changes: 3 additions & 1 deletion src/modules/expenses/expenses.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export const expensesController = {
const t0 = Date.now();

// feat-1: fast path for PENDING expenses view (most common admin use case)
if (statusFilter === "all" || statusFilter === "PENDING") {
// Only use snapshot when explicitly requesting PENDING status
if (statusFilter === "PENDING") {
const snapResult = await expensesRepository.findPendingFromSnapshot(
request,
page,
Expand Down Expand Up @@ -116,6 +117,7 @@ export const expensesController = {
page,
limit,
employeeId,
statusFilter,
);
const durationMs = Date.now() - t0;
const response = paginated(result.data, page, limit, result.total);
Expand Down
6 changes: 6 additions & 0 deletions src/modules/expenses/expenses.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const expensesRepository = {
page: number,
limit: number,
employeeId?: string,
status?: string,
): Promise<{ data: EnrichedExpense[]; total: number }> {
// Phase 30: count:"estimated" eliminates the shadow SELECT COUNT(*) query.
// idx_expenses_org_submitted_desc (org_id, submitted_at DESC) covers the
Expand All @@ -126,6 +127,11 @@ export const expensesRepository = {
baseQuery = (baseQuery as any).eq("employee_id", employeeId);
}

if (status && status !== "all") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
baseQuery = (baseQuery as any).eq("status", status);
}

const { data, error, count } = await applyPagination(baseQuery, page, limit);

if (error) {
Expand Down
3 changes: 2 additions & 1 deletion src/modules/expenses/expenses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ export const expensesService = {
page: number,
limit: number,
employeeId?: string,
status?: string,
): Promise<{ data: EnrichedExpense[]; total: number }> {
return expensesRepository.findExpensesByOrg(request, page, limit, employeeId);
return expensesRepository.findExpensesByOrg(request, page, limit, employeeId, status);
},

/**
Expand Down
27 changes: 27 additions & 0 deletions supabase/migrations/20260408000100_segmentation_indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Phase: Segmentation performance indexes
--
-- Supports the segmented loading strategy:
-- 1. Employees sorted by employee_code β€” idx_employees_org_code covers the sort
-- 2. Sessions segmented by checkout_at β€” idx_sessions_checkout_at for time-range queries
-- 3. Employee last state β€” idx_els_last_checkout for segment computation
--
-- All indexes are idempotent (IF NOT EXISTS).
-- ──────────────────────────────────────────────────────────────────────────────

-- Covers: WHERE organization_id = ? ORDER BY employee_code ASC
-- Used by: GET /admin/employees (listWithLastState)
-- Previously sorted by name, now sorted by employee_code.
CREATE INDEX IF NOT EXISTS idx_employees_org_code
ON public.employees (organization_id, employee_code ASC);

-- Covers: checkout_at time-range queries for RECENT segment detection
-- Used by: session status computation (checkout_at > now() - interval '24 hours')
CREATE INDEX IF NOT EXISTS idx_sessions_checkout_at
ON public.attendance_sessions (checkout_at DESC)
WHERE checkout_at IS NOT NULL;

-- Covers: last_check_out_at lookups for employee activity segmentation
-- Used by: employee segment computation in listWithLastState
CREATE INDEX IF NOT EXISTS idx_els_last_checkout
ON public.employee_last_state (last_check_out_at DESC)
WHERE last_check_out_at IS NOT NULL;
Loading