1388 lines
52 KiB
TypeScript
1388 lines
52 KiB
TypeScript
import { Client, Databases, ID, Query } from 'node-appwrite';
|
|
import {
|
|
City,
|
|
PaymentMethod,
|
|
Order,
|
|
Product,
|
|
Supplier,
|
|
StoreSettings,
|
|
SupplierPost,
|
|
MonitoredWhatsAppGroup,
|
|
MessageTemplate,
|
|
CustomProductRequest,
|
|
Coupon,
|
|
} from '@/types';
|
|
import { INITIAL_CITIES, INITIAL_PAYMENT_METHODS, INITIAL_SUPPLIERS, isGroupIgnoredOrBlacklisted } from './data-service';
|
|
import { DEFAULT_MESSAGE_TEMPLATES } from './templates-service';
|
|
import { sanitizePublicationTitle, sanitizePublicationText } from './sanitizer';
|
|
|
|
const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
|
|
const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
|
|
const apiKey = process.env.APPWRITE_API_KEY || 'standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3';
|
|
const databaseId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID || 'sneakers_multistore_db';
|
|
|
|
const client = new Client()
|
|
.setEndpoint(endpoint)
|
|
.setProject(projectId)
|
|
.setKey(apiKey);
|
|
|
|
export const serverDatabases = new Databases(client);
|
|
|
|
export const DB_ID = databaseId;
|
|
|
|
// =========================================================================
|
|
// 1. CIUDADES Y FLETES (CRUD PERSISTENTE EN APPWRITE)
|
|
// =========================================================================
|
|
export async function getCitiesFromAppwrite(): Promise<City[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'cities', [
|
|
Query.limit(100),
|
|
Query.orderAsc('name'),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => ({
|
|
id: d.$id,
|
|
name: d.name || '',
|
|
department: d.department || 'Colombia',
|
|
shippingCost: typeof d.shippingCost === 'number' ? d.shippingCost : 0,
|
|
deliveryTime: d.deliveryTime || 'Entrega en 24 Horas',
|
|
allowsContraentrega: Boolean(d.allowsContraentrega),
|
|
firstExchangeFree: Boolean(d.firstExchangeFree),
|
|
exchangePolicyNote: d.exchangePolicyNote || '',
|
|
isActive: d.isActive !== false,
|
|
}));
|
|
}
|
|
return INITIAL_CITIES;
|
|
} catch (e: any) {
|
|
console.warn('Error getCitiesFromAppwrite:', e.message);
|
|
return INITIAL_CITIES;
|
|
}
|
|
}
|
|
|
|
export async function createCityInAppwrite(city: Partial<City>): Promise<City> {
|
|
const newId = city.id || `city-${Date.now()}`;
|
|
const payload = {
|
|
name: city.name || 'Nueva Ciudad',
|
|
department: city.department || 'Colombia',
|
|
shippingCost: city.shippingCost || 0,
|
|
deliveryTime: city.deliveryTime || 'Entrega en 24 Horas',
|
|
allowsContraentrega: city.allowsContraentrega !== false,
|
|
firstExchangeFree: Boolean(city.firstExchangeFree),
|
|
exchangePolicyNote: city.exchangePolicyNote || '',
|
|
isActive: city.isActive !== false,
|
|
};
|
|
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'cities', newId, payload);
|
|
return { id: doc.$id, ...payload };
|
|
} catch (e: any) {
|
|
console.error('Error createCityInAppwrite:', e.message);
|
|
return { id: newId, ...payload };
|
|
}
|
|
}
|
|
|
|
export async function updateCityInAppwrite(id: string, updates: Partial<City>): Promise<City | null> {
|
|
try {
|
|
const cleanUpdates: Record<string, any> = {};
|
|
if (updates.name !== undefined) cleanUpdates.name = updates.name;
|
|
if (updates.department !== undefined) cleanUpdates.department = updates.department;
|
|
if (updates.shippingCost !== undefined) cleanUpdates.shippingCost = updates.shippingCost;
|
|
if (updates.deliveryTime !== undefined) cleanUpdates.deliveryTime = updates.deliveryTime;
|
|
if (updates.allowsContraentrega !== undefined) cleanUpdates.allowsContraentrega = updates.allowsContraentrega;
|
|
if (updates.firstExchangeFree !== undefined) cleanUpdates.firstExchangeFree = updates.firstExchangeFree;
|
|
if (updates.exchangePolicyNote !== undefined) cleanUpdates.exchangePolicyNote = updates.exchangePolicyNote;
|
|
if (updates.isActive !== undefined) cleanUpdates.isActive = updates.isActive;
|
|
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'cities', id, cleanUpdates);
|
|
return {
|
|
id: doc.$id,
|
|
name: doc.name,
|
|
department: doc.department,
|
|
shippingCost: doc.shippingCost,
|
|
deliveryTime: doc.deliveryTime,
|
|
allowsContraentrega: doc.allowsContraentrega,
|
|
firstExchangeFree: doc.firstExchangeFree,
|
|
exchangePolicyNote: doc.exchangePolicyNote,
|
|
isActive: doc.isActive,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error updateCityInAppwrite:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteCityInAppwrite(id: string): Promise<boolean> {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'cities', id);
|
|
return true;
|
|
} catch (e: any) {
|
|
console.error('Error deleteCityInAppwrite:', e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 2. MÉTODOS DE PAGO & ANTICIPOS (CRUD PERSISTENTE EN APPWRITE)
|
|
// =========================================================================
|
|
export async function getPaymentMethodsFromAppwrite(): Promise<PaymentMethod[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'payment_methods', [
|
|
Query.limit(100),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => {
|
|
let boldChannel = d.type === 'bold' ? 'all' : undefined;
|
|
let badge = '';
|
|
let icon = '';
|
|
let accountInfo = d.accountInfo || '';
|
|
if (typeof accountInfo === 'string' && accountInfo.startsWith('{')) {
|
|
try {
|
|
const parsed = JSON.parse(accountInfo);
|
|
boldChannel = parsed.boldChannel || boldChannel;
|
|
badge = parsed.badge || '';
|
|
icon = parsed.icon || '';
|
|
accountInfo = parsed.info || '';
|
|
} catch (_) {}
|
|
}
|
|
return {
|
|
id: d.$id,
|
|
name: d.name || '',
|
|
type: d.type || 'contraentrega',
|
|
boldChannel: boldChannel as PaymentMethod['boldChannel'],
|
|
badge: badge || '',
|
|
icon: icon || '',
|
|
description: d.description || '',
|
|
requiresDeposit: Boolean(d.requiresDeposit),
|
|
depositAmount: typeof d.depositAmount === 'number' ? d.depositAmount : 0,
|
|
accountInfo,
|
|
boldPaymentLink: d.boldPaymentLink || '',
|
|
boldQrUrl: d.boldQrUrl || '',
|
|
isActive: d.isActive !== false,
|
|
};
|
|
});
|
|
}
|
|
return INITIAL_PAYMENT_METHODS;
|
|
} catch (e: any) {
|
|
console.warn('Error getPaymentMethodsFromAppwrite:', e.message);
|
|
return INITIAL_PAYMENT_METHODS;
|
|
}
|
|
}
|
|
|
|
export async function createPaymentMethodInAppwrite(method: Partial<PaymentMethod>): Promise<PaymentMethod> {
|
|
const newId = method.id || `pay-${Date.now()}`;
|
|
const meta = {
|
|
boldChannel: method.boldChannel || (method.type === 'bold' ? 'all' : 'deposit'),
|
|
badge: method.badge || '',
|
|
icon: method.icon || '',
|
|
info: method.accountInfo || '',
|
|
};
|
|
const payload: Record<string, any> = {
|
|
name: method.name || 'Nuevo Método de Pago',
|
|
type: method.type || 'contraentrega',
|
|
description: method.description || '',
|
|
requiresDeposit: Boolean(method.requiresDeposit),
|
|
depositAmount: method.depositAmount || 0,
|
|
accountInfo: JSON.stringify(meta),
|
|
boldPaymentLink: method.boldPaymentLink || '',
|
|
boldQrUrl: method.boldQrUrl || '',
|
|
isActive: method.isActive !== false,
|
|
};
|
|
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'payment_methods', newId, payload);
|
|
return {
|
|
id: doc.$id,
|
|
name: doc.name,
|
|
type: doc.type,
|
|
boldChannel: meta.boldChannel as any,
|
|
badge: meta.badge,
|
|
icon: meta.icon,
|
|
description: doc.description,
|
|
requiresDeposit: doc.requiresDeposit,
|
|
depositAmount: doc.depositAmount,
|
|
accountInfo: meta.info,
|
|
boldPaymentLink: doc.boldPaymentLink,
|
|
boldQrUrl: doc.boldQrUrl,
|
|
isActive: doc.isActive,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error createPaymentMethodInAppwrite:', e.message);
|
|
return { id: newId, ...method } as PaymentMethod;
|
|
}
|
|
}
|
|
|
|
export async function updatePaymentMethodInAppwrite(id: string, updates: Partial<PaymentMethod>): Promise<PaymentMethod | null> {
|
|
try {
|
|
// Si viene actualización de metadatos o info, obtener actual para merge
|
|
let currentMeta: Record<string, any> = {};
|
|
try {
|
|
const currentDoc = await serverDatabases.getDocument(DB_ID, 'payment_methods', id);
|
|
if (currentDoc.accountInfo && currentDoc.accountInfo.startsWith('{')) {
|
|
currentMeta = JSON.parse(currentDoc.accountInfo);
|
|
} else {
|
|
currentMeta = { info: currentDoc.accountInfo || '' };
|
|
}
|
|
} catch (_) {}
|
|
|
|
if (updates.boldChannel !== undefined) currentMeta.boldChannel = updates.boldChannel;
|
|
if (updates.badge !== undefined) currentMeta.badge = updates.badge;
|
|
if (updates.icon !== undefined) currentMeta.icon = updates.icon;
|
|
if (updates.accountInfo !== undefined) currentMeta.info = updates.accountInfo;
|
|
|
|
const cleanUpdates: Record<string, any> = {};
|
|
if (updates.name !== undefined) cleanUpdates.name = updates.name;
|
|
if (updates.type !== undefined) cleanUpdates.type = updates.type;
|
|
if (updates.description !== undefined) cleanUpdates.description = updates.description;
|
|
if (updates.requiresDeposit !== undefined) cleanUpdates.requiresDeposit = updates.requiresDeposit;
|
|
if (updates.depositAmount !== undefined) cleanUpdates.depositAmount = updates.depositAmount;
|
|
cleanUpdates.accountInfo = JSON.stringify(currentMeta);
|
|
if (updates.boldPaymentLink !== undefined) cleanUpdates.boldPaymentLink = updates.boldPaymentLink;
|
|
if (updates.boldQrUrl !== undefined) cleanUpdates.boldQrUrl = updates.boldQrUrl;
|
|
if (updates.isActive !== undefined) cleanUpdates.isActive = updates.isActive;
|
|
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'payment_methods', id, cleanUpdates);
|
|
return {
|
|
id: doc.$id,
|
|
name: doc.name,
|
|
type: doc.type,
|
|
boldChannel: currentMeta.boldChannel as PaymentMethod['boldChannel'],
|
|
badge: currentMeta.badge,
|
|
icon: currentMeta.icon,
|
|
description: doc.description,
|
|
requiresDeposit: doc.requiresDeposit,
|
|
depositAmount: doc.depositAmount,
|
|
accountInfo: currentMeta.info,
|
|
boldPaymentLink: doc.boldPaymentLink,
|
|
boldQrUrl: doc.boldQrUrl,
|
|
isActive: doc.isActive,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error updatePaymentMethodInAppwrite:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deletePaymentMethodInAppwrite(id: string): Promise<boolean> {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'payment_methods', id);
|
|
return true;
|
|
} catch (e: any) {
|
|
console.error('Error deletePaymentMethodInAppwrite:', e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sincroniza o restaura los métodos de pago oficiales de Bold en Appwrite
|
|
*/
|
|
export async function syncDefaultPaymentMethodsToAppwrite(): Promise<PaymentMethod[]> {
|
|
try {
|
|
// 1. Obtener existentes en Appwrite
|
|
const existing = await serverDatabases.listDocuments(DB_ID, 'payment_methods', [Query.limit(100)]);
|
|
for (const doc of existing.documents) {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'payment_methods', doc.$id);
|
|
} catch (_) {}
|
|
}
|
|
|
|
// 2. Insertar los métodos oficiales organizados de Bold
|
|
const synced: PaymentMethod[] = [];
|
|
for (const m of INITIAL_PAYMENT_METHODS) {
|
|
const meta = {
|
|
boldChannel: m.boldChannel || 'all',
|
|
badge: m.badge || '',
|
|
icon: m.icon || '',
|
|
info: m.accountInfo || '',
|
|
};
|
|
const payload: Record<string, any> = {
|
|
name: m.name,
|
|
type: m.type,
|
|
description: m.description || '',
|
|
requiresDeposit: Boolean(m.requiresDeposit),
|
|
depositAmount: m.depositAmount || 0,
|
|
accountInfo: JSON.stringify(meta),
|
|
boldPaymentLink: m.boldPaymentLink || '',
|
|
boldQrUrl: m.boldQrUrl || '',
|
|
isActive: m.isActive !== false,
|
|
};
|
|
try {
|
|
const created = await serverDatabases.createDocument(DB_ID, 'payment_methods', m.id, payload);
|
|
synced.push({
|
|
id: created.$id,
|
|
...payload,
|
|
boldChannel: meta.boldChannel,
|
|
badge: meta.badge,
|
|
icon: meta.icon,
|
|
accountInfo: meta.info,
|
|
} as PaymentMethod);
|
|
} catch (err) {
|
|
console.warn(`Error al insertar ${m.id} en Appwrite:`, err);
|
|
synced.push(m);
|
|
}
|
|
}
|
|
return synced;
|
|
} catch (err: any) {
|
|
console.error('Error syncDefaultPaymentMethodsToAppwrite:', err);
|
|
return INITIAL_PAYMENT_METHODS;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 3. PEDIDOS & LOGÍSTICA (CRUD PERSISTENTE EN APPWRITE)
|
|
// =========================================================================
|
|
export async function getOrdersFromAppwrite(): Promise<Order[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'orders', [
|
|
Query.limit(100),
|
|
Query.orderDesc('$createdAt'),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => {
|
|
let items = [];
|
|
try {
|
|
items = typeof d.items === 'string' ? JSON.parse(d.items) : (d.items || []);
|
|
} catch (_) {
|
|
items = [];
|
|
}
|
|
|
|
return {
|
|
id: d.$id,
|
|
customerName: d.customerName || 'Cliente',
|
|
customerPhone: d.customerPhone || '',
|
|
address: d.address || '',
|
|
cityId: d.cityId || 'city-cali',
|
|
cityName: d.cityName || 'Cali',
|
|
items,
|
|
subtotal: typeof d.subtotal === 'number' ? d.subtotal : 0,
|
|
shippingCost: typeof d.shippingCost === 'number' ? d.shippingCost : 0,
|
|
totalAmount: typeof d.totalAmount === 'number' ? d.totalAmount : 0,
|
|
depositAmount: typeof d.depositAmount === 'number' ? d.depositAmount : 10000,
|
|
balanceDue: typeof d.balanceDue === 'number' ? d.balanceDue : 0,
|
|
paymentMethodId: d.paymentMethodId || 'pay-contraentrega',
|
|
paymentMethodName: d.paymentMethodName || 'Contraentrega',
|
|
status: d.status || 'pending',
|
|
notes: d.notes || '',
|
|
couponCode: d.couponCode || '',
|
|
discountAmount: typeof d.discountAmount === 'number' ? d.discountAmount : 0,
|
|
createdAt: d.createdAt || d.$createdAt,
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
} catch (e: any) {
|
|
console.warn('Error getOrdersFromAppwrite:', e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function createOrderInAppwrite(order: Partial<Order>): Promise<Order> {
|
|
const newId = order.id || `ord-${Date.now()}`;
|
|
const total = order.totalAmount || 0;
|
|
const deposit = order.depositAmount !== undefined ? order.depositAmount : 10000;
|
|
const balance = order.balanceDue !== undefined ? order.balanceDue : Math.max(0, total - deposit);
|
|
|
|
const payload = {
|
|
customerName: order.customerName || 'Cliente',
|
|
customerPhone: order.customerPhone || '',
|
|
address: order.address || '',
|
|
cityId: order.cityId || 'city-cali',
|
|
cityName: order.cityName || 'Cali',
|
|
items: JSON.stringify(order.items || []),
|
|
subtotal: order.subtotal || total,
|
|
shippingCost: order.shippingCost || 0,
|
|
totalAmount: total,
|
|
depositAmount: deposit,
|
|
balanceDue: balance,
|
|
paymentMethodId: order.paymentMethodId || 'pay-contraentrega',
|
|
paymentMethodName: order.paymentMethodName || 'Contraentrega',
|
|
status: order.status || 'pending',
|
|
notes: order.notes || '',
|
|
couponCode: order.couponCode || '',
|
|
discountAmount: typeof order.discountAmount === 'number' ? order.discountAmount : 0,
|
|
createdAt: order.createdAt || new Date().toISOString(),
|
|
};
|
|
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'orders', newId, payload);
|
|
|
|
// Si la orden utilizó un cupón de descuento, registrar el uso del cupón
|
|
if (order.couponCode) {
|
|
incrementCouponUsage(order.couponCode).catch((cErr) => {
|
|
console.warn('Error registrando uso de cupón:', cErr.message);
|
|
});
|
|
}
|
|
|
|
return {
|
|
id: doc.$id,
|
|
...payload,
|
|
items: order.items || [],
|
|
status: payload.status as any,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error createOrderInAppwrite:', e.message);
|
|
return {
|
|
id: newId,
|
|
...payload,
|
|
items: order.items || [],
|
|
status: payload.status as any,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function updateOrderInAppwrite(id: string, updates: Partial<Order>): Promise<Order | null> {
|
|
try {
|
|
const cleanUpdates: Record<string, any> = {};
|
|
if (updates.customerName !== undefined) cleanUpdates.customerName = updates.customerName;
|
|
if (updates.customerPhone !== undefined) cleanUpdates.customerPhone = updates.customerPhone;
|
|
if (updates.address !== undefined) cleanUpdates.address = updates.address;
|
|
if (updates.cityId !== undefined) cleanUpdates.cityId = updates.cityId;
|
|
if (updates.cityName !== undefined) cleanUpdates.cityName = updates.cityName;
|
|
if (updates.items !== undefined) cleanUpdates.items = JSON.stringify(updates.items);
|
|
if (updates.subtotal !== undefined) cleanUpdates.subtotal = updates.subtotal;
|
|
if (updates.shippingCost !== undefined) cleanUpdates.shippingCost = updates.shippingCost;
|
|
if (updates.totalAmount !== undefined) cleanUpdates.totalAmount = updates.totalAmount;
|
|
if (updates.depositAmount !== undefined) cleanUpdates.depositAmount = updates.depositAmount;
|
|
if (updates.balanceDue !== undefined) cleanUpdates.balanceDue = updates.balanceDue;
|
|
if (updates.paymentMethodId !== undefined) cleanUpdates.paymentMethodId = updates.paymentMethodId;
|
|
if (updates.paymentMethodName !== undefined) cleanUpdates.paymentMethodName = updates.paymentMethodName;
|
|
if (updates.status !== undefined) cleanUpdates.status = updates.status;
|
|
if (updates.notes !== undefined) cleanUpdates.notes = updates.notes;
|
|
if (updates.couponCode !== undefined) cleanUpdates.couponCode = updates.couponCode;
|
|
if (updates.discountAmount !== undefined) cleanUpdates.discountAmount = updates.discountAmount;
|
|
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'orders', id, cleanUpdates);
|
|
let items = [];
|
|
try {
|
|
items = typeof doc.items === 'string' ? JSON.parse(doc.items) : (doc.items || []);
|
|
} catch (_) {
|
|
items = updates.items || [];
|
|
}
|
|
|
|
return {
|
|
id: doc.$id,
|
|
customerName: doc.customerName,
|
|
customerPhone: doc.customerPhone,
|
|
address: doc.address,
|
|
cityId: doc.cityId,
|
|
cityName: doc.cityName,
|
|
items,
|
|
subtotal: doc.subtotal,
|
|
shippingCost: doc.shippingCost,
|
|
totalAmount: doc.totalAmount,
|
|
depositAmount: doc.depositAmount,
|
|
balanceDue: doc.balanceDue,
|
|
paymentMethodId: doc.paymentMethodId,
|
|
paymentMethodName: doc.paymentMethodName,
|
|
status: doc.status,
|
|
notes: doc.notes,
|
|
createdAt: doc.createdAt || doc.$createdAt,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error updateOrderInAppwrite:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteOrderInAppwrite(id: string): Promise<boolean> {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'orders', id);
|
|
return true;
|
|
} catch (e: any) {
|
|
console.error('Error deleteOrderInAppwrite:', e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 4. PROVEEDORES (CRUD PERSISTENTE EN APPWRITE)
|
|
// =========================================================================
|
|
export async function getSuppliersFromAppwrite(): Promise<Supplier[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'suppliers', [
|
|
Query.limit(100),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => ({
|
|
id: d.$id,
|
|
name: d.name || '',
|
|
phone: d.phone || '',
|
|
contactPerson: d.contactPerson || '',
|
|
address: d.address || '',
|
|
notes: d.notes || '',
|
|
isActive: d.isActive !== false,
|
|
}));
|
|
}
|
|
return INITIAL_SUPPLIERS;
|
|
} catch (e: any) {
|
|
console.warn('Error getSuppliersFromAppwrite:', e.message);
|
|
return INITIAL_SUPPLIERS;
|
|
}
|
|
}
|
|
|
|
export async function createSupplierInAppwrite(supplier: Partial<Supplier>): Promise<Supplier> {
|
|
const newId = supplier.id || `sup-${Date.now()}`;
|
|
const payload = {
|
|
name: supplier.name || 'Nuevo Proveedor',
|
|
phone: supplier.phone || '573167455877',
|
|
contactPerson: supplier.contactPerson || '',
|
|
address: supplier.address || '',
|
|
notes: supplier.notes || '',
|
|
isActive: supplier.isActive !== false,
|
|
};
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'suppliers', newId, payload);
|
|
return { id: doc.$id, ...payload };
|
|
} catch (e: any) {
|
|
return { id: newId, ...payload };
|
|
}
|
|
}
|
|
|
|
export async function updateSupplierInAppwrite(id: string, updates: Partial<Supplier>): Promise<Supplier | null> {
|
|
try {
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'suppliers', id, updates);
|
|
return {
|
|
id: doc.$id,
|
|
name: doc.name,
|
|
phone: doc.phone,
|
|
contactPerson: doc.contactPerson,
|
|
address: doc.address,
|
|
notes: doc.notes,
|
|
isActive: doc.isActive,
|
|
};
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteSupplierInAppwrite(id: string): Promise<boolean> {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'suppliers', id);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 5. CONFIGURACIÓN DE LA TIENDA
|
|
// =========================================================================
|
|
export async function getSettingsFromAppwrite(): Promise<StoreSettings | null> {
|
|
try {
|
|
const doc = await serverDatabases.getDocument(DB_ID, 'store_settings', 'main_settings');
|
|
let boldSandboxIdentityKey = '';
|
|
let boldSandboxSecretKey = '';
|
|
let boldProdIdentityKey = '';
|
|
let boldProdSecretKey = '';
|
|
|
|
if (doc.boldApiKey && typeof doc.boldApiKey === 'string' && doc.boldApiKey.startsWith('{')) {
|
|
try {
|
|
const parsedKeys = JSON.parse(doc.boldApiKey);
|
|
boldSandboxIdentityKey = parsedKeys.boldSandboxIdentityKey || '';
|
|
boldSandboxSecretKey = parsedKeys.boldSandboxSecretKey || '';
|
|
boldProdIdentityKey = parsedKeys.boldProdIdentityKey || '';
|
|
boldProdSecretKey = parsedKeys.boldProdSecretKey || '';
|
|
} catch (_) {}
|
|
}
|
|
|
|
return {
|
|
storeName: doc.storeName || 'DripZone',
|
|
slogan: doc.slogan || '',
|
|
email: doc.email || '',
|
|
phone: doc.phone || '',
|
|
whatsappNumber: doc.whatsappNumber || '',
|
|
instagramHandle: doc.instagramHandle || '',
|
|
address: doc.address || '',
|
|
city: doc.city || 'Cali, Colombia',
|
|
nequiAccount: doc.nequiAccount || '',
|
|
daviplataAccount: doc.daviplataAccount || '',
|
|
accountHolder: doc.accountHolder || '',
|
|
boldPaymentLink: doc.boldPaymentLink || '',
|
|
boldApiKey: doc.boldApiKey || '',
|
|
boldMerchantId: doc.boldMerchantId || '',
|
|
boldQrUrl: doc.boldQrUrl || '',
|
|
boldEnabled: doc.boldEnabled !== false,
|
|
boldEnvironment: (doc.boldMerchantId === 'sandbox' ? 'sandbox' : 'production'),
|
|
boldSandboxIdentityKey,
|
|
boldSandboxSecretKey,
|
|
boldProdIdentityKey,
|
|
boldProdSecretKey,
|
|
defaultProfitMargin: doc.defaultProfitMargin || 35,
|
|
defaultDepositAmount: doc.defaultDepositAmount || 10000,
|
|
};
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function updateSettingsInAppwrite(settings: Partial<StoreSettings>): Promise<StoreSettings> {
|
|
try {
|
|
const cleanUpdates: Record<string, any> = {};
|
|
if (settings.storeName !== undefined) cleanUpdates.storeName = settings.storeName;
|
|
if (settings.slogan !== undefined) cleanUpdates.slogan = settings.slogan;
|
|
if (settings.email !== undefined) cleanUpdates.email = settings.email;
|
|
if (settings.phone !== undefined) cleanUpdates.phone = settings.phone;
|
|
if (settings.whatsappNumber !== undefined) cleanUpdates.whatsappNumber = settings.whatsappNumber;
|
|
if (settings.instagramHandle !== undefined) cleanUpdates.instagramHandle = settings.instagramHandle;
|
|
if (settings.address !== undefined) cleanUpdates.address = settings.address;
|
|
if (settings.city !== undefined) cleanUpdates.city = settings.city;
|
|
if (settings.nequiAccount !== undefined) cleanUpdates.nequiAccount = settings.nequiAccount;
|
|
if (settings.daviplataAccount !== undefined) cleanUpdates.daviplataAccount = settings.daviplataAccount;
|
|
if (settings.accountHolder !== undefined) cleanUpdates.accountHolder = settings.accountHolder;
|
|
if (settings.boldPaymentLink !== undefined) cleanUpdates.boldPaymentLink = settings.boldPaymentLink;
|
|
if (settings.boldQrUrl !== undefined) cleanUpdates.boldQrUrl = settings.boldQrUrl;
|
|
if (settings.boldEnabled !== undefined) cleanUpdates.boldEnabled = settings.boldEnabled;
|
|
if (settings.defaultProfitMargin !== undefined) cleanUpdates.defaultProfitMargin = settings.defaultProfitMargin;
|
|
if (settings.defaultDepositAmount !== undefined) cleanUpdates.defaultDepositAmount = settings.defaultDepositAmount;
|
|
|
|
// Almacenar el entorno (production | sandbox) en boldMerchantId
|
|
if (settings.boldEnvironment !== undefined) {
|
|
cleanUpdates.boldMerchantId = settings.boldEnvironment;
|
|
}
|
|
|
|
// Almacenar claves API en boldApiKey como JSON
|
|
if (
|
|
settings.boldSandboxIdentityKey !== undefined ||
|
|
settings.boldSandboxSecretKey !== undefined ||
|
|
settings.boldProdIdentityKey !== undefined ||
|
|
settings.boldProdSecretKey !== undefined
|
|
) {
|
|
cleanUpdates.boldApiKey = JSON.stringify({
|
|
boldSandboxIdentityKey: settings.boldSandboxIdentityKey || '',
|
|
boldSandboxSecretKey: settings.boldSandboxSecretKey || '',
|
|
boldProdIdentityKey: settings.boldProdIdentityKey || '',
|
|
boldProdSecretKey: settings.boldProdSecretKey || '',
|
|
});
|
|
}
|
|
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'store_settings', 'main_settings', cleanUpdates);
|
|
return {
|
|
storeName: doc.storeName,
|
|
slogan: doc.slogan,
|
|
email: doc.email,
|
|
phone: doc.phone,
|
|
whatsappNumber: doc.whatsappNumber,
|
|
instagramHandle: doc.instagramHandle,
|
|
address: doc.address,
|
|
city: doc.city,
|
|
nequiAccount: doc.nequiAccount,
|
|
daviplataAccount: doc.daviplataAccount,
|
|
accountHolder: doc.accountHolder,
|
|
boldPaymentLink: doc.boldPaymentLink,
|
|
boldApiKey: doc.boldApiKey,
|
|
boldMerchantId: doc.boldMerchantId,
|
|
boldQrUrl: doc.boldQrUrl,
|
|
boldEnabled: doc.boldEnabled,
|
|
boldEnvironment: doc.boldMerchantId === 'sandbox' ? 'sandbox' : 'production',
|
|
boldSandboxIdentityKey: settings.boldSandboxIdentityKey,
|
|
boldSandboxSecretKey: settings.boldSandboxSecretKey,
|
|
boldProdIdentityKey: settings.boldProdIdentityKey,
|
|
boldProdSecretKey: settings.boldProdSecretKey,
|
|
defaultProfitMargin: doc.defaultProfitMargin,
|
|
defaultDepositAmount: doc.defaultDepositAmount,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error updateSettingsInAppwrite:', e.message);
|
|
return settings as StoreSettings;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 6. PRODUCTOS Y CATÁLOGO
|
|
// =========================================================================
|
|
export async function getProductsFromAppwrite(): Promise<Product[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'products', [
|
|
Query.limit(100),
|
|
Query.orderDesc('$createdAt'),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => {
|
|
let sizes = ['37', '38', '39', '40', '41', '42', '43'];
|
|
let images = ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80'];
|
|
let colors = [];
|
|
|
|
try {
|
|
if (d.sizes) sizes = typeof d.sizes === 'string' ? JSON.parse(d.sizes) : d.sizes;
|
|
} catch (_) {}
|
|
|
|
try {
|
|
if (d.images) images = typeof d.images === 'string' ? JSON.parse(d.images) : d.images;
|
|
if (Array.isArray(images)) {
|
|
images = images.map((img: string) => {
|
|
if (img && typeof img === 'string') {
|
|
if (img.startsWith('data:image') && d.whatsappMessageId) {
|
|
return `https://drip-zone.online/api/whatsapp-media?id=${encodeURIComponent(d.whatsappMessageId)}`;
|
|
}
|
|
if (img.startsWith('/')) {
|
|
return `https://drip-zone.online${img}`;
|
|
}
|
|
}
|
|
return img;
|
|
});
|
|
}
|
|
} catch (_) {}
|
|
|
|
try {
|
|
if (d.colors) colors = typeof d.colors === 'string' ? JSON.parse(d.colors) : d.colors;
|
|
} catch (_) {}
|
|
|
|
const brand = d.brand || 'Sneakers';
|
|
const cleanName = sanitizePublicationTitle(d.name || 'Zapatillas', [d.supplierName], brand);
|
|
const cleanDesc = sanitizePublicationText(d.description || '', [d.supplierName], d.supplierPrice);
|
|
|
|
return {
|
|
id: d.$id,
|
|
name: cleanName,
|
|
modelName: cleanName,
|
|
colorName: d.colorName || '',
|
|
brand: brand,
|
|
supplierPrice: d.supplierPrice || 90000,
|
|
retailPrice: d.retailPrice || 125000,
|
|
originalPrice: d.originalPrice || Math.round((d.retailPrice || 125000) * 1.35),
|
|
sizes,
|
|
images,
|
|
colors,
|
|
description: cleanDesc,
|
|
category: d.category || 'Calzado / Sneakers',
|
|
inStock: d.inStock !== false,
|
|
isActive: d.isActive !== false,
|
|
supplierName: d.supplierName || '',
|
|
supplierPhone: d.supplierPhone || '',
|
|
whatsappMessageId: d.whatsappMessageId || '',
|
|
createdAt: d.createdAt || d.$createdAt || new Date().toISOString(),
|
|
publishedAt: d.publishedAt || d.createdAt || d.$createdAt || new Date().toISOString(),
|
|
autoDisabledAt: d.autoDisabledAt || undefined,
|
|
manuallyEnabled: d.manuallyEnabled === true || d.manuallyEnabled === 'true' || false,
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
} catch (e: any) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function saveProductToAppwrite(product: Product): Promise<Product> {
|
|
const brand = product.brand || 'Sneakers';
|
|
const cleanName = sanitizePublicationTitle(product.name, [product.supplierName], brand);
|
|
const cleanDesc = sanitizePublicationText(product.description || '', [product.supplierName], product.supplierPrice);
|
|
|
|
const payload = {
|
|
name: cleanName,
|
|
modelName: cleanName,
|
|
brand: brand,
|
|
colorName: product.colorName || '',
|
|
supplierPrice: product.supplierPrice || 0,
|
|
retailPrice: product.retailPrice || 0,
|
|
originalPrice: product.originalPrice || Math.round((product.retailPrice || 0) * 1.35),
|
|
sizes: JSON.stringify(product.sizes || []),
|
|
images: JSON.stringify(product.images || []),
|
|
colors: JSON.stringify(product.colors || []),
|
|
description: cleanDesc,
|
|
category: product.category || 'Sneakers',
|
|
inStock: product.inStock !== false,
|
|
isActive: product.isActive !== false,
|
|
supplierName: product.supplierName || '',
|
|
supplierPhone: product.supplierPhone || '',
|
|
whatsappMessageId: product.whatsappMessageId || '',
|
|
};
|
|
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'products', product.id, payload);
|
|
return { ...product, name: cleanName, modelName: cleanName, description: cleanDesc };
|
|
} catch (e: any) {
|
|
if (e.code === 409) {
|
|
try {
|
|
await serverDatabases.updateDocument(DB_ID, 'products', product.id, payload);
|
|
} catch (_) {}
|
|
}
|
|
return { ...product, name: cleanName, modelName: cleanName, description: cleanDesc };
|
|
}
|
|
}
|
|
|
|
export async function deleteProductFromAppwrite(id: string): Promise<boolean> {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'products', id);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 7. FEED WHATSAPP (POSTS PERSISTENTES EN APPWRITE)
|
|
// =========================================================================
|
|
export async function getSupplierPostsFromAppwrite(): Promise<SupplierPost[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'supplier_posts', [
|
|
Query.limit(100),
|
|
Query.orderDesc('$createdAt'),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => {
|
|
let rawImages: string[] = [];
|
|
let extractedProduct = undefined;
|
|
try {
|
|
rawImages = typeof d.rawImages === 'string' ? JSON.parse(d.rawImages) : (d.rawImages || []);
|
|
} catch (_) {}
|
|
try {
|
|
extractedProduct = typeof d.extractedProduct === 'string' ? JSON.parse(d.extractedProduct) : d.extractedProduct;
|
|
} catch (_) {}
|
|
|
|
return {
|
|
id: d.$id,
|
|
whatsappMessageId: d.whatsappMessageId || d.$id.replace('post-', ''),
|
|
channelName: d.channelName || '',
|
|
channelJid: d.channelJid || '',
|
|
supplierName: d.supplierName || '',
|
|
supplierPhone: d.supplierPhone || '',
|
|
rawMessage: d.rawMessage || '',
|
|
rawImages,
|
|
extractedProduct,
|
|
status: d.status || 'pending_review',
|
|
receivedAt: d.receivedAt || d.$createdAt,
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
} catch (e: any) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<SupplierPost> {
|
|
const whatsappMessageId = post.whatsappMessageId || post.id.replace('post-', '');
|
|
const payload = {
|
|
whatsappMessageId,
|
|
channelName: post.channelName || 'Grupo WhatsApp',
|
|
channelJid: post.channelJid || '',
|
|
supplierName: post.supplierName || '',
|
|
supplierPhone: post.supplierPhone || '',
|
|
rawMessage: post.rawMessage || '',
|
|
rawImages: JSON.stringify(post.rawImages || []),
|
|
extractedProduct: JSON.stringify(post.extractedProduct || {}),
|
|
status: post.status || 'pending_review',
|
|
receivedAt: post.receivedAt || new Date().toISOString(),
|
|
};
|
|
|
|
try {
|
|
await serverDatabases.createDocument(DB_ID, 'supplier_posts', post.id, payload);
|
|
return { ...post, whatsappMessageId };
|
|
} catch (e: any) {
|
|
if (e.code === 409) {
|
|
try {
|
|
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', post.id, payload);
|
|
} catch (_) {}
|
|
}
|
|
return { ...post, whatsappMessageId };
|
|
}
|
|
}
|
|
|
|
export async function updateSupplierPostStatusInAppwrite(
|
|
id: string,
|
|
status: SupplierPost['status']
|
|
): Promise<boolean> {
|
|
const cleanId = id.trim();
|
|
const whatsappMessageId = cleanId.replace('post-', '');
|
|
try {
|
|
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', cleanId, {
|
|
status,
|
|
whatsappMessageId,
|
|
});
|
|
return true;
|
|
} catch (e) {
|
|
try {
|
|
await serverDatabases.createDocument(DB_ID, 'supplier_posts', cleanId, {
|
|
status,
|
|
whatsappMessageId,
|
|
receivedAt: new Date().toISOString(),
|
|
});
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 8. GRUPOS WHATSAPP MONITOREADOS (PERSISTENCIA APPWRITE)
|
|
// =========================================================================
|
|
export async function getMonitoredGroupsFromAppwrite(): Promise<MonitoredWhatsAppGroup[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'monitored_groups', [
|
|
Query.limit(100),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => {
|
|
const isBlacklisted = isGroupIgnoredOrBlacklisted(d.jid, d.name);
|
|
return {
|
|
id: d.$id,
|
|
jid: d.jid || '',
|
|
name: d.name || '',
|
|
isMonitored: isBlacklisted ? false : Boolean(d.isMonitored),
|
|
autoPublish: isBlacklisted ? false : d.autoPublish !== false,
|
|
notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : d.notes || '',
|
|
supplierName: d.name || '',
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
} catch (e: any) {
|
|
console.warn('Error getMonitoredGroupsFromAppwrite:', e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function saveMonitoredGroupToAppwrite(
|
|
group: Partial<MonitoredWhatsAppGroup>
|
|
): Promise<MonitoredWhatsAppGroup> {
|
|
const isBlacklisted = isGroupIgnoredOrBlacklisted(group.jid || '', group.name);
|
|
const docId = group.id || `grp-${(group.jid || '').replace(/\D/g, '').slice(-8)}` || `grp-${Date.now()}`;
|
|
const payload = {
|
|
jid: group.jid || '',
|
|
name: group.name || '',
|
|
isMonitored: isBlacklisted ? false : Boolean(group.isMonitored),
|
|
autoPublish: isBlacklisted ? false : group.autoPublish !== false,
|
|
notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : group.notes || '',
|
|
};
|
|
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'monitored_groups', docId, payload);
|
|
return {
|
|
id: doc.$id,
|
|
jid: doc.jid,
|
|
name: doc.name,
|
|
isMonitored: doc.isMonitored,
|
|
autoPublish: doc.autoPublish,
|
|
notes: doc.notes,
|
|
supplierName: doc.name,
|
|
};
|
|
} catch (e: any) {
|
|
if (e.code === 409) {
|
|
try {
|
|
await serverDatabases.updateDocument(DB_ID, 'monitored_groups', docId, payload);
|
|
} catch (_) {}
|
|
}
|
|
return {
|
|
id: docId,
|
|
jid: payload.jid,
|
|
name: payload.name,
|
|
isMonitored: payload.isMonitored,
|
|
autoPublish: payload.autoPublish,
|
|
notes: payload.notes,
|
|
supplierName: payload.name,
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function toggleMonitoredGroupInAppwrite(
|
|
idOrJid: string,
|
|
isMonitored: boolean
|
|
): Promise<boolean> {
|
|
try {
|
|
const isBlacklisted = isGroupIgnoredOrBlacklisted(idOrJid);
|
|
const finalState = isBlacklisted ? false : isMonitored;
|
|
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'monitored_groups', [Query.limit(100)]);
|
|
const matchingDocs = res.documents.filter(
|
|
(d: any) => d.$id === idOrJid || d.jid === idOrJid || d.name === idOrJid
|
|
);
|
|
|
|
if (matchingDocs.length > 0) {
|
|
await Promise.all(
|
|
matchingDocs.map((doc: any) =>
|
|
serverDatabases.updateDocument(DB_ID, 'monitored_groups', doc.$id, {
|
|
isMonitored: finalState,
|
|
autoPublish: finalState,
|
|
notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : doc.notes,
|
|
})
|
|
)
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 9. PLANTILLAS DE MENSAJES & NOTIFICACIONES (CRUD APPWRITE)
|
|
// =========================================================================
|
|
export async function getMessageTemplatesFromAppwrite(): Promise<MessageTemplate[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'message_templates', [
|
|
Query.limit(50),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => {
|
|
let vars = [];
|
|
try {
|
|
vars = typeof d.availableVariables === 'string' ? JSON.parse(d.availableVariables) : (d.availableVariables || []);
|
|
} catch (_) {}
|
|
return {
|
|
id: d.$id,
|
|
title: d.title || '',
|
|
description: d.description || '',
|
|
category: d.category || 'customer_sales',
|
|
channel: d.channel || 'whatsapp',
|
|
content: d.content || '',
|
|
defaultContent: d.content || '',
|
|
availableVariables: vars,
|
|
isActive: d.isActive !== false,
|
|
updatedAt: d.updatedAt || d.$updatedAt,
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
} catch (e: any) {
|
|
console.warn('Error getMessageTemplatesFromAppwrite:', e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function updateMessageTemplateInAppwrite(
|
|
id: string,
|
|
updates: Partial<MessageTemplate>
|
|
): Promise<MessageTemplate | null> {
|
|
try {
|
|
const payload: Record<string, any> = {
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
if (updates.title !== undefined) payload.title = updates.title;
|
|
if (updates.description !== undefined) payload.description = updates.description;
|
|
if (updates.content !== undefined) payload.content = updates.content;
|
|
if (updates.isActive !== undefined) payload.isActive = updates.isActive;
|
|
if (updates.category !== undefined) payload.category = updates.category;
|
|
if (updates.channel !== undefined) payload.channel = updates.channel;
|
|
if (updates.availableVariables !== undefined) {
|
|
payload.availableVariables = typeof updates.availableVariables === 'string'
|
|
? updates.availableVariables
|
|
: JSON.stringify(updates.availableVariables);
|
|
}
|
|
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'message_templates', id, payload);
|
|
let vars = [];
|
|
try {
|
|
vars = typeof doc.availableVariables === 'string' ? JSON.parse(doc.availableVariables) : (doc.availableVariables || []);
|
|
} catch (_) {}
|
|
|
|
return {
|
|
id: doc.$id,
|
|
title: doc.title,
|
|
description: doc.description,
|
|
category: doc.category,
|
|
channel: doc.channel,
|
|
content: doc.content,
|
|
defaultContent: doc.content,
|
|
availableVariables: vars,
|
|
isActive: doc.isActive !== false,
|
|
updatedAt: doc.updatedAt,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error updateMessageTemplateInAppwrite:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// ENCARGOS Y PEDIDOS ESPECIALES (CUSTOM REQUESTS)
|
|
// =========================================================================
|
|
|
|
export async function fetchCustomRequestsFromAppwrite(): Promise<CustomProductRequest[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'custom_requests', [
|
|
Query.orderDesc('$createdAt'),
|
|
Query.limit(100),
|
|
]);
|
|
return res.documents.map((doc: any) => ({
|
|
id: doc.$id,
|
|
userId: doc.userId,
|
|
customerName: doc.customerName,
|
|
customerPhone: doc.customerPhone,
|
|
customerEmail: doc.customerEmail,
|
|
customerCity: doc.customerCity,
|
|
brand: doc.brand,
|
|
modelName: doc.modelName,
|
|
size: doc.size,
|
|
colorway: doc.colorway,
|
|
targetBudget: doc.targetBudget,
|
|
notes: doc.notes,
|
|
imageUrl: doc.imageUrl,
|
|
status: doc.status as any,
|
|
quotedPrice: doc.quotedPrice,
|
|
adminNotes: doc.adminNotes,
|
|
createdAt: doc.createdAt || doc.$createdAt,
|
|
updatedAt: doc.updatedAt || doc.$updatedAt,
|
|
}));
|
|
} catch (e: any) {
|
|
console.warn('Error fetchCustomRequestsFromAppwrite:', e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function createCustomRequestInAppwrite(req: CustomProductRequest): Promise<CustomProductRequest> {
|
|
const newId = req.id && !req.id.includes(' ') ? req.id.replace(/[^a-zA-Z0-9._-]/g, '-') : ID.unique();
|
|
const payload: Record<string, any> = {
|
|
customerName: req.customerName,
|
|
customerPhone: req.customerPhone,
|
|
customerEmail: req.customerEmail || '',
|
|
customerCity: req.customerCity || 'Colombia',
|
|
brand: req.brand,
|
|
modelName: req.modelName,
|
|
size: req.size,
|
|
colorway: req.colorway || '',
|
|
notes: req.notes || '',
|
|
imageUrl: req.imageUrl || '',
|
|
status: req.status || 'pending',
|
|
createdAt: req.createdAt || new Date().toISOString(),
|
|
};
|
|
if (req.targetBudget !== undefined && req.targetBudget !== null) payload.targetBudget = req.targetBudget;
|
|
if (req.quotedPrice !== undefined && req.quotedPrice !== null) payload.quotedPrice = req.quotedPrice;
|
|
if (req.adminNotes) payload.adminNotes = req.adminNotes;
|
|
if (req.userId) payload.userId = req.userId;
|
|
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'custom_requests', newId, payload);
|
|
return {
|
|
...req,
|
|
id: doc.$id,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error createCustomRequestInAppwrite:', e.message);
|
|
return req;
|
|
}
|
|
}
|
|
|
|
export async function updateCustomRequestInAppwrite(
|
|
id: string,
|
|
updates: Partial<CustomProductRequest>
|
|
): Promise<CustomProductRequest | null> {
|
|
try {
|
|
const payload: Record<string, any> = {
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
if (updates.customerName !== undefined) payload.customerName = updates.customerName;
|
|
if (updates.customerPhone !== undefined) payload.customerPhone = updates.customerPhone;
|
|
if (updates.customerEmail !== undefined) payload.customerEmail = updates.customerEmail;
|
|
if (updates.customerCity !== undefined) payload.customerCity = updates.customerCity;
|
|
if (updates.brand !== undefined) payload.brand = updates.brand;
|
|
if (updates.modelName !== undefined) payload.modelName = updates.modelName;
|
|
if (updates.size !== undefined) payload.size = updates.size;
|
|
if (updates.colorway !== undefined) payload.colorway = updates.colorway;
|
|
if (updates.notes !== undefined) payload.notes = updates.notes;
|
|
if (updates.imageUrl !== undefined) payload.imageUrl = updates.imageUrl;
|
|
if (updates.status !== undefined) payload.status = updates.status;
|
|
if (updates.targetBudget !== undefined) payload.targetBudget = updates.targetBudget;
|
|
if (updates.quotedPrice !== undefined) payload.quotedPrice = updates.quotedPrice;
|
|
if (updates.adminNotes !== undefined) payload.adminNotes = updates.adminNotes;
|
|
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'custom_requests', id, payload);
|
|
return {
|
|
id: doc.$id,
|
|
userId: doc.userId,
|
|
customerName: doc.customerName,
|
|
customerPhone: doc.customerPhone,
|
|
customerEmail: doc.customerEmail,
|
|
customerCity: doc.customerCity,
|
|
brand: doc.brand,
|
|
modelName: doc.modelName,
|
|
size: doc.size,
|
|
colorway: doc.colorway,
|
|
targetBudget: doc.targetBudget,
|
|
notes: doc.notes,
|
|
imageUrl: doc.imageUrl,
|
|
status: doc.status as any,
|
|
quotedPrice: doc.quotedPrice,
|
|
adminNotes: doc.adminNotes,
|
|
createdAt: doc.createdAt || doc.$createdAt,
|
|
updatedAt: doc.updatedAt || doc.$updatedAt,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error updateCustomRequestInAppwrite:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteCustomRequestInAppwrite(id: string): Promise<boolean> {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'custom_requests', id);
|
|
return true;
|
|
} catch (e: any) {
|
|
console.error('Error deleteCustomRequestInAppwrite:', e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 12. CUPONES Y PROMOCIONES (CRUD PERSISTENTE EN APPWRITE)
|
|
// =========================================================================
|
|
export async function getCouponsFromAppwrite(): Promise<Coupon[]> {
|
|
try {
|
|
const res = await serverDatabases.listDocuments(DB_ID, 'coupons', [
|
|
Query.limit(100),
|
|
Query.orderDesc('$createdAt'),
|
|
]);
|
|
if (res.documents && res.documents.length > 0) {
|
|
return res.documents.map((d: any) => {
|
|
let categories: string[] = [];
|
|
try {
|
|
if (d.applicableCategories) {
|
|
categories = typeof d.applicableCategories === 'string'
|
|
? JSON.parse(d.applicableCategories)
|
|
: d.applicableCategories;
|
|
}
|
|
} catch (_) {
|
|
categories = [];
|
|
}
|
|
|
|
return {
|
|
id: d.$id,
|
|
code: (d.code || '').toUpperCase().trim(),
|
|
description: d.description || '',
|
|
discountType: (d.discountType || 'percentage') as 'percentage' | 'fixed',
|
|
discountValue: Number(d.discountValue) || 0,
|
|
minSpend: Number(d.minSpend) || 0,
|
|
maxDiscount: Number(d.maxDiscount) || 0,
|
|
usageLimit: Number(d.usageLimit) || 0,
|
|
usageCount: Number(d.usageCount) || 0,
|
|
expiresAt: d.expiresAt || undefined,
|
|
startDate: d.startDate || undefined,
|
|
isActive: d.isActive !== false,
|
|
applicableCategories: categories,
|
|
createdAt: d.$createdAt || d.createdAt,
|
|
updatedAt: d.$updatedAt || d.updatedAt,
|
|
};
|
|
});
|
|
}
|
|
return [];
|
|
} catch (e: any) {
|
|
console.warn('Error getCouponsFromAppwrite:', e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function getCouponByCodeFromAppwrite(code: string): Promise<Coupon | null> {
|
|
if (!code) return null;
|
|
const cleanCode = code.toUpperCase().trim();
|
|
try {
|
|
const coupons = await getCouponsFromAppwrite();
|
|
const found = coupons.find((c) => c.code === cleanCode);
|
|
return found || null;
|
|
} catch (e: any) {
|
|
console.warn('Error getCouponByCodeFromAppwrite:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function createCouponInAppwrite(coupon: Partial<Coupon>): Promise<Coupon> {
|
|
const cleanCode = (coupon.code || `CUPON-${Date.now()}`).toUpperCase().trim();
|
|
const newId = coupon.id || `cup-${cleanCode.toLowerCase().replace(/[^a-z0-9]/g, '')}-${Date.now().toString(36).slice(-4)}`;
|
|
|
|
const payload = {
|
|
code: cleanCode,
|
|
description: coupon.description || '',
|
|
discountType: coupon.discountType || 'percentage',
|
|
discountValue: typeof coupon.discountValue === 'number' ? coupon.discountValue : 10,
|
|
minSpend: typeof coupon.minSpend === 'number' ? coupon.minSpend : 0,
|
|
maxDiscount: typeof coupon.maxDiscount === 'number' ? coupon.maxDiscount : 0,
|
|
usageLimit: typeof coupon.usageLimit === 'number' ? coupon.usageLimit : 0,
|
|
usageCount: typeof coupon.usageCount === 'number' ? coupon.usageCount : 0,
|
|
expiresAt: coupon.expiresAt || '',
|
|
startDate: coupon.startDate || new Date().toISOString(),
|
|
isActive: coupon.isActive !== false,
|
|
applicableCategories: Array.isArray(coupon.applicableCategories) ? JSON.stringify(coupon.applicableCategories) : (coupon.applicableCategories || ''),
|
|
};
|
|
|
|
try {
|
|
const doc = await serverDatabases.createDocument(DB_ID, 'coupons', newId, payload);
|
|
return {
|
|
id: doc.$id,
|
|
...payload,
|
|
applicableCategories: coupon.applicableCategories || [],
|
|
createdAt: doc.$createdAt,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error createCouponInAppwrite:', e.message);
|
|
return {
|
|
id: newId,
|
|
...payload,
|
|
applicableCategories: coupon.applicableCategories || [],
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function updateCouponInAppwrite(id: string, updates: Partial<Coupon>): Promise<Coupon | null> {
|
|
try {
|
|
const cleanUpdates: Record<string, any> = {};
|
|
if (updates.code !== undefined) cleanUpdates.code = updates.code.toUpperCase().trim();
|
|
if (updates.description !== undefined) cleanUpdates.description = updates.description;
|
|
if (updates.discountType !== undefined) cleanUpdates.discountType = updates.discountType;
|
|
if (updates.discountValue !== undefined) cleanUpdates.discountValue = updates.discountValue;
|
|
if (updates.minSpend !== undefined) cleanUpdates.minSpend = updates.minSpend;
|
|
if (updates.maxDiscount !== undefined) cleanUpdates.maxDiscount = updates.maxDiscount;
|
|
if (updates.usageLimit !== undefined) cleanUpdates.usageLimit = updates.usageLimit;
|
|
if (updates.usageCount !== undefined) cleanUpdates.usageCount = updates.usageCount;
|
|
if (updates.expiresAt !== undefined) cleanUpdates.expiresAt = updates.expiresAt || '';
|
|
if (updates.startDate !== undefined) cleanUpdates.startDate = updates.startDate || '';
|
|
if (updates.isActive !== undefined) cleanUpdates.isActive = updates.isActive;
|
|
if (updates.applicableCategories !== undefined) {
|
|
cleanUpdates.applicableCategories = Array.isArray(updates.applicableCategories)
|
|
? JSON.stringify(updates.applicableCategories)
|
|
: updates.applicableCategories;
|
|
}
|
|
|
|
const doc = await serverDatabases.updateDocument(DB_ID, 'coupons', id, cleanUpdates);
|
|
return {
|
|
id: doc.$id,
|
|
code: doc.code,
|
|
description: doc.description,
|
|
discountType: doc.discountType as any,
|
|
discountValue: doc.discountValue,
|
|
minSpend: doc.minSpend,
|
|
maxDiscount: doc.maxDiscount,
|
|
usageLimit: doc.usageLimit,
|
|
usageCount: doc.usageCount,
|
|
expiresAt: doc.expiresAt,
|
|
startDate: doc.startDate,
|
|
isActive: doc.isActive !== false,
|
|
applicableCategories: updates.applicableCategories || [],
|
|
createdAt: doc.$createdAt,
|
|
updatedAt: doc.$updatedAt,
|
|
};
|
|
} catch (e: any) {
|
|
console.error('Error updateCouponInAppwrite:', e.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteCouponInAppwrite(id: string): Promise<boolean> {
|
|
try {
|
|
await serverDatabases.deleteDocument(DB_ID, 'coupons', id);
|
|
return true;
|
|
} catch (e: any) {
|
|
console.error('Error deleteCouponInAppwrite:', e.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export async function incrementCouponUsage(idOrCode: string): Promise<void> {
|
|
if (!idOrCode) return;
|
|
try {
|
|
const coupons = await getCouponsFromAppwrite();
|
|
const clean = idOrCode.toUpperCase().trim();
|
|
const matched = coupons.find((c) => c.id === idOrCode || c.code.toUpperCase() === clean);
|
|
if (matched) {
|
|
await updateCouponInAppwrite(matched.id, {
|
|
usageCount: (matched.usageCount || 0) + 1,
|
|
});
|
|
}
|
|
} catch (e: any) {
|
|
console.warn('Error incrementCouponUsage:', e.message);
|
|
}
|
|
}
|
|
|
|
|
|
|