Admin AI Tools page: unified design system styling, provider selector with OpenAI turd emoji, free-text model input with exact ID warning
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"use server";
|
||||
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
export type AIAuthConfig = {
|
||||
provider: string;
|
||||
api_key: string;
|
||||
organization_id: string;
|
||||
base_url: string;
|
||||
model: string;
|
||||
max_tokens: number;
|
||||
};
|
||||
|
||||
export async function getAIPreferences(brandId: string): Promise<{
|
||||
api_key?: string;
|
||||
organization_id?: string;
|
||||
base_url?: string;
|
||||
model?: string;
|
||||
max_tokens?: number;
|
||||
} | null> {
|
||||
const { data, error } = await supabase
|
||||
.from("brand_ai_settings")
|
||||
.select("api_key, organization_id, base_url, model, max_tokens")
|
||||
.eq("brand_id", brandId)
|
||||
.single();
|
||||
|
||||
if (error || !data) return null;
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function saveAIPreferences(
|
||||
brandId: string,
|
||||
config: AIAuthConfig
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const { error } = await supabase
|
||||
.from("brand_ai_settings")
|
||||
.upsert({
|
||||
brand_id: brandId,
|
||||
provider: config.provider,
|
||||
api_key: config.api_key || null,
|
||||
organization_id: config.organization_id || null,
|
||||
base_url: config.base_url || null,
|
||||
model: config.model || "gpt-4o-mini",
|
||||
max_tokens: config.max_tokens || 4000,
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (error) return { success: false, error: error.message };
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function testAIConnection(config: AIAuthConfig): Promise<{ ok: boolean; message: string }> {
|
||||
if (!config.api_key?.trim()) {
|
||||
return { ok: false, message: "API key is required" };
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl = config.base_url?.trim() || "https://api.openai.com/v1";
|
||||
const url = `${baseUrl.replace(/\/$/, "")}/models`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.api_key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return { ok: true, message: "Connection successful! Your API key is valid." };
|
||||
} else if (response.status === 401) {
|
||||
return { ok: false, message: "Invalid API key. Please check and try again." };
|
||||
} else {
|
||||
return { ok: false, message: `Error: ${response.status} ${response.statusText}` };
|
||||
}
|
||||
} catch {
|
||||
return { ok: false, message: "Could not connect. Check your network and API key." };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
// Contact imports bucket
|
||||
const CONTACTS_BUCKET_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
||||
|
||||
export type UploadContactsResult =
|
||||
| { success: true; fileId: string; fileUrl: string; recordCount: number }
|
||||
| { success: false; error: string };
|
||||
|
||||
export async function uploadContactsToBucket(
|
||||
brandId: string,
|
||||
file: File
|
||||
): Promise<UploadContactsResult> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
// Validate file type
|
||||
if (!file.name.endsWith(".csv")) {
|
||||
return { success: false, error: "Only CSV files are supported" };
|
||||
}
|
||||
|
||||
// For very large files (farmers with tons of data), check size
|
||||
const maxSize = 50 * 1024 * 1024; // 50MB max
|
||||
if (file.size > maxSize) {
|
||||
return { success: false, error: "File too large. Max 50MB for large imports." };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// Generate unique path
|
||||
const timestamp = Date.now();
|
||||
const safeName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_");
|
||||
const path = `imports/${brandId}/${timestamp}-${safeName}`;
|
||||
|
||||
// Upload to bucket
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/${CONTACTS_BUCKET_ID}/${path}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
...svcHeaders(supabaseKey),
|
||||
"Authorization": `Bearer ${supabaseKey}`,
|
||||
"Content-Type": "text/csv",
|
||||
"x-upsert": "false",
|
||||
},
|
||||
body: buffer,
|
||||
}
|
||||
);
|
||||
|
||||
if (!uploadRes.ok) {
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
}
|
||||
|
||||
const fileUrl = `${supabaseUrl}/storage/v1/object/public/${CONTACTS_BUCKET_ID}/${path}`;
|
||||
const fileId = `${brandId}/${timestamp}`;
|
||||
|
||||
// Get rough row count from file size (approx 200 bytes per row)
|
||||
const estimatedRows = Math.floor(file.size / 200);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fileId,
|
||||
fileUrl,
|
||||
recordCount: estimatedRows,
|
||||
};
|
||||
}
|
||||
|
||||
export type ProcessImportResult =
|
||||
| { success: true; created: number; updated: number; skipped: number; errors: number }
|
||||
| { success: false; error: string };
|
||||
|
||||
export async function processBucketImport(
|
||||
brandId: string,
|
||||
fileUrl: string,
|
||||
allowOptInOverride: boolean = false
|
||||
): Promise<ProcessImportResult> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// Call RPC to process the file from bucket
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/process_contact_import_from_url`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
p_file_url: fileUrl,
|
||||
p_allow_opt_in_override: allowOptInOverride,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: `Processing failed: ${await response.text()}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
created: data.created ?? 0,
|
||||
updated: data.updated ?? 0,
|
||||
skipped: data.skipped ?? 0,
|
||||
errors: data.errors ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listImportHistory(
|
||||
brandId: string,
|
||||
limit: number = 10
|
||||
): Promise<{ success: true; imports: ImportHistoryItem[] } | { success: false; error: string }> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// List files in the imports folder for this brand
|
||||
const response = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/list/${CONTACTS_BUCKET_ID}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
prefix: `imports/${brandId}/`,
|
||||
limit: limit,
|
||||
sortBy: { column: "created_at", order: "desc" },
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return { success: false, error: "Failed to list imports" };
|
||||
}
|
||||
|
||||
const files = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
imports: files.map((f: { name: string; metadata: { size: number }; created_at: string }) => ({
|
||||
filename: f.name.split("/").pop() ?? "",
|
||||
size: f.metadata?.size ?? 0,
|
||||
createdAt: f.created_at,
|
||||
url: `${supabaseUrl}/storage/v1/object/public/${CONTACTS_BUCKET_ID}/${f.name}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export type ImportHistoryItem = {
|
||||
filename: string;
|
||||
size: number;
|
||||
createdAt: string;
|
||||
url: string;
|
||||
};
|
||||
@@ -51,6 +51,7 @@ export async function savePaymentSettings(params: {
|
||||
provider: PaymentProvider | null;
|
||||
stripePublishableKey?: string;
|
||||
stripeSecretKey?: string;
|
||||
stripeUserId?: string;
|
||||
squareAccessToken?: string;
|
||||
squareLocationId?: string;
|
||||
squareSyncEnabled?: boolean;
|
||||
@@ -84,6 +85,7 @@ export async function savePaymentSettings(params: {
|
||||
p_provider: params.provider,
|
||||
p_stripe_publishable_key: params.stripePublishableKey ?? null,
|
||||
p_stripe_secret_key: params.stripeSecretKey ?? null,
|
||||
p_stripe_user_id: params.stripeUserId ?? null,
|
||||
p_square_access_token: params.squareAccessToken ?? null,
|
||||
p_square_location_id: params.squareLocationId ?? null,
|
||||
p_square_sync_enabled: params.squareSyncEnabled ?? null,
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
|
||||
// Product images bucket - UUID from Supabase
|
||||
const PRODUCT_IMAGES_BUCKET_ID = "80aa01da-ab4b-44f8-b6e7-700552457e18";
|
||||
|
||||
export type UploadProductImageResult =
|
||||
| { success: true; imageUrl: string }
|
||||
| { success: false; error: string };
|
||||
@@ -32,7 +35,7 @@ export async function uploadProductImage(
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`${supabaseUrl}/storage/v1/object/product-images/${path}`,
|
||||
`${supabaseUrl}/storage/v1/object/${PRODUCT_IMAGES_BUCKET_ID}/${path}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { ...svcHeaders(supabaseKey), "Authorization": `Bearer ${supabaseKey}`, "Content-Type": `image/${ext}`, "x-upsert": "true" },
|
||||
@@ -44,7 +47,7 @@ export async function uploadProductImage(
|
||||
return { success: false, error: `Upload failed: ${await uploadRes.text()}` };
|
||||
}
|
||||
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/product-images/${path}`;
|
||||
const publicUrl = `${supabaseUrl}/storage/v1/object/public/${PRODUCT_IMAGES_BUCKET_ID}/${path}`;
|
||||
|
||||
// If productId is "__NEW__", we just upload and return the URL (caller handles saving it)
|
||||
if (productId === "__NEW__") {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
"use server";
|
||||
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import { svcHeaders } from "@/lib/svc-headers";
|
||||
import Stripe from "stripe";
|
||||
|
||||
/**
|
||||
* Get Stripe Connect status for a brand.
|
||||
* Checks if brand has a stripe_user_id (connected account).
|
||||
*/
|
||||
export async function getStripeConnectStatus(brandId: string): Promise<{
|
||||
is_connected: boolean;
|
||||
account_id?: string;
|
||||
charges_enabled?: boolean;
|
||||
payouts_enabled?: boolean;
|
||||
details_submitted?: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { is_connected: false, error: "Not authenticated" };
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// Get brand's payment settings
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/get_brand_payment_settings`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_brand_id: brandId }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return { is_connected: false, error: "Failed to fetch payment settings" };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const stripeUserId = data?.stripe_user_id;
|
||||
|
||||
if (!stripeUserId) {
|
||||
return { is_connected: false };
|
||||
}
|
||||
|
||||
// Verify the account exists and get status
|
||||
try {
|
||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!stripeKey) {
|
||||
return { is_connected: true, account_id: stripeUserId };
|
||||
}
|
||||
|
||||
const stripe = new Stripe(stripeKey, { apiVersion: "2026-04-22.dahlia" as any });
|
||||
const account = await stripe.accounts.retrieve(stripeUserId);
|
||||
|
||||
return {
|
||||
is_connected: true,
|
||||
account_id: stripeUserId,
|
||||
charges_enabled: account.charges_enabled,
|
||||
payouts_enabled: account.payouts_enabled,
|
||||
details_submitted: account.details_submitted,
|
||||
};
|
||||
} catch (err) {
|
||||
// If we can't verify, assume connected but with stale data
|
||||
return {
|
||||
is_connected: true,
|
||||
account_id: stripeUserId,
|
||||
charges_enabled: false,
|
||||
payouts_enabled: false,
|
||||
details_submitted: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Stripe Connect Express account for a brand
|
||||
* and return an onboarding link.
|
||||
*/
|
||||
export async function createStripeConnectLink(brandId: string): Promise<{
|
||||
success: boolean;
|
||||
url?: string;
|
||||
account_id?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!stripeKey) {
|
||||
return { success: false, error: "Stripe is not configured on this platform" };
|
||||
}
|
||||
|
||||
try {
|
||||
const stripe = new Stripe(stripeKey, { apiVersion: "2026-04-22.dahlia" as any });
|
||||
const origin = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
|
||||
|
||||
// Create Express account
|
||||
const account = await stripe.accounts.create({
|
||||
type: "express",
|
||||
capabilities: {
|
||||
card_payments: { requested: true },
|
||||
transfers: { requested: true },
|
||||
},
|
||||
metadata: {
|
||||
brand_id: brandId,
|
||||
},
|
||||
});
|
||||
|
||||
// Create account link for onboarding
|
||||
const accountLink = await stripe.accountLinks.create({
|
||||
account: account.id,
|
||||
refresh_url: `${origin}/admin/advanced?stripe_refresh=true`,
|
||||
return_url: `${origin}/admin/advanced?stripe_connected=true`,
|
||||
type: "account_onboarding",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: accountLink.url,
|
||||
account_id: account.id,
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to create Stripe account";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new account link for an existing Stripe Connect account
|
||||
* (for refreshing expired links or re-onboarding)
|
||||
*/
|
||||
export async function refreshStripeConnectLink(brandId: string): Promise<{
|
||||
success: boolean;
|
||||
url?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
// Get existing account ID
|
||||
const status = await getStripeConnectStatus(brandId);
|
||||
if (!status.is_connected || !status.account_id) {
|
||||
// No existing account, create new one
|
||||
return createStripeConnectLink(brandId);
|
||||
}
|
||||
|
||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!stripeKey) {
|
||||
return { success: false, error: "Stripe is not configured on this platform" };
|
||||
}
|
||||
|
||||
try {
|
||||
const stripe = new Stripe(stripeKey, { apiVersion: "2026-04-22.dahlia" as any });
|
||||
const origin = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
|
||||
|
||||
const accountLink = await stripe.accountLinks.create({
|
||||
account: status.account_id,
|
||||
refresh_url: `${origin}/admin/advanced?stripe_refresh=true`,
|
||||
return_url: `${origin}/admin/advanced?stripe_connected=true`,
|
||||
type: "account_onboarding",
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: accountLink.url,
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to refresh onboarding link";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Stripe Connect account ID to brand settings
|
||||
*/
|
||||
export async function saveStripeConnectAccount(brandId: string, accountId: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
// Save to payment_settings via RPC
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/set_stripe_connect_account`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
p_brand_id: brandId,
|
||||
p_stripe_user_id: accountId,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.text();
|
||||
return { success: false, error: `Failed to save: ${error}` };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect Stripe Connect account from a brand
|
||||
*/
|
||||
export async function disconnectStripeConnect(brandId: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
|
||||
|
||||
const res = await fetch(
|
||||
`${supabaseUrl}/rest/v1/rpc/disconnect_stripe_connect`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ p_brand_id: brandId }),
|
||||
}
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return { success: false, error: "Failed to disconnect Stripe account" };
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Stripe Express dashboard login link
|
||||
*/
|
||||
export async function createStripeDashboardLink(brandId: string): Promise<{
|
||||
success: boolean;
|
||||
url?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return { success: false, error: "Not authenticated" };
|
||||
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
|
||||
return { success: false, error: "Not authorized" };
|
||||
}
|
||||
|
||||
const status = await getStripeConnectStatus(brandId);
|
||||
if (!status.is_connected || !status.account_id) {
|
||||
return { success: false, error: "No Stripe account connected" };
|
||||
}
|
||||
|
||||
const stripeKey = process.env.STRIPE_SECRET_KEY;
|
||||
if (!stripeKey) {
|
||||
return { success: false, error: "Stripe is not configured" };
|
||||
}
|
||||
|
||||
try {
|
||||
const stripe = new Stripe(stripeKey, { apiVersion: "2026-04-22.dahlia" as any });
|
||||
const loginLink = await stripe.accounts.createLoginLink(status.account_id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: loginLink.url,
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to create dashboard link";
|
||||
return { success: false, error: message };
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,15 @@ function rpcBody(body: Record<string, unknown>) {
|
||||
|
||||
export type TimeWorker = {
|
||||
id: string;
|
||||
brand_id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
lang: string;
|
||||
pin: string;
|
||||
active: boolean;
|
||||
last_used_at: string | null;
|
||||
created_at: string;
|
||||
worker_number: number | null;
|
||||
};
|
||||
|
||||
export type TimeTask = {
|
||||
@@ -62,12 +65,15 @@ export async function getTimeTrackingWorkers(brandId: string): Promise<TimeWorke
|
||||
if (useMockData) {
|
||||
return mockWorkers.map(w => ({
|
||||
id: w.id,
|
||||
brand_id: brandId,
|
||||
name: w.name,
|
||||
role: w.role,
|
||||
lang: w.language,
|
||||
pin: "0000",
|
||||
active: w.is_active,
|
||||
last_used_at: null,
|
||||
created_at: new Date().toISOString(),
|
||||
worker_number: null,
|
||||
}));
|
||||
}
|
||||
const res = await fetch(
|
||||
@@ -80,7 +86,18 @@ export async function getTimeTrackingWorkers(brandId: string): Promise<TimeWorke
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data?.workers ?? [];
|
||||
return (data?.workers ?? []).map((w: Record<string, unknown>) => ({
|
||||
id: w.id as string,
|
||||
brand_id: w.brand_id as string,
|
||||
name: w.name as string,
|
||||
role: w.role as string,
|
||||
lang: w.lang as string,
|
||||
pin: w.pin as string,
|
||||
active: w.active as boolean,
|
||||
last_used_at: w.last_used_at as string | null,
|
||||
created_at: w.created_at as string,
|
||||
worker_number: w.worker_number as number | null,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function createTimeWorker(
|
||||
|
||||
Reference in New Issue
Block a user