Spaces:
Sleeping
Sleeping
| /** | |
| * Customer Manager - Pini Print Bot | |
| * ================================== | |
| * ื ืืืื ืืงืืืืช ืขื ืืืืื ืืคื ืืืคืื | |
| * ืืืกืืืจืืืช ืืืื ืืช, ืืขืืคืืช, ื-CRM ืืกืืกื | |
| */ | |
| // === ืืืืจ ืืงืืืืช (ืืืืืจืื - ืืคืจืืืงืฉื: MongoDB/PostgreSQL) === | |
| const customers = new Map(); | |
| // === ืืื ื ืืงืื === | |
| function createCustomer(phone, name = null) { | |
| return { | |
| id: `cust_${Date.now()}`, | |
| phone: normalizePhone(phone), | |
| name: name, | |
| email: null, | |
| // ืกืืืืกืืืงืืช | |
| stats: { | |
| totalOrders: 0, | |
| totalSpent: 0, | |
| firstOrder: null, | |
| lastOrder: null, | |
| averageOrder: 0 | |
| }, | |
| // ืืขืืคืืช ืฉื ืืืื | |
| preferences: { | |
| preferredProducts: [], // ืืืฆืจืื ืฉืืืืื ืืื ืืจืื | |
| preferredMaterials: [], // ืืืืจืื ืืืขืืคืื | |
| usualQuantities: {}, // ืืืืืืช ืจืืืืืช ืืคื ืืืฆืจ | |
| priceRange: null, // ืืืื ืืืืจืื ืจืืื | |
| designStatus: 'unknown' // ืืื ืื"ื ืืืืข ืขื ืขืืฆืื | |
| }, | |
| // ืืืกืืืจืื | |
| orderHistory: [], // ืืืื ืืช ืงืืืืืช | |
| quoteHistory: [], // ืืฆืขืืช ืืืืจ (ืื ืืื ืฉืื ืืคืื ืืืืื ื) | |
| // ืืขืจืืช | |
| notes: [], // ืืขืจืืช ืฉื ืืืช ืืืคืืก | |
| tags: [], // ืชืืืืช: 'VIP', 'ืขืกืงื', 'ืคืจืื', 'ืืขืืืชื' | |
| // ืืื | |
| createdAt: new Date(), | |
| updatedAt: new Date() | |
| }; | |
| } | |
| // === ื ืจืืื ืืืคืื === | |
| function normalizePhone(phone) { | |
| if (!phone) return null; | |
| // ืืกืจ ืื ืื ืฉืื ืกืคืจื | |
| let normalized = phone.replace(/\D/g, ''); | |
| // ืืืจ 972 ื-0 | |
| if (normalized.startsWith('972')) { | |
| normalized = '0' + normalized.slice(3); | |
| } | |
| // ืืืื ืฉืืชืืื ื-0 | |
| if (!normalized.startsWith('0') && normalized.length === 9) { | |
| normalized = '0' + normalized; | |
| } | |
| return normalized; | |
| } | |
| // === ืืืคืืฉ/ืืฆืืจืช ืืงืื === | |
| function findOrCreateCustomer(phone, name = null) { | |
| const normalizedPhone = normalizePhone(phone); | |
| if (!normalizedPhone) { | |
| return null; | |
| } | |
| // ืืคืฉ ืืงืื ืงืืื | |
| let customer = customers.get(normalizedPhone); | |
| if (customer) { | |
| // ืขืืื ืฉื ืื ื ืืชื ืืืฉ | |
| if (name && !customer.name) { | |
| customer.name = name; | |
| customer.updatedAt = new Date(); | |
| } | |
| console.log(` ๐ค Found existing customer: ${customer.name || normalizedPhone}`); | |
| return customer; | |
| } | |
| // ืฆืืจ ืืงืื ืืืฉ | |
| customer = createCustomer(normalizedPhone, name); | |
| customers.set(normalizedPhone, customer); | |
| console.log(` ๐ค Created new customer: ${name || normalizedPhone}`); | |
| return customer; | |
| } | |
| // === ืืืคืืฉ ืืงืื ืืคื ืืืคืื === | |
| function getCustomerByPhone(phone) { | |
| const normalizedPhone = normalizePhone(phone); | |
| return customers.get(normalizedPhone) || null; | |
| } | |
| // === ืขืืืื ืืงืื ืืืจื ืืืื ื === | |
| function updateCustomerAfterOrder(phone, order) { | |
| const customer = getCustomerByPhone(phone); | |
| if (!customer) return null; | |
| // ืขืืื ืกืืืืกืืืงืืช | |
| customer.stats.totalOrders++; | |
| customer.stats.totalSpent += order.total; | |
| customer.stats.lastOrder = new Date(); | |
| if (!customer.stats.firstOrder) { | |
| customer.stats.firstOrder = new Date(); | |
| } | |
| customer.stats.averageOrder = Math.round(customer.stats.totalSpent / customer.stats.totalOrders); | |
| // ืขืืื ืืขืืคืืช | |
| order.items.forEach(item => { | |
| // ืืืฆืจืื ืืืขืืคืื | |
| const prodIndex = customer.preferences.preferredProducts.findIndex(p => p.name === item.product_name); | |
| if (prodIndex >= 0) { | |
| customer.preferences.preferredProducts[prodIndex].count++; | |
| } else { | |
| customer.preferences.preferredProducts.push({ name: item.product_name, count: 1 }); | |
| } | |
| // ืืืืืืช ืจืืืืืช | |
| if (!customer.preferences.usualQuantities[item.product_name]) { | |
| customer.preferences.usualQuantities[item.product_name] = []; | |
| } | |
| customer.preferences.usualQuantities[item.product_name].push(item.qty); | |
| }); | |
| // ืืืื ืืืขืืคืื | |
| customer.preferences.preferredProducts.sort((a, b) => b.count - a.count); | |
| // ืฉืืืจ ืืืืกืืืจืื | |
| customer.orderHistory.push({ | |
| id: `ord_${Date.now()}`, | |
| date: new Date(), | |
| items: order.items, | |
| total: order.total, | |
| status: 'confirmed' | |
| }); | |
| customer.updatedAt = new Date(); | |
| return customer; | |
| } | |
| // === ืฉืืืจืช ืืฆืขืช ืืืืจ (ืื ืื ืื ืืคืื ืืืืื ื) === | |
| function saveQuoteToCustomer(phone, cart, total) { | |
| const customer = getCustomerByPhone(phone); | |
| if (!customer) return null; | |
| customer.quoteHistory.push({ | |
| id: `quote_${Date.now()}`, | |
| date: new Date(), | |
| items: cart.map(i => ({ | |
| product_name: i.product_name, | |
| qty: i.qty, | |
| price: i.client_price | |
| })), | |
| total, | |
| convertedToOrder: false | |
| }); | |
| customer.updatedAt = new Date(); | |
| return customer; | |
| } | |
| // === ืืืกืคืช ืืขืจื ืืืงืื === | |
| function addNoteToCustomer(phone, note, author = 'system') { | |
| const customer = getCustomerByPhone(phone); | |
| if (!customer) return null; | |
| customer.notes.push({ | |
| text: note, | |
| author, | |
| date: new Date() | |
| }); | |
| customer.updatedAt = new Date(); | |
| return customer; | |
| } | |
| // === ืืืกืคืช ืชืืืช === | |
| function addTagToCustomer(phone, tag) { | |
| const customer = getCustomerByPhone(phone); | |
| if (!customer) return null; | |
| if (!customer.tags.includes(tag)) { | |
| customer.tags.push(tag); | |
| customer.updatedAt = new Date(); | |
| } | |
| return customer; | |
| } | |
| // === ืงืืืช ืกืืืื ืืงืื ืืืฉืืืจื === | |
| function getCustomerSummary(phone) { | |
| const customer = getCustomerByPhone(phone); | |
| if (!customer) return null; | |
| // ืืฉื ืืืืช ืจืืืื ืืืืฆืขืช ืืื ืืืฆืจ | |
| const usualQuantities = {}; | |
| for (const [product, quantities] of Object.entries(customer.preferences.usualQuantities)) { | |
| const avg = Math.round(quantities.reduce((a, b) => a + b, 0) / quantities.length); | |
| usualQuantities[product] = avg; | |
| } | |
| return { | |
| id: customer.id, | |
| name: customer.name, | |
| phone: customer.phone, | |
| // ืชืืฆืืช ืืืฉืืืจื | |
| isNew: customer.stats.totalOrders === 0, | |
| isVIP: customer.tags.includes('VIP'), | |
| isReturning: customer.stats.totalOrders > 0, | |
| // ืกืืืืกืืืงืืช | |
| totalOrders: customer.stats.totalOrders, | |
| totalSpent: customer.stats.totalSpent, | |
| averageOrder: customer.stats.averageOrder, | |
| daysSinceLastOrder: customer.stats.lastOrder | |
| ? Math.floor((Date.now() - new Date(customer.stats.lastOrder)) / (1000 * 60 * 60 * 24)) | |
| : null, | |
| // ืืขืืคืืช | |
| topProducts: customer.preferences.preferredProducts.slice(0, 3), | |
| usualQuantities, | |
| // ืชืืืืช ืืืขืจืืช | |
| tags: customer.tags, | |
| lastNote: customer.notes.length > 0 | |
| ? customer.notes[customer.notes.length - 1] | |
| : null, | |
| // ืืืกืืืจืื ืืืจืื ื | |
| lastOrders: customer.orderHistory.slice(-3).reverse() | |
| }; | |
| } | |
| // === ืืืคืืฉ ืืงืืืืช === | |
| function searchCustomers(query) { | |
| const results = []; | |
| const searchLower = query.toLowerCase(); | |
| for (const customer of customers.values()) { | |
| if ( | |
| customer.phone.includes(query) || | |
| (customer.name && customer.name.toLowerCase().includes(searchLower)) || | |
| (customer.email && customer.email.toLowerCase().includes(searchLower)) | |
| ) { | |
| results.push(getCustomerSummary(customer.phone)); | |
| } | |
| } | |
| return results; | |
| } | |
| // === ืงืืืช ืื ืืืงืืืืช (ืืืฉืืืจื ื ืืืื) === | |
| function getAllCustomers(options = {}) { | |
| const { | |
| sortBy = 'lastOrder', | |
| limit = 50, | |
| tags = null, | |
| minSpent = null | |
| } = options; | |
| let results = Array.from(customers.values()); | |
| // ืกืื ืื ืืคื ืชืืืืช | |
| if (tags && tags.length > 0) { | |
| results = results.filter(c => tags.some(t => c.tags.includes(t))); | |
| } | |
| // ืกืื ืื ืืคื ืกืืื ืืื ืืืื | |
| if (minSpent) { | |
| results = results.filter(c => c.stats.totalSpent >= minSpent); | |
| } | |
| // ืืืื | |
| switch (sortBy) { | |
| case 'lastOrder': | |
| results.sort((a, b) => (b.stats.lastOrder || 0) - (a.stats.lastOrder || 0)); | |
| break; | |
| case 'totalSpent': | |
| results.sort((a, b) => b.stats.totalSpent - a.stats.totalSpent); | |
| break; | |
| case 'totalOrders': | |
| results.sort((a, b) => b.stats.totalOrders - a.stats.totalOrders); | |
| break; | |
| case 'name': | |
| results.sort((a, b) => (a.name || '').localeCompare(b.name || '')); | |
| break; | |
| } | |
| // ืืืืื | |
| results = results.slice(0, limit); | |
| return results.map(c => getCustomerSummary(c.phone)); | |
| } | |
| // === ืกืืืืกืืืงืืช ืืืืืืช === | |
| function getCustomerStats() { | |
| const allCustomers = Array.from(customers.values()); | |
| const totalCustomers = allCustomers.length; | |
| const totalRevenue = allCustomers.reduce((sum, c) => sum + c.stats.totalSpent, 0); | |
| const totalOrders = allCustomers.reduce((sum, c) => sum + c.stats.totalOrders, 0); | |
| const vipCustomers = allCustomers.filter(c => c.tags.includes('VIP')).length; | |
| const newCustomers = allCustomers.filter(c => c.stats.totalOrders === 0).length; | |
| const returningCustomers = allCustomers.filter(c => c.stats.totalOrders > 1).length; | |
| // ืืงืืืืช ืคืขืืืื (ืืืืื ื ื-30 ืืื ืืืืจืื ืื) | |
| const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000); | |
| const activeCustomers = allCustomers.filter(c => | |
| c.stats.lastOrder && new Date(c.stats.lastOrder) > thirtyDaysAgo | |
| ).length; | |
| return { | |
| totalCustomers, | |
| totalRevenue, | |
| totalOrders, | |
| averageOrderValue: totalOrders > 0 ? Math.round(totalRevenue / totalOrders) : 0, | |
| vipCustomers, | |
| newCustomers, | |
| returningCustomers, | |
| activeCustomers, | |
| conversionRate: totalCustomers > 0 | |
| ? Math.round((returningCustomers / totalCustomers) * 100) | |
| : 0 | |
| }; | |
| } | |
| // === ืืืืื ืืืคืื ืืืงืกื === | |
| function extractPhoneFromText(text) { | |
| // ืืคืฉ ืืกืคืจ ืืืคืื ืืฉืจืืื | |
| const patterns = [ | |
| /0[5-9]\d[- ]?\d{3}[- ]?\d{4}/, // 050-123-4567 or 0501234567 | |
| /\+972[- ]?5\d[- ]?\d{3}[- ]?\d{4}/, // +972-50-123-4567 | |
| /972[- ]?5\d[- ]?\d{3}[- ]?\d{4}/ // 972-50-123-4567 | |
| ]; | |
| for (const pattern of patterns) { | |
| const match = text.match(pattern); | |
| if (match) { | |
| return normalizePhone(match[0]); | |
| } | |
| } | |
| return null; | |
| } | |
| // === ืืืืื ืฉื ืืืงืกื === | |
| function extractNameFromText(text) { | |
| // ืืคืฉ "ืื ื X" ืื "ืงืืจืืื ืื X" ืื "ืฉืื X" | |
| const patterns = [ | |
| /(?:ืื ื|ืฉืื|ืงืืจืืื ืื)\s+([ื-ืช]+(?:\s+[ื-ืช]+)?)/, | |
| /(?:I'm|I am|my name is)\s+(\w+(?:\s+\w+)?)/i | |
| ]; | |
| for (const pattern of patterns) { | |
| const match = text.match(pattern); | |
| if (match) { | |
| return match[1].trim(); | |
| } | |
| } | |
| return null; | |
| } | |
| module.exports = { | |
| // ื ืืืื ืืงืืืืช | |
| findOrCreateCustomer, | |
| getCustomerByPhone, | |
| updateCustomerAfterOrder, | |
| saveQuoteToCustomer, | |
| addNoteToCustomer, | |
| addTagToCustomer, | |
| // ืฉืืืคืช ืืืืข | |
| getCustomerSummary, | |
| searchCustomers, | |
| getAllCustomers, | |
| getCustomerStats, | |
| // ืขืืจ | |
| normalizePhone, | |
| extractPhoneFromText, | |
| extractNameFromText, | |
| // ืืืฉื ืืฉืืจื (ืืืืืงืืช) | |
| _customers: customers | |
| }; | |