canManage($request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot access.") ]); } $tableName = 'customers'; $tenantId = $request->tenantId(); $perPage = isset($request->params['per_page']) ? $request->params['per_page'] : 25; $currentPage = isset($request->params['page']) ? $request->params['page'] : 1; $search = isset($request->params['search']) ? $request->params['search'] : null; $orderBy = isset($request->params['order']) ? $request->params['order'] : 'name'; $orderBy = empty($orderBy) ? 'name' : $orderBy; $orderByDirection = isset($request->params['direction']) ? $request->params['direction'] : 'asc'; $orderByDirection = empty($orderByDirection) ? 'asc' : $orderByDirection; // When using Query does not have access to $appends parameter of the model $query_count = Query::table($tableName)->where('restaurant_id', $tenantId); $query = Customer::where('restaurant_id', $tenantId) ->orderBy("{$tableName}.{$orderBy}", $orderByDirection) ->page($currentPage, $perPage); // Filters visits $filters = isset($request->params['filters']) ? $request->params['filters'] : false; if ($filters){ $this->applyFilters($query_count, $filters); $this->applyFilters($query, $filters); } // Filter tags $tags_id = isset($request->params['tags']) ? $request->params['tags'] : false; if ($tags_id) { $this->applyTags($query_count, $tags_id); $this->applyTags($query, $tags_id); } // Filter search $this->applySearch($query_count, $search); $this->applySearch($query, $search); // Do the query //Query::setDebug(true); //ray('QUERY CUSTOMERS'); $total_arr = $query_count->count($perPage); //Query::setDebug(false); //ray('END QUERY CUSTOMERS'); //ray($total_arr); $rows = $query->get()->toArray(); return $this->response([ 'total' => $total_arr['count'], 'resources' => $rows, 'per_page' => $perPage, 'page' => $currentPage, 'total_pages' => $total_arr['pages'], ]); } public function customersList(Request $request) { $params = $request->params; $customers_id = explode(',', $params['customers']); $items = Customer::whereIn('id', $customers_id)->get(); return $this->response([ 'success' => true, 'customers' => $items->toArray() ]); } /** * Statistics: Get new vs returning customers per day for a given month. * Endpoint: GET /app/customers-stats/{yearmonth}?tenant=X * * Returns an array of daily stats: [ { date, new, returning } ... ] * A customer is "new" if their date_created falls within the requested month. * A customer is "returning" if their date_created is before the requested month. * Each customer is counted only once per day (even with multiple bookings). */ public function customerStats(Request $request) { $user = Eva::make('user'); if (!$user->canManage($request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot access.") ]); } $yearmonth = $request->yearmonth; // e.g. '2026-02' $tenantId = $request->tenantId(); // Get all bookings for this month (excluding selected/deleted) $bookings = Booking::where('restaurant_id', $tenantId) ->where('date', 'like', $yearmonth . '%') ->where('status', '!=', BookingStatus::SELECTED) ->where('status', '!=', BookingStatus::DELETED) ->get(); // Collect unique customer IDs $customerIds = []; foreach ($bookings as $booking) { if ($booking->customer_id) { $customerIds[] = $booking->customer_id; } } $customerIds = array_unique($customerIds); // Load those customers in one query $customers = []; if (!empty($customerIds)) { $customerModels = Customer::whereIn('id', $customerIds)->get(); foreach ($customerModels as $customer) { $customers[$customer->id] = $customer; } } // Build daily stats // For each day, track unique customer_ids to avoid counting duplicates $dailyStats = []; $dailyCustomers = []; // date => [ customer_id => 'new'|'returning' ] foreach ($bookings as $booking) { $date = $booking->date; $customerId = $booking->customer_id; if (!$customerId || !isset($customers[$customerId])) { continue; } if (!isset($dailyCustomers[$date])) { $dailyCustomers[$date] = []; } // Skip if already counted this customer for this day if (isset($dailyCustomers[$date][$customerId])) { continue; } $customer = $customers[$customerId]; // Determine if new: date_created month matches the requested yearmonth $customerCreatedMonth = substr($customer->date_created, 0, 7); // 'YYYY-MM' $isNew = ($customerCreatedMonth === $yearmonth); $dailyCustomers[$date][$customerId] = $isNew ? 'new' : 'returning'; } // Build the response array with all days of the month $year = intval(substr($yearmonth, 0, 4)); $month = intval(substr($yearmonth, 5, 2)); $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); $days = []; $totalNew = 0; $totalReturning = 0; for ($d = 1; $d <= $daysInMonth; $d++) { $date = $yearmonth . '-' . str_pad($d, 2, '0', STR_PAD_LEFT); $newCount = 0; $returningCount = 0; if (isset($dailyCustomers[$date])) { foreach ($dailyCustomers[$date] as $type) { if ($type === 'new') { $newCount++; } else { $returningCount++; } } } $totalNew += $newCount; $totalReturning += $returningCount; $days[] = [ 'date' => $date, 'new' => $newCount, 'returning' => $returningCount, ]; } return $this->response([ 'success' => true, 'days' => $days, 'total_new' => $totalNew, 'total_returning' => $totalReturning, 'total_customers' => $totalNew + $totalReturning, ]); } /** * Statistics: Get inactive customers grouped by inactivity period. * Endpoint: GET /app/customers-inactive/{yearmonth}?tenant=X * * Uses the last day of the requested month as reference date. * Groups customers into bands: 30, 60, 90, >90 days without a booking. * * Only considers customers that have at least one booking in the restaurant * (so they are real customers, not just created accounts). * * Returns: * - bands: [ { label, min_days, max_days, count, customers: [{first_name, last_name, email, last_visit, days_inactive}] } ] * - total_inactive: total count across all bands */ public function customersInactive(Request $request) { $user = Eva::make('user'); if (!$user->canManage($request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot access.") ]); } $yearmonth = $request->yearmonth; // e.g. '2026-02' $tenantId = $request->tenantId(); // Reference date: last day of the requested month $year = intval(substr($yearmonth, 0, 4)); $month = intval(substr($yearmonth, 5, 2)); $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); $referenceDate = $yearmonth . '-' . str_pad($daysInMonth, 2, '0', STR_PAD_LEFT); // Get all customers for this restaurant that have a last_visit // last_visit is set by the system when a booking is completed $customers = Customer::where('restaurant_id', $tenantId) ->whereNotNull('last_visit') ->where('last_visit', '<', $referenceDate) ->get(); // Define bands $bands = [ [ 'label' => '30 days', 'min_days' => 1, 'max_days' => 30, 'count' => 0, 'customers' => [] ], [ 'label' => '31-60 days', 'min_days' => 31, 'max_days' => 60, 'count' => 0, 'customers' => [] ], [ 'label' => '61-90 days', 'min_days' => 61, 'max_days' => 90, 'count' => 0, 'customers' => [] ], [ 'label' => '> 90 days', 'min_days' => 91, 'max_days' => 999999, 'count' => 0, 'customers' => [] ], ]; $totalInactive = 0; foreach ($customers as $customer) { $lastVisit = $customer->last_visit; // Skip customers with empty or invalid last_visit if (empty($lastVisit) || $lastVisit === '0000-00-00') { continue; } // Calculate days since last visit from reference date $refTimestamp = strtotime($referenceDate); $lastTimestamp = strtotime($lastVisit); if ($lastTimestamp === false || $refTimestamp === false) { continue; } $daysInactive = (int) floor(($refTimestamp - $lastTimestamp) / 86400); if ($daysInactive < 1) { continue; } $customerData = [ 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'email' => $customer->email, 'phone' => $customer->phone, 'dial_code' => $customer->dial_code, 'last_visit' => $lastVisit, 'days_inactive' => $daysInactive, ]; foreach ($bands as &$band) { if ($daysInactive >= $band['min_days'] && $daysInactive <= $band['max_days']) { $band['customers'][] = $customerData; $band['count']++; $totalInactive++; break; } } unset($band); } // Sort customers within each band by days_inactive desc foreach ($bands as &$band) { usort($band['customers'], function($a, $b) { return $b['days_inactive'] - $a['days_inactive']; }); } unset($band); return $this->response([ 'success' => true, 'bands' => $bands, 'total_inactive' => $totalInactive, 'reference_date' => $referenceDate, ]); } /** * Search customer by phone number (phone-first booking flow). * Returns enriched customer data with future bookings and WhatsApp info. */ public function searchByPhone(Request $request) { $user = Eva::make('user'); if (!$user->canManage($request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot access.") ]); } $tenantId = $request->tenantId(); $params = $request->params; $phone = isset($params['phone']) ? trim($params['phone']) : ''; $dialCode = isset($params['dial_code']) ? trim($params['dial_code']) : ''; $checkWhatsapp = !empty($params['check_whatsapp']); if (empty($phone)) { return $this->response([ 'success' => true, 'found' => false ]); } // Build full phone for search $fullPhone = !empty($dialCode) ? '+' . $dialCode . $phone : $phone; // Use existing Customer::findByPhone (searches by last 9 digits) $customer = Customer::findByPhone($tenantId, $fullPhone); if (!$customer) { $responseData = [ 'success' => true, 'found' => false ]; // If frontend indicates WhatsApp addon is active, check via provider if ($checkWhatsapp && class_exists(WhatsappProviderFactory::class)) { $provider = WhatsappProviderFactory::forRestaurant($tenantId); if ($provider && $provider->isConfigured()) { $contactResult = $provider->checkContact($fullPhone); $responseData['has_whatsapp'] = $contactResult['has_whatsapp']; $responseData['wa_id'] = $contactResult['wa_id'] ?? null; } } return $this->response($responseData); } // Get customer tags $tagsList = $customer->tagsList ?? []; // Get next 3 future bookings $today = evavel_date_now()->format('Y-m-d'); $futureBookings = Booking::where('customer_id', $customer->id) ->where('date', '>=', $today) ->whereIn('status', BookingStatus::all_status_no_selected()) ->orderBy('date', 'asc') ->limit(3) ->get(); $futureBookingsData = []; foreach ($futureBookings as $booking) { $futureBookingsData[] = [ 'id' => $booking->id, 'date' => $booking->date, 'time' => $booking->time, 'party' => $booking->party, 'status' => $booking->status, ]; } // Check for WhatsApp conversation (only if model exists) $conversationId = null; if (class_exists(WhatsappConversation::class)) { $conversation = WhatsappConversation::where('restaurant_id', $tenantId) ->where('customer_id', $customer->id) ->orderBy('last_message_at', 'DESC') ->first(); if ($conversation) { $conversationId = $conversation->id; } } return $this->response([ 'success' => true, 'found' => true, 'customer' => [ 'id' => $customer->id, 'first_name' => $customer->first_name, 'last_name' => $customer->last_name, 'email' => $customer->email, 'phone' => $customer->phone, 'dial_code' => $customer->dial_code, 'dial_code_country' => $customer->dial_code_country, 'country_code' => $customer->country_code, 'language' => $customer->language, 'visits' => $customer->visits ?? 0, 'cancels' => $customer->cancels ?? 0, 'no_shows' => $customer->no_shows ?? 0, 'spend' => $customer->spend ?? 0, 'has_whatsapp' => (bool) ($customer->has_whatsapp ?? true), 'tags' => $tagsList, 'notes' => $customer->notes, ], 'future_bookings' => $futureBookingsData, 'conversation_id' => $conversationId, ]); } // QUERY FILTERS //--------------------------------------------------------- public function applyFilters(Query $query, $filters) { foreach($filters as $filter){ // visits_10, visits_20, etc if (preg_match('#visits_(\d+)#', $filter, $matches)) { $num = intval($matches[1]); $query->where('visits', '>=', $num); } else if (preg_match('#last_visit_(\d+)_days#', $filter, $matches)) { $num = intval($matches[1]); //$last_date = Carbon::now()->subDay($num)->format('Y-m-d'); $last_date = evavel_date_now()->subDays($num)->format('Y-m-d'); $query->where('last_visit', '>=', $last_date); } else if (preg_match('#last_visit_(\d+)_months#', $filter, $matches)) { $num = intval($matches[1]); //$last_date = Carbon::now()->subMonth($num)->format('Y-m-d'); $last_date = evavel_date_now()->subMonths($num)->format('Y-m-d'); $query->where('last_visit', '>=', $last_date); } else if (preg_match('#last_visit_(\d+)_years#', $filter, $matches)) { $num = intval($matches[1]); //$last_date = Carbon::now()->subYear($num)->format('Y-m-d'); $last_date = evavel_date_now()->subYear($num)->format('Y-m-d'); $query->where('last_visit', '>=', $last_date); } // Consent to receive email marketing (captured in the booking form) else if ($filter === 'email_marketing_yes') { $query->where('agree_receive_email_marketing', 1); } else if ($filter === 'email_marketing_no') { $this->whereConsentNotGiven($query, 'agree_receive_email_marketing'); } // Consent to receive SMS notifications else if ($filter === 'sms_yes') { $query->where('agree_receive_sms', 1); } else if ($filter === 'sms_no') { $this->whereConsentNotGiven($query, 'agree_receive_sms'); } } return $query; } /** * "Does not accept" for a consent column. * * Old customers created before the consent columns existed have NULL, and in * SQL `column != 1` discards NULL rows (NULL != 1 is NULL, not true). So the * condition has to be grouped as: (column != 1 OR column IS NULL). * * @param Query $query * @param string $column * * @return Query */ protected function whereConsentNotGiven(Query $query, $column) { $query->where(function($q) use ($column) { $q->where($column, '!=', 1) ->orWhere($column, 'IS NULL'); return $q; }); return $query; } public function applyTags(Query $query, $tags_id) { // ERROR: is taking the id of the customer_ctag table /*for ($i = 0; $i < count($tags_id); $i++) { if ($i == 0) { $query->join('customer_ctag', 'customer_ctag.customer_id', '=', 'customers.id') ->where('customer_ctag.ctag_id', $tags_id[0]); } else { $query->orWhere('customer_ctag.ctag_id', $tags_id[$i]); } }*/ $list = Query::table('customer_ctag')->whereIn('ctag_id', $tags_id)->get(); $list_ids = array_unique((new Collection($list)) ->map( function($ref) { return $ref->customer_id; } ) ->toArray()); $query->whereIn('id', $list_ids); return $query; } public function applySearch(Query $query, $search_value) { if (!$search_value) return $query; $this->querySearch($query, ['first_name', 'last_name', 'name', 'email', 'phone'], $search_value); } public function querySearch($query, $where_fields, $search_value) { $closure = function($query) use($where_fields, $search_value) { for ($i = 0; $i < count($where_fields); $i++) { $w_field = $where_fields[$i]; if ($i == 0){ $query->where($w_field, 'like', $search_value); } else { $query->orWhere($w_field, 'like', $search_value); } } return $query; }; $query->where($closure); } // CREATE CUSTOMER //--------------------------------------------------------- public function create(Request $request) { $user = Eva::make('user'); $restaurant_id = $request->tenant; if (!$user->canCreate('customers', $request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot create customers.") ]); } $customer = new Customer(); $customer->restaurant_id = $restaurant_id; return $this->updateCustomerData($request, $customer, true); } // UPDATE CUSTOMER requests //--------------------------------------------------------- // Get single customer public function customer(Request $request) { $id = intval($request->customerId); $customer = Customer::find($id); if ($customer == null){ return $this->response([ 'success' => false, 'error' => __eva("No customer found.") ]); } $user = Eva::make('user'); if (!$user->canManage($customer->restaurant_id)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot access.") ]); } $bookings = Booking::where('customer_id', $id) ->whereIn('status', BookingStatus::all_status_no_selected()) ->orderBy('date', 'desc') ->get()->toArray(); return $this->response([ 'customer' => $customer, 'bookings' => $bookings, 'areas' => Area::where('restaurant_id', $customer->restaurant_id)->get()->toArray(), 'tables' => Table::where('restaurant_id', $customer->restaurant_id)->get()->toArray(), ]); } public function update(Request $request) { $user = Eva::make('user'); if (!$user->canEdit('customers', $request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot edit this customer.") ]); } $customer = Customer::find($request->customerId); return $this->updateCustomerData($request, $customer, false); } protected function updateCustomerData(Request $request, $customer, $is_new = true) { $params = $request->body_params; $tags = json_decode($params['tags']); $customer->restaurant_id = $request->tenantId(); $customer->first_name = $params['first_name']; $customer->last_name = $params['last_name']; $customer->name = $params['first_name'] . ' ' . $params['last_name']; $customer->company = $params['company']; $customer->email = $params['email']; $customer->phone = $params['phone']; $customer->language = $params['language']; $customer->dial_code_country = $params['dial_code_country']; $customer->country_code = $params['country_code']; $customer->dial_code = $params['dial_code']; $customer->notes = $params['notes']; $customer->birthday = $params['birthday']; $customer->gender = $params['gender']; $customer->agree_receive_email_marketing = intval($params['agree_receive_email_marketing']); $customer->agree_receive_email = intval($params['agree_receive_email']); $customer->agree_receive_sms = intval($params['agree_receive_sms']); $customer->has_whatsapp = intval($params['has_whatsapp']); $customer->save(); $customer->tags()->sync($tags); return $this->response([ 'success' => true, 'customer' => $customer ]); } public function toggleHasWhatsapp(Request $request) { $user = Eva::make('user'); if (!$user->canEdit('bookings', $request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot edit.") ]); } $params = $request->params; $customerId = intval($params['customer_id'] ?? 0); $hasWhatsapp = intval($params['has_whatsapp'] ?? 1); if (!$customerId) { return $this->response([ 'success' => false, 'error' => __eva("Customer not found.") ]); } $customer = Customer::find($customerId); if (!$customer) { return $this->response([ 'success' => false, 'error' => __eva("Customer not found.") ]); } $customer->has_whatsapp = $hasWhatsapp; $customer->save(); return $this->response([ 'success' => true, 'has_whatsapp' => (bool) $customer->has_whatsapp ]); } public function delete(Request $request) { $user = Eva::make('user'); if (!$user->canDelete('customers', $request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot delete customers.") ]); } $params = $request->body_params; $list = $params['list']; if (!is_array($list)){ $list = explode(',', $list); } Customer::whereIn('id', $list)->delete(); //return $this->response([ 'success' => false, 'error' => __eva('Error deleting customers') ]); return $this->response([ 'success' => true, 'message' => __eva('Deleted') ]); } public function merge(Request $request) { $user = Eva::make('user'); if (!$user->canEdit('customers', $request->tenant)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot update customers.") ]); } $params = $request->body_params; $list = $params['list']; if (!is_array($list)){ $list = explode(',', $list); } // First customer will receive all the bookings $first_id = array_shift($list); Booking::whereIn('customer_id', $list) ->update([ 'customer_id' => $first_id ]); // Merge WhatsApp conversations before deleting customers (CASCADE would delete them) $this->mergeWhatsapp($first_id, $list); // Remove the other customers Customer::whereIn('id', $list)->delete(); Customer::find($first_id)->calculateVisits(); //return $this->response([ 'success' => false, 'error' => __eva('Error merging customers') ]); return $this->response([ 'success' => true, 'message' => __eva('Done') ]); } /** * Merge WhatsApp conversations from secondary customers into the primary customer. * Must be called BEFORE deleting secondary customers (to avoid CASCADE deletion). * * @param int $primaryId The customer ID that survives the merge * @param array $secondaryIds Customer IDs being merged (will be deleted after) */ protected function mergeWhatsapp($primaryId, $secondaryIds) { if (empty($secondaryIds)) return; // Get all conversations from the secondary customers $secondaryConversations = WhatsappConversation::whereIn('customer_id', $secondaryIds)->get(); if ($secondaryConversations->isEmpty()) return; // Get conversations from the primary customer, indexed by phone_normalized $primaryConversations = WhatsappConversation::where('customer_id', $primaryId)->get(); $primaryByPhone = []; foreach ($primaryConversations as $conv) { $primaryByPhone[$conv->phone_normalized] = $conv; } foreach ($secondaryConversations as $secondaryConv) { $phone = $secondaryConv->phone_normalized; if (isset($primaryByPhone[$phone])) { // Primary already has a conversation with the same phone // Move all messages from secondary conversation → primary conversation $primaryConv = $primaryByPhone[$phone]; WhatsappMessage::where('conversation_id', $secondaryConv->id) ->update([ 'conversation_id' => $primaryConv->id, 'customer_id' => $primaryId ]); // Update counts on primary conversation $primaryConv->message_count = WhatsappMessage::where('conversation_id', $primaryConv->id)->count(); $primaryConv->save(); // Delete the now-empty secondary conversation $secondaryConv->delete(); } else { // Primary has NO conversation with this phone — adopt it $secondaryConv->customer_id = $primaryId; $secondaryConv->save(); // Update customer_id on all messages in this conversation WhatsappMessage::where('conversation_id', $secondaryConv->id) ->update(['customer_id' => $primaryId]); // Track it so if another secondary has the same phone, we merge into this one $primaryByPhone[$phone] = $secondaryConv; } } } // CSV, PRINT PDF //------------------------------------------------------------- public function downloadCSV(Request $request) { $tenantId = $request->tenantId(); $restaurant = Restaurant::find($tenantId); if (!$restaurant){ return $this->response(['success' => false, 'error' => __eva('Invalid restaurant')]); } // Authorization $user = Eva::make('user'); if (!$user->canExport('customers', $tenantId)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot export data.") ]); } // Get the list of ids to export $list_ids = $request->list; if (empty($list_ids)) { return $this->response(['success' => false, 'error' => __eva('No data to download.')]); } $list_ids = explode(',', $list_ids); $customers = Customer::whereIn('id', $list_ids)->orderBy('name', 'asc')->get(); $list = []; foreach($customers as $customer){ $list[] = $customer->toCsvArray(); } if (empty($list)) { return $this->response(['success' => false, 'error' => __eva('No data to download.')]); } return $this->response(['success' => 'true', 'data' => $this->convertToCsv($list)]); } public function printPDF(Request $request) { $lang = $request->lang; $tenantId = $request->tenantId(); $restaurant = Restaurant::find($tenantId); if (!$restaurant){ return $this->response(['success' => false, 'error' => __eva('Invalid restaurant')]); } // Authorization $user = Eva::make('user'); if (!$user->canExport('customers', $tenantId)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot print data.") ]); } // Get the list of ids to export $list_ids = $request->list; if (empty($list_ids)) { return $this->response(['success' => false, 'error' => __eva('No data to download.')]); } $list_ids = explode(',', $list_ids); // Prepare the booking $customers = Customer::whereIn('id', $list_ids)->orderBy('name', 'asc')->get(); $html = __eva("This is the FREE version, you need to upgrade to the PRO version"); if (defined("ALEXR_PRO_PLUGIN_DIR")){ ob_start(); include ALEXR_PRO_PLUGIN_DIR . 'includes-pro/dashboard/templates/pdf/customers.php'; $html = ob_get_clean(); } return $this->response(['success' => true, 'html' => $html]); } // IMPORT CUSTOMERS //=================================================================== /** * Import one user * Sent from the import component CSV * * @param Request $request * * @return \WP_REST_Response */ public function import(Request $request) { // Check restaurant $tenantId = $request->tenantId(); $restaurant = Restaurant::find($tenantId); if (!$restaurant){ return $this->response(['success' => false, 'error' => __eva('Invalid restaurant')]); } // Check permissions $user = Eva::make('user'); if (!$user->canImport('customers', $tenantId)){ return $this->response([ 'success' => false, 'error' => __eva("You cannot import customers.") ]); } // Check email validation $email = $request->email; if (!$email) { return $this->response(['success' => false, 'error' => __eva('No email')]); } $email = trim($email); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return $this->response(['success' => false, 'error' => __eva('Invalid email')]); } // Check it is not imported $customer = Customer::where(evavel_tenant_field(), $tenantId)->where('email', $email)->first(); $already_exists = false; if ($customer) { $already_exists = true; } // Prepare the fields $params = $request->params; $first_name = isset($params['first_name']) ? $params['first_name'] : null; $last_name = isset($params['last_name']) ? $params['last_name'] : null; $dial_code_country_array = $this->map_dial_code_country($params); $dial_code = $dial_code_country_array['dial_code']; $country_code = $dial_code_country_array['country']; $dial_code_country = $dial_code_country_array['country']; //$dial_code = $this->map_dial_code($params); //$country_code = $this->map_dial_code($params); if (isset($params['phone'])) { $phone = $this->map_correct_phone($params); } else { $phone = null; } //$phone = isset($params['phone']) ? $params['phone'] : null; $visits_imported = isset($params['visits']) ? intval($params['visits']) : 0; $spend_imported = isset($params['spend']) ? intval($params['spend']) : 0; $notes = isset($params['notes']) ? $params['notes'] : ''; $language = $this->map_language($params); $tags_ids = $this->map_tags($params, $tenantId); $args = [ 'name' => $first_name ? $first_name.' '.$last_name : $last_name, 'first_name' => $first_name, 'last_name' => $last_name, 'dial_code' => $dial_code, 'dial_code_country' => $dial_code_country, 'country_code' => $country_code, 'phone' => $phone, 'visits_imported' => $visits_imported, 'spend_imported' => $spend_imported, 'language' => $language, 'notes' => $notes ]; // Update or create if ($already_exists) { Customer::where('id', $customer->id)->update($args); } else { $args[evavel_tenant_field()] = $tenantId; $args['email'] = $email; $customer = Customer::create($args); } $customer->calculateVisits(); // Tags if (!empty($tags_ids)) { $customer->tags()->sync($tags_ids); $customer->save(); } if ($already_exists) { return $this->response(['success' => true, 'is_new' => false, 'message' => __eva('Already exist')]); } return $this->response(['success' => true, 'is_new' => true, 'message' => __eva('Imported')]); } // Devuelve solo el código de marcado limpio (números) // Ejemplos: "+56" -> "56", "CL" -> "56", "56" -> "56" protected function map_dial_code($params) { return $this->map_dial_code_country($params)['dial_code']; } // Devuelve el código de país de 2 letras // Ejemplos: "56" -> "CL", "CL" -> "CL", "+56" -> "CL" protected function map_country($params) { return $this->map_dial_code_country($params)['country']; } // Devuelve un array con ambos valores // Ejemplos: "56" -> ["dial_code" => "56", "country" => "CL"] // "CL" -> ["dial_code" => "56", "country" => "CL"] protected function map_dial_code_country($params) { $country_code = isset($params['country_code']) ? $params['country_code'] : null; if (!$country_code) { return ['dial_code' => null, 'country' => null]; } // Limpiar el código $clean_code = preg_replace('/[^0-9A-Z]/i', '', strtoupper($country_code)); $dial_code = null; $country = null; // Si tiene solo letras (2 caracteres), es un código de país if (preg_match('/^[A-Z]{2}$/', $clean_code)) { $country = $clean_code; $dial_code = Countries::phonePrefix($country); } else { // Si tiene números, es un dial code $dial_code = preg_replace('/[^0-9]/', '', $country_code); if ($dial_code !== '') { $country = Countries::countryByPrefix($dial_code); } } return [ 'dial_code' => $dial_code, 'country' => $country ]; } protected function map_correct_phone($params) { $phone = isset($params['phone']) ? $params['phone'] : null; if (!$phone) { return null; } // Limpiar el teléfono (quitar todo excepto números) $clean_phone = preg_replace('/[^0-9]/', '', $phone); // Obtener el dial code $dial_code_country_array = $this->map_dial_code_country($params); $dial_code = $dial_code_country_array['dial_code']; // Si tenemos dial code y el teléfono empieza con él, quitarlo if ($dial_code && strpos($clean_phone, $dial_code) === 0) { $clean_phone = substr($clean_phone, strlen($dial_code)); } return $clean_phone; } protected function map_language($params) { $language = isset($params['language']) ? strtolower($params['language']) : null; $languages = evavel_languages_all(); $found_language = false; foreach($languages as $key => $label) { if ($key == $language || strtolower($label) == $language){ $language = $key; $found_language = true; } } if (!$found_language) { $language = null; } return $language; } protected function map_tags($params, $tenantId) { if (!isset($params['tags'])) return []; $tags = $params['tags']; if (empty($tags)) return []; $tags = explode(',', $tags); if (!is_array($tags)) return []; $tags = array_filter($tags, function($var){ return ($var !== NULL && $var !== false && $var !== ""); }); if (count($tags) == 0) return []; // Comprobar que existe un grupo llamado Imported $tag_group = CTagGroup::where('name', 'Imported') ->where('restaurant_id', $tenantId) ->first(); if (!$tag_group) { $tag_group = CTagGroup::create([ 'restaurant_id' => $tenantId, 'name' => 'Imported', 'ordering' => 999, 'color' => '#FFFFFF', 'backcolor' => '#000000', 'is_deletable' => 1, 'is_vip' => 0, 'notes' => '' ]); } // Y añadir las etiquetas ahí $tags_ids = []; foreach($tags as $tag) { $tag = trim($tag); $tag_model = CTag::where('name', $tag) ->where('group_id', $tag_group->id) ->first(); if (!$tag_model) { $tag_model = CTag::create([ 'restaurant_id' => $tenantId, 'group_id' => $tag_group->id, 'name' => $tag, 'ordering' => 999, 'is_deletable' => 1, 'notes' => '' ]); } $tags_ids[] = $tag_model->id; } // Y devolver los ids return $tags_ids; } }