migrate: replace Supabase REST with Drizzle/pg in 28 more files (wave 5 final)
- admin components: TaxDashboard, OrderTableBody, TaxQuarterlySummary, StopProductAssignment, StopTableClient, ProductAssignmentForm, StopMessagingForm - api routes: wholesale/checkout, wholesale/price-sheet, api/supabase (DELETED), api/reports/export, route-trace/* (stubbed - feature retired) - lib: src/lib/supabase.ts (mock-only shim, no @supabase imports), src/lib/supabase/server.ts (DELETED) - actions: wholesale-auth (stubbed - awaiting Auth.js migration), shipping/settings, water-log/*, tax (added getTaxSummaryAction/getTaxableOrdersAction), time-tracking/* (stubbed) - new actions: stops/manage-stop-products (assign/unassign), stops/get-stop-customers (pending pickup list), orders.toggleOrderPickupComplete (legacy column) All Supabase REST calls replaced with Drizzle ORM, raw pg.Pool queries, or existing server actions. Typecheck: clean. Tests: 22/22 pass. Build: OK.
This commit is contained in:
@@ -2,14 +2,18 @@
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
function rpcBody(body: Record<string, unknown>) {
|
||||
return JSON.stringify(body);
|
||||
}
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
// TODO(migration): the time-tracking feature was built on Supabase RPCs
|
||||
// (verify_time_tracking_pin, clock_in_worker, clock_out_worker,
|
||||
// get_open_clock_in, get_time_tracking_tasks,
|
||||
// get_worker_pay_period_hours, get_time_tracking_settings,
|
||||
// get_water_user_by_id, get_water_admin_session, submit_water_entry,
|
||||
// trigger_water_alert) plus tables that don't exist in the SaaS rebuild
|
||||
// schema (`time_workers`, `time_tasks`, `time_logs`, `water_*`). The
|
||||
// functions below are stubs that preserve the original signature so
|
||||
// the field UI degrades gracefully (no PIN login, no entry submit) —
|
||||
// exactly the same pattern as `actions/route-trace/lots.ts`. To bring
|
||||
// time tracking back, add the tables to `db/schema/` and re-implement
|
||||
// these functions against Drizzle.
|
||||
|
||||
export type TimeTrackingSession = {
|
||||
worker_id: string;
|
||||
@@ -49,119 +53,41 @@ function parseSessionCookie(cookie: string): TimeTrackingSession | null {
|
||||
// ── Verify PIN ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function verifyTimeTrackingPin(
|
||||
brandId: string,
|
||||
pin: string
|
||||
_brandId: string,
|
||||
_pin: string
|
||||
): Promise<{ success: boolean; session?: TimeTrackingSession; error?: string }> {
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/verify_time_tracking_pin`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apikey: supabaseKey,
|
||||
Authorization: `Bearer ${supabaseKey}`,
|
||||
},
|
||||
body: rpcBody({ p_brand_id: brandId, p_pin: pin }),
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data?.success) {
|
||||
return { success: false, error: data?.error ?? "Invalid PIN" };
|
||||
}
|
||||
|
||||
const session: TimeTrackingSession = {
|
||||
worker_id: data.worker_id,
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
lang: data.lang,
|
||||
session_id: data.session_id,
|
||||
brand_id: data.brand_id,
|
||||
expires_at: data.expires_at,
|
||||
};
|
||||
|
||||
// Set HTTP-only cookie
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(SESSION_COOKIE, sessionCookie(session), {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
maxAge: COOKIE_MAX_AGE,
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return { success: true, session };
|
||||
// RPC no longer exists; surface a friendly error so the field UI can
|
||||
// disable the PIN pad.
|
||||
return { success: false, error: "Time tracking is not configured" };
|
||||
}
|
||||
|
||||
// ── Clock In ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function clockInWorker(
|
||||
brandId: string,
|
||||
taskId?: string,
|
||||
taskName = "General Labor"
|
||||
_brandId: string,
|
||||
_taskId?: string,
|
||||
_taskName = "General Labor"
|
||||
): Promise<{ success: boolean; log_id?: string; clock_in?: string; error?: string }> {
|
||||
const session = await getTimeTrackingSession();
|
||||
if (!session) return { success: false, error: "Not logged in" };
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/clock_in_worker`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apikey: supabaseKey,
|
||||
Authorization: `Bearer ${supabaseKey}`,
|
||||
},
|
||||
body: rpcBody({
|
||||
p_brand_id: brandId,
|
||||
p_worker_id: session.worker_id,
|
||||
p_task_id: taskId ?? null,
|
||||
p_task_name: taskName,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data?.success) return { success: false, error: data?.error ?? "Failed" };
|
||||
return { success: true, log_id: data.log_id, clock_in: data.clock_in };
|
||||
return { success: false, error: "Time tracking is not configured" };
|
||||
}
|
||||
|
||||
// ── Clock Out ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function clockOutWorker(
|
||||
lunchMinutes = 0,
|
||||
notes?: string
|
||||
_lunchMinutes = 0,
|
||||
_notes?: string
|
||||
): Promise<{ success: boolean; log_id?: string; clock_out?: string; total_minutes?: number; error?: string }> {
|
||||
const session = await getTimeTrackingSession();
|
||||
if (!session) return { success: false, error: "Not logged in" };
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/clock_out_worker`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apikey: supabaseKey,
|
||||
Authorization: `Bearer ${supabaseKey}`,
|
||||
},
|
||||
body: rpcBody({
|
||||
p_worker_id: session.worker_id,
|
||||
p_lunch_minutes: lunchMinutes,
|
||||
p_notes: notes ?? null,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data?.success) return { success: false, error: data?.error ?? "Failed" };
|
||||
return {
|
||||
success: true,
|
||||
log_id: data.log_id,
|
||||
clock_out: data.clock_out,
|
||||
total_minutes: data.total_minutes,
|
||||
};
|
||||
return { success: false, error: "Time tracking is not configured" };
|
||||
}
|
||||
|
||||
// ── Get Open Clock In ──────────────────────────────────────────────────────────
|
||||
|
||||
export async function getOpenClockIn(
|
||||
brandId: string
|
||||
_brandId: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
open?: boolean;
|
||||
@@ -173,29 +99,7 @@ export async function getOpenClockIn(
|
||||
}> {
|
||||
const session = await getTimeTrackingSession();
|
||||
if (!session) return { success: false, error: "Not logged in" };
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_open_clock_in`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apikey: supabaseKey,
|
||||
Authorization: `Bearer ${supabaseKey}`,
|
||||
},
|
||||
body: rpcBody({ p_worker_id: session.worker_id }),
|
||||
}
|
||||
);
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data?.success) return { success: false, error: data?.error ?? "Failed" };
|
||||
return {
|
||||
success: true,
|
||||
open: data.open,
|
||||
log_id: data.log_id,
|
||||
task_name: data.task_name,
|
||||
clock_in: data.clock_in,
|
||||
elapsed_minutes: data.elapsed_minutes,
|
||||
};
|
||||
return { success: true, open: false };
|
||||
}
|
||||
|
||||
// ── Logout ─────────────────────────────────────────────────────────────────────
|
||||
@@ -226,24 +130,10 @@ export type TimeTaskField = {
|
||||
};
|
||||
|
||||
export async function getTimeTrackingTasksField(
|
||||
brandId: string,
|
||||
activeOnly = true
|
||||
_brandId: string,
|
||||
_activeOnly = true
|
||||
): Promise<TimeTaskField[]> {
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_tasks`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
apikey: supabaseKey,
|
||||
Authorization: `Bearer ${supabaseKey}`,
|
||||
},
|
||||
body: rpcBody({ p_brand_id: brandId, p_active_only: activeOnly }),
|
||||
}
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data?.tasks ?? [];
|
||||
return [];
|
||||
}
|
||||
|
||||
// ── Pay Period Hours ───────────────────────────────────────────────────────────
|
||||
@@ -264,39 +154,26 @@ export type PayPeriodHours = {
|
||||
weekly_threshold: number;
|
||||
};
|
||||
|
||||
const EMPTY_PAY_PERIOD: PayPeriodHours = {
|
||||
success: false,
|
||||
total_minutes: 0,
|
||||
total_hours: 0,
|
||||
daily_minutes: 0,
|
||||
daily_hours: 0,
|
||||
weekly_minutes: 0,
|
||||
weekly_hours: 0,
|
||||
daily_overtime: false,
|
||||
weekly_overtime: false,
|
||||
period_start: "",
|
||||
period_end: "",
|
||||
daily_threshold: 12,
|
||||
weekly_threshold: 56,
|
||||
};
|
||||
|
||||
export async function getWorkerPayPeriodHours(
|
||||
brandId: string
|
||||
_brandId: string
|
||||
): Promise<PayPeriodHours> {
|
||||
const session = await getTimeTrackingSession();
|
||||
if (!session) return { success: false, total_minutes: 0, total_hours: 0, daily_minutes: 0, daily_hours: 0, weekly_minutes: 0, weekly_hours: 0, daily_overtime: false, weekly_overtime: false, period_start: "", period_end: "", daily_threshold: 12, weekly_threshold: 56 };
|
||||
|
||||
const [hoursRes, settingsRes] = await Promise.all([
|
||||
fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_worker_pay_period_hours`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", apikey: supabaseKey, Authorization: `Bearer ${supabaseKey}` },
|
||||
body: rpcBody({ p_brand_id: brandId, p_worker_id: session.worker_id }),
|
||||
}
|
||||
),
|
||||
fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_time_tracking_settings`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", apikey: supabaseKey, Authorization: `Bearer ${supabaseKey}` },
|
||||
body: rpcBody({ p_brand_id: brandId }),
|
||||
}
|
||||
),
|
||||
]);
|
||||
|
||||
const data = await hoursRes.json();
|
||||
const settings = settingsRes.ok ? (await settingsRes.json()) : null;
|
||||
|
||||
if (!hoursRes.ok) return { success: false, total_minutes: 0, total_hours: 0, daily_minutes: 0, daily_hours: 0, weekly_minutes: 0, weekly_hours: 0, daily_overtime: false, weekly_overtime: false, period_start: "", period_end: "", daily_threshold: 12, weekly_threshold: 56 };
|
||||
|
||||
return {
|
||||
...data,
|
||||
daily_threshold: settings ? Number(settings.daily_overtime_threshold) : 12,
|
||||
weekly_threshold: settings ? Number(settings.weekly_overtime_threshold) : 56,
|
||||
};
|
||||
}
|
||||
if (!session) return { ...EMPTY_PAY_PERIOD };
|
||||
return { ...EMPTY_PAY_PERIOD };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user