proyectoFinal/frontend/src/services/api.js

898 lines
27 KiB
JavaScript

import { supabase } from "./supabaseClient";
// Generador de código de seguimiento único alfanumérico (6 caracteres)
const generateTrackingCode = () => {
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return "RT-" + Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
};
// Helper para obtener el usuario de localStorage
const getLocalUser = () => {
return JSON.parse(localStorage.getItem("repairit_user") || "{}");
};
const mapClient = (client) => {
if (!client) return null;
if (typeof client === "string") return client;
const c = Array.isArray(client) ? client[0] : client;
if (!c) return null;
return {
...c,
_id: c.id,
};
};
const mapOrder = (order) => {
if (!order) return null;
return {
...order,
_id: order.id,
venueId: order.venue_id,
clientId: mapClient(order.clientId || order.client_id),
trackingCode: order.tracking_code,
deviceType: order.device_type,
deviceModel: order.device_model,
diagnosis: order.diagnosis || "",
budget: order.budget || null,
};
};
export const api = {
// Autenticación & Perfil
auth: {
login: async (email, password) => {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
throw new Error(error.message);
}
// Obtener perfil público asociado al usuario
const { data: profile, error: profileError } = await supabase
.from("profiles")
.select("*, organization:organizations(*), venue:venues(*)")
.eq("id", data.user.id)
.single();
if (profileError) {
throw new Error("No se pudo obtener el perfil del usuario.");
}
const userData = {
_id: profile.id,
name: profile.name,
email: data.user.email,
role: profile.role,
organizationId: profile.organization_id,
venueId: profile.venue_id,
token: data.session.access_token,
};
localStorage.setItem("repairit_token", data.session.access_token);
localStorage.setItem("repairit_user", JSON.stringify(userData));
return userData;
},
register: async (registerData) => {
// Registrar cuenta en Supabase Auth
const { data, error } = await supabase.auth.signUp({
email: registerData.email,
password: registerData.password,
options: {
data: {
name: registerData.name,
},
},
});
if (error) {
throw new Error(error.message);
}
// Crear Organización
const { data: org, error: orgError } = await supabase
.from("organizations")
.insert({ name: registerData.organizationName || "Organización RepairIT" })
.select()
.single();
if (orgError) throw new Error(orgError.message);
// Crear Sucursal por defecto
const { data: venue, error: venueError } = await supabase
.from("venues")
.insert({
organization_id: org.id,
name: registerData.venueName || "Sucursal Central",
email: registerData.venueEmail || registerData.email,
phone: registerData.venuePhone || "+54 381 4223344",
address: registerData.venueAddress || "Av. Sarmiento 1234, San Miguel de Tucumán",
})
.select()
.single();
if (venueError) throw new Error(venueError.message);
// Crear Perfil Público
const { error: profileError } = await supabase
.from("profiles")
.insert({
id: data.user.id,
organization_id: org.id,
venue_id: venue.id,
name: registerData.name,
role: "admin",
});
if (profileError) throw new Error(profileError.message);
return { message: "Registro completado con éxito. Por favor inicia sesión." };
},
logout: async () => {
await supabase.auth.signOut();
localStorage.removeItem("repairit_token");
localStorage.removeItem("repairit_user");
},
getProfile: async () => {
let user = getLocalUser();
if (!user.venueId || user.venueId === "undefined" || !user.organizationId) {
try {
const { data: authData } = await supabase.auth.getUser();
if (authData?.user) {
const { data: profile } = await supabase
.from("profiles")
.select("*, organization:organizations(*), venue:venues(*)")
.eq("id", authData.user.id)
.maybeSingle();
if (profile) {
user = {
_id: profile.id,
name: profile.name,
email: authData.user.email,
role: profile.role,
subscriptionPlan: profile.organization?.subscription_plan || "Multi-Taller Pro",
subscriptionStatus: profile.organization?.subscription_status || "activo",
organizationId: profile.organization_id,
venueId: profile.venue_id,
};
localStorage.setItem("repairit_user", JSON.stringify(user));
}
}
} catch (e) {
console.error("Error al autorecuperar perfil:", e);
}
}
if (!user.venueId || user.venueId === "undefined") {
return { name: "Sucursal Central", address: "Av. Sarmiento 1234, San Miguel de Tucumán", phone: "+54 381 4223344", email: "central@repairit.cloud" };
}
const { data, error } = await supabase
.from("venues")
.select("*")
.eq("id", user.venueId)
.maybeSingle();
if (error || !data) {
return { name: "Sucursal Central", address: "Av. Sarmiento 1234, San Miguel de Tucumán", phone: "+54 381 4223344", email: "central@repairit.cloud" };
}
return { ...data, _id: data.id };
},
updateProfile: async (profileData) => {
const user = getLocalUser();
if (!user.venueId || user.venueId === "undefined") {
throw new Error("No hay sucursal activa en sesión.");
}
const { data, error } = await supabase
.from("venues")
.update(profileData)
.eq("id", user.venueId)
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
getVenues: async () => {
let user = getLocalUser();
if (!user.organizationId || user.organizationId === "undefined") {
try {
const { data: authData } = await supabase.auth.getUser();
if (authData?.user) {
const { data: profile } = await supabase
.from("profiles")
.select("organization_id")
.eq("id", authData.user.id)
.maybeSingle();
if (profile) {
user.organizationId = profile.organization_id;
}
}
} catch (e) {
console.error("Error al autorecuperar sucursales:", e);
}
}
if (!user.organizationId) return [];
const { data, error } = await supabase
.from("venues")
.select("*")
.eq("organization_id", user.organizationId);
if (error || !data) return [];
return data.map((v) => ({ ...v, _id: v.id }));
},
createVenue: async (venueData) => {
const user = getLocalUser();
const { data, error } = await supabase
.from("venues")
.insert({
organization_id: user.organizationId,
...venueData,
})
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
updateVenue: async (id, venueData) => {
const user = getLocalUser();
const { data, error } = await supabase
.from("venues")
.update(venueData)
.eq("id", id)
.eq("organization_id", user.organizationId)
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
switchVenue: async (venueId) => {
const user = getLocalUser();
const { error } = await supabase
.from("profiles")
.update({ venue_id: venueId })
.eq("id", user._id || user.id);
if (error) throw new Error(error.message);
return { venueId };
},
},
// Clientes
clients: {
getAll: async () => {
const user = getLocalUser();
if (!user.organizationId || !user.venueId || user.organizationId === "undefined" || user.venueId === "undefined") {
return [];
}
const { data, error } = await supabase
.from("clients")
.select("*")
.eq("organization_id", user.organizationId)
.eq("venue_id", user.venueId);
if (error) throw new Error(error.message);
return data.map((c) => ({ ...c, _id: c.id }));
},
create: async (clientData) => {
const user = getLocalUser();
// Buscar si ya existe por DNI dentro de la sucursal
const { data: existingClient } = await supabase
.from("clients")
.select("*")
.eq("organization_id", user.organizationId)
.eq("venue_id", user.venueId)
.eq("dni", clientData.dni)
.maybeSingle();
if (existingClient) {
// Actualizar datos
const { data, error } = await supabase
.from("clients")
.update(clientData)
.eq("id", existingClient.id)
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
}
// Crear nuevo
const { data, error } = await supabase
.from("clients")
.insert({
organization_id: user.organizationId,
venue_id: user.venueId,
...clientData,
})
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
update: async (id, clientData) => {
const user = getLocalUser();
const { data, error } = await supabase
.from("clients")
.update(clientData)
.eq("id", id)
.eq("organization_id", user.organizationId)
.eq("venue_id", user.venueId)
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
delete: async (id) => {
const user = getLocalUser();
const { error } = await supabase
.from("clients")
.delete()
.eq("id", id)
.eq("organization_id", user.organizationId)
.eq("venue_id", user.venueId);
if (error) throw new Error(error.message);
return { message: "Cliente eliminado correctamente." };
},
},
// Órdenes
orders: {
getAll: async () => {
const user = getLocalUser();
if (!user.venueId || user.venueId === "undefined") return [];
const { data, error } = await supabase
.from("orders")
.select("*, clientId:clients(*)")
.eq("venue_id", user.venueId);
if (error) throw new Error(error.message);
return data.map(mapOrder);
},
create: async (orderData) => {
const user = getLocalUser();
const trackingCode = generateTrackingCode();
const { data, error } = await supabase
.from("orders")
.insert({
venue_id: user.venueId,
client_id: orderData.clientId,
tracking_code: trackingCode,
device_type: orderData.deviceType,
device_model: orderData.deviceModel,
accessories: orderData.accessories,
cosmetic: orderData.cosmetic,
issue: orderData.issue,
date: orderData.date,
time: orderData.time,
})
.select()
.single();
if (error) throw new Error(error.message);
return mapOrder(data);
},
update: async (id, orderData) => {
const { data, error } = await supabase
.from("orders")
.update(orderData)
.eq("id", id)
.select()
.single();
if (error) throw new Error(error.message);
return mapOrder(data);
},
updateStatus: async (id, status, text) => {
// Obtener orden actual para leer su historial
const { data: order } = await supabase
.from("orders")
.select("history")
.eq("id", id)
.single();
const history = Array.isArray(order?.history) ? order.history : [];
const dateStr = new Date().toLocaleDateString("es-AR") + " " + new Date().toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" });
history.push({ date: dateStr, text });
const { data, error } = await supabase
.from("orders")
.update({ status, history })
.eq("id", id)
.select()
.single();
if (error) throw new Error(error.message);
return mapOrder(data);
},
updateDiagnosisAndBudget: async (id, { diagnosis, budget, status }) => {
const { data: order } = await supabase
.from("orders")
.select("history")
.eq("id", id)
.single();
const history = Array.isArray(order?.history) ? order.history : [];
const dateStr = new Date().toLocaleDateString("es-AR") + " " + new Date().toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" });
const statusText = status ? status.toUpperCase() : "ACTUALIZADO";
history.push({ date: dateStr, text: `Diagnóstico / Presupuesto cargado (${statusText})` });
const updateData = { history };
if (diagnosis !== undefined) updateData.diagnosis = diagnosis;
if (budget !== undefined) updateData.budget = budget;
if (status) updateData.status = status;
const { data, error } = await supabase
.from("orders")
.update(updateData)
.eq("id", id)
.select()
.single();
if (error) throw new Error(error.message);
return mapOrder(data);
},
track: async (code) => {
const cleanCode = (code || "").trim().toLowerCase();
if (cleanCode === "demo-id" || cleanCode === "demo") {
return {
id: "demo-id-12345",
_id: "demo-id-12345",
trackingCode: "demo-id",
deviceType: "Notebook",
deviceModel: "Lenovo ThinkPad E14 Gen 2",
issue: "El equipo no enciende tras descarga eléctrica. Parpadea LED de carga.",
diagnosis: "Falla en circuito primario de alimentación 19V. Cortocircuito en mosfet de entrada y chip IC PMIC dañados.",
status: "presupuestado",
clientId: {
name: "Juan Pérez (Cliente Demo)",
dni: "38.450.123",
phone: "+54 381 555-0192",
email: "juan.demo@email.com"
},
venueId: {
name: "Sucursal Central (Demo)",
address: "Av. Sarmiento 1234, San Miguel de Tucumán"
},
budget: {
items: [
{ desc: "Reemplazo de Mosfet e IC Regulador PMIC", price: 18500 },
{ desc: "Mantenimiento térmico y limpieza de placa madre", price: 6500 }
],
approved: false,
dateApproved: null
},
history: [
{ date: "24/07/2026 09:30", text: "Equipo ingresado en recepción." },
{ date: "24/07/2026 11:15", text: "Diagnóstico finalizado por el laboratorio técnico." },
{ date: "24/07/2026 11:30", text: "Presupuesto detallado generado y disponible online." }
]
};
}
const { data, error } = await supabase
.from("orders")
.select("*, venueId:venues(*), clientId:clients(*)")
.eq("tracking_code", code)
.maybeSingle();
if (error) throw new Error(error.message);
if (!data) throw new Error("Orden de servicio no encontrada.");
const mapped = mapOrder(data);
if (mapped && data.venueId) {
const v = Array.isArray(data.venueId) ? data.venueId[0] : data.venueId;
mapped.venueId = v ? { ...v, _id: v.id } : null;
}
return mapped;
},
approveBudget: async (code) => {
const cleanCode = (code || "").trim().toLowerCase();
if (cleanCode === "demo-id" || cleanCode === "demo") {
return {
id: "demo-id-12345",
_id: "demo-id-12345",
trackingCode: "demo-id",
deviceType: "Notebook",
deviceModel: "Lenovo ThinkPad E14 Gen 2",
issue: "El equipo no enciende tras descarga eléctrica. Parpadea LED de carga.",
diagnosis: "Falla en circuito primario de alimentación 19V. Cortocircuito en mosfet de entrada y chip IC PMIC dañados.",
status: "en_reparacion",
clientId: {
name: "Juan Pérez (Cliente Demo)",
dni: "38.450.123",
phone: "+54 381 555-0192",
email: "juan.demo@email.com"
},
venueId: {
name: "Sucursal Central (Demo)",
address: "Av. Sarmiento 1234, San Miguel de Tucumán"
},
budget: {
items: [
{ desc: "Reemplazo de Mosfet e IC Regulador PMIC", price: 18500 },
{ desc: "Mantenimiento térmico y limpieza de placa madre", price: 6500 }
],
approved: true,
dateApproved: new Date().toLocaleDateString("es-AR")
},
history: [
{ date: "24/07/2026 09:30", text: "Equipo ingresado en recepción." },
{ date: "24/07/2026 11:15", text: "Diagnóstico finalizado por el laboratorio técnico." },
{ date: "24/07/2026 11:30", text: "Presupuesto detallado generado y disponible online." },
{ date: new Date().toLocaleDateString("es-AR") + " " + new Date().toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" }), text: "Presupuesto aprobado online por el cliente (Demo)." }
]
};
}
const { data: order } = await supabase
.from("orders")
.select("budget, history")
.eq("tracking_code", code)
.single();
const budget = order.budget || {};
budget.approved = true;
budget.dateApproved = new Date().toLocaleDateString("es-AR");
const history = Array.isArray(order?.history) ? order.history : [];
const dateStr = new Date().toLocaleDateString("es-AR") + " " + new Date().toLocaleTimeString("es-AR", { hour: "2-digit", minute: "2-digit" });
history.push({ date: dateStr, text: "Presupuesto aprobado online por el cliente." });
const { data, error } = await supabase
.from("orders")
.update({
status: "en_reparacion",
budget,
history,
})
.eq("tracking_code", code)
.select()
.single();
if (error) throw new Error(error.message);
return mapOrder(data);
},
},
// Inventario
inventory: {
getAll: async () => {
const user = getLocalUser();
if (!user.venueId || user.venueId === "undefined") return [];
const { data, error } = await supabase
.from("inventory")
.select("*")
.eq("venue_id", user.venueId);
if (error) throw new Error(error.message);
return data.map(item => ({ ...item, _id: item.id }));
},
create: async (itemData) => {
const user = getLocalUser();
const { data, error } = await supabase
.from("inventory")
.insert({
organization_id: user.organizationId,
venue_id: user.venueId,
code: itemData.code,
name: itemData.name,
description: itemData.description,
category: itemData.category,
quantity: itemData.quantity,
min_quantity: itemData.minQuantity,
price: itemData.price,
})
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
update: async (id, itemData) => {
const user = getLocalUser();
const { data, error } = await supabase
.from("inventory")
.update({
code: itemData.code,
name: itemData.name,
description: itemData.description,
category: itemData.category,
quantity: itemData.quantity,
min_quantity: itemData.minQuantity,
price: itemData.price,
})
.eq("id", id)
.eq("venue_id", user.venueId)
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
delete: async (id) => {
const user = getLocalUser();
const { error } = await supabase
.from("inventory")
.delete()
.eq("id", id)
.eq("venue_id", user.venueId);
if (error) throw new Error(error.message);
return { message: "Artículo eliminado." };
},
adjustStock: async (id, amount) => {
const user = getLocalUser();
const { data: item } = await supabase
.from("inventory")
.select("quantity")
.eq("id", id)
.eq("venue_id", user.venueId)
.single();
const newQty = Math.max(0, (item?.quantity || 0) + amount);
const { data, error } = await supabase
.from("inventory")
.update({ quantity: newQty })
.eq("id", id)
.eq("venue_id", user.venueId)
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
bulkUpsert: async (itemsArray) => {
const { data, error } = await supabase
.from("inventory")
.upsert(itemsArray, { onConflict: "venue_id, code" });
if (error) throw new Error(error.message);
return data;
},
},
// Ventas de Caja (POS)
sales: {
getAll: async (venueId = null) => {
const user = getLocalUser();
let query = supabase
.from("sales")
.select("*, venue:venues(name), profile:profiles(name)");
if (venueId && venueId !== "todas") {
query = query.eq("venue_id", venueId);
} else if (user.role !== "superadmin") {
if (user.subscriptionPlan === "Multi-Taller Pro") {
query = query.eq("organization_id", user.organizationId);
} else {
query = query.eq("venue_id", user.venueId);
}
}
const { data, error } = await query.order("created_at", { ascending: false });
if (error) throw new Error(error.message);
return data.map((s) => ({
...s,
_id: s.id,
venueName: s.venue?.name || "Desconocida",
cajero: s.profile?.name || "Sistema",
}));
},
create: async (saleData) => {
const user = getLocalUser();
const { data: sale, error: saleErr } = await supabase
.from("sales")
.insert({
organization_id: user.organizationId,
venue_id: user.venueId,
total: saleData.total,
payment_method: saleData.paymentMethod,
items: saleData.items,
created_by: user._id || user.id,
})
.select()
.single();
if (saleErr) throw new Error(saleErr.message);
// Descontar stock para cada producto
for (const item of saleData.items) {
const { data: currentItem } = await supabase
.from("inventory")
.select("quantity")
.eq("id", item.id)
.single();
const newQty = Math.max(0, (currentItem?.quantity || 0) - item.quantity);
await supabase
.from("inventory")
.update({ quantity: newQty })
.eq("id", item.id);
}
return { ...sale, _id: sale.id };
},
},
// SuperAdmin
super: {
getOrganizations: async () => {
const { data, error } = await supabase
.from("organizations")
.select("*, venues(id), profiles(id)");
if (error) throw new Error(error.message);
return data.map((org) => ({
...org,
_id: org.id,
subscriptionPlan: org.subscription_plan,
subscriptionStatus: org.subscription_status,
createdAt: org.created_at,
venuesCount: Array.isArray(org.venues) ? org.venues.length : 0,
usersCount: Array.isArray(org.profiles) ? org.profiles.length : 0,
}));
},
updateSubscription: async (id, subscriptionPlan, subscriptionStatus) => {
const updateData = {};
if (subscriptionPlan) updateData.subscription_plan = subscriptionPlan;
if (subscriptionStatus) updateData.subscription_status = subscriptionStatus;
const { data, error } = await supabase
.from("organizations")
.update(updateData)
.eq("id", id)
.select()
.single();
if (error) throw new Error(error.message);
return {
...data,
_id: data.id,
subscriptionPlan: data.subscription_plan,
subscriptionStatus: data.subscription_status,
createdAt: data.created_at,
};
},
getUsers: async () => {
const { data, error } = await supabase
.from("profiles")
.select("*, organizationId:organizations(name), venueId:venues(name)");
if (error) throw new Error(error.message);
return data.map((profile) => ({
...profile,
_id: profile.id,
createdAt: profile.created_at,
organizationId: profile.organizationId ? { ...profile.organizationId, _id: profile.organizationId.id } : null,
venueId: profile.venueId ? { ...profile.venueId, _id: profile.venueId.id } : null,
}));
},
updateUserRole: async (id, role) => {
const { data, error } = await supabase
.from("profiles")
.update({ role })
.eq("id", id)
.select()
.single();
if (error) throw new Error(error.message);
return { ...data, _id: data.id };
},
manageUser: async (userId, name, email, role, password = null) => {
const { error } = await supabase.rpc("super_admin_manage_user", {
p_user_id: userId,
p_new_name: name,
p_new_email: email,
p_new_role: role,
p_new_password: password,
});
if (error) throw new Error(error.message);
return { message: "Usuario actualizado correctamente." };
},
deleteUser: async (userId) => {
const { error } = await supabase.rpc("super_admin_delete_user", {
p_user_id: userId,
});
if (error) throw new Error(error.message);
return { message: "Usuario eliminado correctamente." };
},
},
// Cuentas de Acceso de Venue (Para Administradores)
venueAccounts: {
getAll: async () => {
const user = getLocalUser();
if (!user.organizationId || user.organizationId === "undefined") return [];
const { data, error } = await supabase
.from("profiles")
.select("*, venue:venues(*)")
.eq("organization_id", user.organizationId)
.eq("role", "tecnico");
if (error) throw new Error(error.message);
return data.map((p) => ({
...p,
_id: p.id,
venueId: p.venue ? { ...p.venue, _id: p.venue.id } : null,
}));
},
create: async (accountData) => {
const { data, error } = await supabase.rpc("create_venue_account", {
p_email: accountData.email,
p_password: accountData.password,
p_name: accountData.name,
p_venue_id: accountData.venueId,
});
if (error) throw new Error(error.message);
return data;
},
delete: async (accountId) => {
const { error } = await supabase.rpc("delete_venue_account", {
p_user_id: accountId,
});
if (error) throw new Error(error.message);
return { message: "Cuenta eliminada correctamente." };
},
},
};