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:
+38
-177
@@ -1,7 +1,17 @@
|
||||
"use server";
|
||||
|
||||
import { cookies } from "next/headers";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
// TODO(migration): the water-log field UI used a chain of Supabase RPCs
|
||||
// (`get_water_headgates`, `verify_water_pin`, `get_water_user_by_id`,
|
||||
// `submit_water_entry`, `trigger_water_alert`,
|
||||
// `get_water_admin_session`) and tables (`water_headgates`,
|
||||
// `water_users`, `water_sessions`, `water_admin_sessions`,
|
||||
// `water_entries`, `water_alert_log`) that are not in the SaaS
|
||||
// rebuild's `db/schema/`. The actions below preserve the original
|
||||
// signatures and return empty / no-op responses so the field UI
|
||||
// degrades gracefully. See `actions/route-trace/lots.ts` for the
|
||||
// same pattern.
|
||||
|
||||
type VerifyPinResult = {
|
||||
success: true;
|
||||
@@ -30,102 +40,31 @@ type Headgate = {
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type HeadgatesResult = {
|
||||
headgates: Headgate[];
|
||||
};
|
||||
const NOT_CONFIGURED = "Water log is not configured in the SaaS rebuild";
|
||||
|
||||
export async function getWaterHeadgates(brandId: string, activeOnly = false): Promise<Headgate[]> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_headgates`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_brand_id: brandId, p_active_only: activeOnly }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) return [];
|
||||
const data = await response.json();
|
||||
return data?.headgates ?? [];
|
||||
export async function getWaterHeadgates(
|
||||
_brandId: string,
|
||||
_activeOnly = false
|
||||
): Promise<Headgate[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function verifyWaterPin(brandId: string, pin: string): Promise<VerifyPinResult> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/verify_water_pin`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_brand_id: brandId, p_pin: pin }),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data?.success) {
|
||||
return { success: false, error: data?.error ?? "Invalid PIN" };
|
||||
}
|
||||
|
||||
// Get user's language preference via SECURITY DEFINER RPC (avoids direct table access)
|
||||
const userResponse = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_user_by_id`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_user_id: data.user_id }),
|
||||
}
|
||||
);
|
||||
const userData = await userResponse.json();
|
||||
const lang = userData?.language_preference ?? "en";
|
||||
|
||||
// Use the session already created by verify_water_pin RPC
|
||||
const sessionId = data.session_id;
|
||||
|
||||
if (!sessionId) {
|
||||
return { success: false, error: "Failed to create session" };
|
||||
}
|
||||
|
||||
// Set HTTP-only session cookie
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set("wl_session", sessionId, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 4 * 60 * 60, // 4 hours
|
||||
path: "/",
|
||||
});
|
||||
cookieStore.set("wl_lang", lang, {
|
||||
httpOnly: false,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
path: "/",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user_id: data.user_id,
|
||||
name: data.name,
|
||||
role: data.role,
|
||||
session_id: sessionId,
|
||||
lang,
|
||||
};
|
||||
export async function verifyWaterPin(
|
||||
_brandId: string,
|
||||
_pin: string
|
||||
): Promise<VerifyPinResult> {
|
||||
return { success: false, error: NOT_CONFIGURED };
|
||||
}
|
||||
|
||||
export async function submitWaterEntry(
|
||||
headgateId: string,
|
||||
measurement: number,
|
||||
unit: string,
|
||||
notes: string,
|
||||
photoUrl?: string,
|
||||
latitude?: number,
|
||||
longitude?: number,
|
||||
headgateLocked?: boolean
|
||||
_headgateId: string,
|
||||
_measurement: number,
|
||||
_unit: string,
|
||||
_notes: string,
|
||||
_photoUrl?: string,
|
||||
_latitude?: number,
|
||||
_longitude?: number,
|
||||
_headgateLocked?: boolean
|
||||
): Promise<SubmitEntryResult> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("wl_session")?.value;
|
||||
@@ -134,74 +73,7 @@ export async function submitWaterEntry(
|
||||
return { success: false, error: "Not logged in" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/submit_water_entry`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_session_id: sessionId,
|
||||
p_headgate_id: headgateId,
|
||||
p_measurement: measurement,
|
||||
p_unit: unit,
|
||||
p_notes: notes,
|
||||
p_submitted_via: "field",
|
||||
p_photo_url: photoUrl ?? null,
|
||||
p_latitude: latitude ?? null,
|
||||
p_longitude: longitude ?? null,
|
||||
p_headgate_locked: headgateLocked ?? false,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data?.success) {
|
||||
return { success: false, error: data?.error ?? "Failed to submit entry" };
|
||||
}
|
||||
|
||||
const entryId = data.entry_id as string;
|
||||
|
||||
// ── Alert check (fire-and-forget, non-blocking) ─────────────────
|
||||
try {
|
||||
const alertRes = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/trigger_water_alert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_entry_id: entryId,
|
||||
p_alert_type: "high",
|
||||
p_threshold_value: measurement,
|
||||
p_reading_value: measurement,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const highData = await alertRes.json();
|
||||
// If not triggered as high, try low
|
||||
if (!highData?.triggered) {
|
||||
await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/trigger_water_alert`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_entry_id: entryId,
|
||||
p_alert_type: "low",
|
||||
p_threshold_value: measurement,
|
||||
p_reading_value: measurement,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Alert failures should not affect entry submission success
|
||||
}
|
||||
|
||||
return { success: true, entry_id: entryId };
|
||||
return { success: false, error: NOT_CONFIGURED };
|
||||
}
|
||||
|
||||
export async function logoutWater(): Promise<void> {
|
||||
@@ -230,26 +102,15 @@ export async function setWaterLang(lang: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getWaterAdminSession(): Promise<{ user_id: string; name: string; role: string } | null> {
|
||||
export async function getWaterAdminSession(): Promise<{
|
||||
user_id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
} | null> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("wl_admin_session")?.value;
|
||||
|
||||
if (!sessionId) return null;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
|
||||
// Use get_water_admin_session RPC (SECURITY DEFINER) to avoid direct water_sessions + water_users JOIN
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_water_admin_session`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_session_id: sessionId }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
return data;
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user