From 2ac4519d9a64b5cea53c108385c563116c5cd480 Mon Sep 17 00:00:00 2001 From: Antoine Le Gonidec Date: Wed, 3 Jul 2024 14:02:11 +0200 Subject: [PATCH] Link Customer instances to a Mailbox instance --- ...d_mailbox_id_column_to_customers_table.php | 49 ++ routes/web.php | 31 + src/Customer.php | 46 ++ ...coutRestrictedCustomersServiceProvider.php | 16 +- src/Http/Controllers/CrmController.php | 749 ++++++++++++++++++ src/Http/Controllers/CustomersController.php | 386 +++++++++ src/Mailbox.php | 19 + 7 files changed, 1295 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2024_07_03_135202_add_mailbox_id_column_to_customers_table.php create mode 100644 routes/web.php create mode 100644 src/Customer.php create mode 100644 src/Http/Controllers/CrmController.php create mode 100644 src/Http/Controllers/CustomersController.php create mode 100644 src/Mailbox.php diff --git a/database/migrations/2024_07_03_135202_add_mailbox_id_column_to_customers_table.php b/database/migrations/2024_07_03_135202_add_mailbox_id_column_to_customers_table.php new file mode 100644 index 0000000..6d46639 --- /dev/null +++ b/database/migrations/2024_07_03_135202_add_mailbox_id_column_to_customers_table.php @@ -0,0 +1,49 @@ + + */ + +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; +use MMF\FreescoutRestrictedCustomers\Customer; + +class AddMailboxIdColumnToCustomersTable extends Migration { + /** + * Run the migrations. + * + * @return void + */ + public function up() { + Schema::table('customers', function (Blueprint $table) { + // Add a "mailbox_id" field to the customers table, linking each customer entry to a specific mailbox. + $table + ->integer('mailbox_id') + ->unsigned() + // The column is nullable because entries without a linked mailbox might already exist. + ->nullable(); + $table + ->foreign('mailbox_id') + ->references('id') + ->on('mailboxes') + // On mailbox deletion, delete all customer entries that are linked to it. + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() { + Schema::table('customers', function (Blueprint $table) { + // Delete all entries from the customers table that are linked to a specific mailbox. + Customer::has('mailbox')->delete(); + // Delete the extra "mailbox_id" field from the customers table. + $table->dropForeign(['mailbox_id']); + $table->dropColumn('mailbox_id'); + }); + } +} diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..902879f --- /dev/null +++ b/routes/web.php @@ -0,0 +1,31 @@ + + */ + +use Illuminate\Support\Facades\Route; +use MMF\FreescoutRestrictedCustomers\Http\Controllers\CrmController; +use MMF\FreescoutRestrictedCustomers\Http\Controllers\CustomersController; + +// FIXME: Routes are not correctly exposed to the main application, +// routes/web.php and Modules/Crm/Http/routes.php must be manually edited. + +Route::get('/customers/{id}/edit', CustomersController::class . '@update')->name('customers.update'); +Route::post('/customers/{id}/edit', CustomersController::class . '@updateSave'); +Route::get('/customers/{id}/', CustomersController::class . '@conversations')->name('customers.conversations'); +Route::get('/customers/ajax-search', ['uses' => CustomersController::class . '@ajaxSearch', 'laroute' => true])->name('customers.ajax_search'); +Route::post('/customers/ajax', ['uses' => CustomersController::class . '@ajax', 'laroute' => true])->name('customers.ajax'); + +Route::group([ 'roles' => ['user', 'admin'] ], function() { + Route::get('/customers/new', CrmController::class . '@createCustomer')->name('crm.create_customer'); + Route::post('/customers/new', CrmController::class . '@createCustomerSave'); + Route::get('/crm/ajax-html/{action}/{param?}', ['uses' => CrmController::class . '@ajaxHtml'])->name('crm.ajax_html'); + Route::get('/customers/fields/ajax-search', ['uses' => CrmController::class . '@ajaxSearch', 'laroute' => true])->name('crm.ajax_search'); + Route::post('/crm/ajax', ['uses' => CrmController::class . '@ajax', 'laroute' => true])->name('crm.ajax'); +}); + +Route::group([ 'roles' => ['admin'] ], function() { + Route::post('/customers/export', ['uses' => CrmController::class . '@export'])->name('crm.export'); + Route::post('/crm/ajax-admin', ['uses' => CrmController::class . '@ajaxAdmin', 'laroute' => true])->name('crm.ajax_admin'); +}); diff --git a/src/Customer.php b/src/Customer.php new file mode 100644 index 0000000..2678d1c --- /dev/null +++ b/src/Customer.php @@ -0,0 +1,46 @@ + + */ + +namespace MMF\FreescoutRestrictedCustomers; + +use MMF\FreescoutRestrictedCustomers\Mailbox; +use App\Customer as BaseCustomer; + +class Customer extends BaseCustomer { + /** + * Attributes fillable using fill() method. + * + * @var [type] + */ + protected $fillable = [ + // Default list, imported from BaseCustomer. + 'first_name', + 'last_name', + 'company', + 'job_title', + 'address', + 'city', + 'state', + 'zip', + 'country', + 'photo_url', + 'age', + 'gender', + 'notes', + 'channel', + 'channel_id', + 'social_profiles', + // Addition specific to this package. + 'mailbox_id', + ]; + + /** + * Get the Mailbox that is allowed to access this Customer information. + */ + public function mailbox() { + return $this->belongsTo(Mailbox::class); + } +} diff --git a/src/FreescoutRestrictedCustomersServiceProvider.php b/src/FreescoutRestrictedCustomersServiceProvider.php index cdcbeb6..1524c24 100644 --- a/src/FreescoutRestrictedCustomersServiceProvider.php +++ b/src/FreescoutRestrictedCustomersServiceProvider.php @@ -6,6 +6,7 @@ namespace MMF\FreescoutRestrictedCustomers; +use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; class FreescoutRestrictedCustomersServiceProvider extends ServiceProvider { @@ -14,6 +15,19 @@ class FreescoutRestrictedCustomersServiceProvider extends ServiceProvider { } public function boot() { - // + $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); + $this->registerRoutes(); + } + + protected function registerRoutes() { + Route::group($this->routeConfiguration(), function () { + $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); + }); + } + + protected function routeConfiguration() { + return [ + 'middleware' => ['web', 'auth', 'roles'], + ]; } } diff --git a/src/Http/Controllers/CrmController.php b/src/Http/Controllers/CrmController.php new file mode 100644 index 0000000..8a482a5 --- /dev/null +++ b/src/Http/Controllers/CrmController.php @@ -0,0 +1,749 @@ + + */ + +namespace MMF\FreescoutRestrictedCustomers\Http\Controllers; + +use App\Conversation; +use App\Email; +use Modules\Crm\Entities\CustomerField; +use Modules\Crm\Entities\CustomerCustomerField; +use Validator; +use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Illuminate\Routing\Controller; +use Modules\Crm\Http\Controllers\CrmController as BaseCrmController; +use MMF\FreescoutRestrictedCustomers\Customer; + +class CrmController extends BaseCrmController { + public function createCustomer(Request $request) { + // TODO: Find a way to call parent::createCustomer while only overriding the Customer class, + // instead of overriding the whole method here. + $customer = new Customer(); + + return view('crm::create_customer', [ + 'customer' => $customer, + 'emails' => [''] + ]); + } + + public function createCustomerSave(Request $request) { + // TODO: Find a way to call parent::createCustomerSave while only overriding the Customer class, + // instead of overriding the whole method here. + $validator = Validator::make($request->all(), [ + 'first_name' => 'nullable|string|max:255|required_without:emails.0', + 'last_name' => 'nullable|string|max:255', + 'city' => 'nullable|string|max:255', + 'state' => 'nullable|string|max:255', + 'zip' => 'nullable|string|max:12', + 'country' => 'nullable|string|max:2', + //'emails' => 'array|required_without:first_name', + //'emails.1' => 'nullable|email|required_without:first_name', + 'emails.*' => 'nullable|email|distinct|required_without:first_name', + ]); + $validator->setAttributeNames([ + //'emails.1' => __('Email'), + 'emails.*' => __('Email'), + ]); + + // Check email uniqueness. + $fail = false; + foreach ($request->emails as $i => $email) { + $sanitized_email = Email::sanitizeEmail($email); + if ($sanitized_email) { + $email_exists = Email::where('email', $sanitized_email)->first(); + + if ($email_exists) { + $validator->getMessageBag()->add('emails.'.$i, __('A customer with this email already exists.')); + $fail = true; + } + } + } + + if ($fail || $validator->fails()) { + return redirect()->route('crm.create_customer') + ->withErrors($validator) + ->withInput(); + } + + $customer = new Customer(); + + $customer->setData($request->all()); + $customer->save(); + $customer->syncEmails($request->emails); + \Eventy::action('customer.created', $customer); + + \Session::flash('flash_success_unescaped', __('Customer saved successfully.')); + + \Session::flash('customer.updated', 1); + + // Create customer. + if ($customer->id) { + return redirect()->route('customers.update', ['id' => $customer->id]); + } else { + // Something went wrong. + return $this->createCustomer($request); + } + } + + /** + * Ajax controller. + */ + public function ajax(Request $request) { + // TODO: Find a way to call parent::ajax while only overriding the Customer class, + // instead of overriding the whole method here. + $response = [ + 'status' => 'error', + 'msg' => '', // this is error message + ]; + + switch ($request->action) { + + // Delete customer. + case 'delete_customer': + $has_conversations = Conversation::where('customer_id', $request->customer_id)->first(); + + if ($has_conversations) { + $response['msg'] = __("This customer has conversations. In order to delete the customer you need to completely delete all customer's conversations first."); + } + if (!$response['msg']) { + $customer = Customer::find($request->customer_id); + if ($customer) { + $customer->deleteCustomer(); + $response['msg_success'] = __('Customer deleted'); + $response['status'] = 'success'; + } else { + $response['msg'] = __('Customer not found'); + } + } + break; + + case 'delete_without_conv': + // Delete customers by bunches. + do { + $customers = $this->getCustomersWithoutConvQuery() + ->limit(100) + ->get(); + + foreach ($customers as $customer) { + $customer->deleteCustomer(); + } + } while(count($customers)); + + $response['msg_success'] = __('Customers deleted'); + $response['status'] = 'success'; + break; + + default: + $response['msg'] = 'Unknown action'; + break; + } + + if ($response['status'] == 'error' && empty($response['msg'])) { + $response['msg'] = 'Unknown error occured'; + } + + return \Response::json($response); + } + + /** + * Ajax controller. + */ + public function ajaxAdmin(Request $request) { + // TODO: Find a way to call parent::ajaxAdmin while only overriding the Customer class, + // instead of overriding the whole method here. + $response = [ + 'status' => 'error', + 'msg' => '', // this is error message + ]; + + switch ($request->action) { + + // Create/update saved reply + case 'customer_field_create': + case 'customer_field_update': + + // if (!$user->isAdmin()) { + // $response['msg'] = __('Not enough permissions'); + // } + + if (!$response['msg']) { + $name = $request->name; + + if (!$name) { + $response['msg'] = __('Name is required'); + } + } + + // Check unique name. + if (!$response['msg']) { + $name_exists = CustomerField::where('name', $name); + + if ($request->action == 'customer_field_update') { + $name_exists->where('id', '!=', $request->customer_field_id); + } + $name_exists = $name_exists->first(); + + if ($name_exists) { + $response['msg'] = __('A Customer Field with this name already exists.'); + } + } + + if (!$response['msg']) { + + if ($request->action == 'customer_field_update') { + $customer_field = CustomerField::find($request->customer_field_id); + if (!$customer_field) { + $response['msg'] = __('Customer Field not found'); + } + } else { + $customer_field = new CustomerField(); + $customer_field->setSortOrderLast(); + } + + if (!$response['msg']) { + //$customer_field->mailbox_id = $mailbox->id; + $customer_field->name = $name; + if ($request->action != 'customer_field_update') { + $customer_field->type = $request->type; + } + if ($customer_field->type == CustomerField::TYPE_DROPDOWN) { + + if ($request->action == 'customer_field_create') { + $options = []; + $options_tmp = preg_split('/\r\n|[\r\n]/', $request->options ?? ''); + // Remove empty + $option_index = 1; + foreach ($options_tmp as $i => $value) { + $value = trim($value); + if ($value) { + $options[$option_index] = $value; + $option_index++; + } + } + if (empty($options)) { + $options = [1 => '']; + } + } else { + $options = $request->options; + } + + $customer_field->options = $options; + + // Remove values. + if ($customer_field->id) { + CustomerCustomerField::where('customer_field_id', $customer_field->id) + ->whereNotIn('value', array_keys($request->options)) + ->delete(); + } + } elseif (isset($request->options)) { + $customer_field->options = $request->options; + } else { + $customer_field->options = ''; + } + $customer_field->required = $request->filled('required'); + $customer_field->display = $request->filled('display'); + $customer_field->conv_list = $request->filled('conv_list'); + $customer_field->customer_can_view = $request->filled('customer_can_view'); + $customer_field->customer_can_edit = $request->filled('customer_can_edit'); + $customer_field->save(); + + $response['id'] = $customer_field->id; + $response['name'] = $customer_field->name; + $response['required'] = (int)$customer_field->required; + $response['display'] = (int)$customer_field->display; + $response['conv_list'] = (int)$customer_field->conv_list; + $response['customer_can_view'] = (int)$customer_field->customer_can_view; + $response['customer_can_edit'] = (int)$customer_field->customer_can_edit; + $response['status'] = 'success'; + + if ($request->action == 'customer_field_update') { + $response['msg_success'] = __('Customer field updated'); + } else { + // Flash + \Session::flash('flash_success_floating', __('Customer field created')); + } + } + } + break; + + // Delete + case 'customer_field_delete': + + // if (!$user->isAdmin()) { + // $response['msg'] = __('Not enough permissions'); + // } + + if (!$response['msg']) { + $customer_field = CustomerField::find($request->customer_field_id); + + if (!$customer_field) { + $response['msg'] = __('Customer Field not found'); + } + } + + if (!$response['msg']) { + \Eventy::action('customer_field.before_delete', $customer_field); + $customer_field->delete(); + + // Delete links to customers; + CustomerCustomerField::where('customer_field_id', $request->customer_field_id)->delete(); + + $response['status'] = 'success'; + $response['msg_success'] = __('Customer Field deleted'); + + \Eventy::action('customer_field.after_delete', $request->customer_field_id); + } + break; + + // Update saved reply + case 'customer_field_update_sort_order': + + // if (!$user->isAdmin()) { + // $response['msg'] = __('Not enough permissions'); + // } + + if (!$response['msg']) { + + $customer_fields = CustomerField::whereIn('id', $request->customer_fields)->select('id', 'sort_order')->get(); + + if (count($customer_fields)) { + foreach ($request->customer_fields as $i => $request_customer_field_id) { + foreach ($customer_fields as $customer_field) { + if ($customer_field->id != $request_customer_field_id) { + continue; + } + $customer_field->sort_order = $i+1; + $customer_field->save(); + } + } + $response['status'] = 'success'; + } + } + break; + + // Parse CSV before importing. + case 'import_parse': + if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) { + $response['msg'] = __('Error occurred uploading file'); + } + + if (!$response['msg']) { + try { + $csv = $this->readCsv($request->file('file')->getPathName(), $request->separator, $request->enclosure, $request->encoding); + } catch (\Exception $e) { + $response['msg'] = __('Error occurred').': '.$e->getMessage(); + } + + if (!$response['msg'] && $csv && is_array($csv)) { + $response['cols'] = []; + foreach ($csv as $r => $row) { + if ($request->skip_header && $r == 0) { + continue; + } + foreach ($row as $c => $value) { + if (!empty($response['cols'][$c]) + && $response['cols'][$c] != __('Column :number', ['number' => $c+1]) + ) { + continue; + } + if ($request->skip_header) { + if ($r == 0) { + if (isset($csv[1][$c]) && $csv[1][$c] != '') { + $response['cols'][$c] = $value . ' ('.$csv[1][$c].')'; + } elseif ($value != '') { + $response['cols'][$c] = $value; + } else { + $response['cols'][$c] = __('Column :number', ['number' => $c+1]); + } + } elseif (isset($csv[0][$c]) && $value) { + $response['cols'][$c] = $csv[0][$c] . ' ('.$value.')'; + } elseif ($value != '') { + $response['cols'][$c] = $value; + } else { + $response['cols'][$c] = __('Column :number', ['number' => $c+1]); + } + } elseif ($value != '') { + $response['cols'][$c] = $value; + } else { + $response['cols'][$c] = __('Column :number', ['number' => $c+1]); + } + } + } + } + if (!$response['msg']) { + if (!empty($response['cols'])) { + $response['status'] = 'success'; + } else { + $response['msg'] = __('Could not parse CSV file.'); + } + } + } + break; + + // Import. + case 'import_import': + if (!$request->hasFile('file') || !$request->file('file')->isValid() || !$request->file) { + $response['msg'] = __('Error occurred uploading file'); + } + + if (!$response['msg']) { + try { + $csv = $this->readCsv($request->file('file')->getPathName(), $request->separator, $request->enclosure, $request->encoding); + $imported = 0; + $errors = []; + $email_conflicts = []; + if ($csv && is_array($csv)) { + foreach ($csv as $r => $row) { + if ($request->skip_header && $r == 0) { + continue; + } + $data = $this->importParseRow($row, json_decode($request->mapping, true)); + + try { + if (!empty($data['emails'])) { + // Try to find customers with emails. + // If found one - update. + // If found more than one customer - it's a conflict. + $customers_count = 0; + $customer_email = ''; + $customer_customer = null; + foreach ($data['emails'] as $email) { + $customer = Customer::getByEmail($email); + if ($customer) { + $customer_email = $email; + $customer_customer = $customer; + $customers_count++; + } + } + if ($customers_count > 1) { + $email_conflicts[] = (int)($r+1); + } elseif ($customers_count == 1 && $customer_customer) { + // Update existing customer. + $imported++; + $customer_customer->setData($this->prepareEmails($data), true, true); + } else { + $customer_customer = Customer::create($data['emails'][0], $this->prepareEmails($data)); + if ($customer_customer) { + $imported++; + } + } + } else { + // Create without email. + if (!empty($data['first_name'])) { + $customer_customer = Customer::createWithoutEmail($this->prepareEmails($data)); + if ($customer_customer) { + $imported++; + } + } else { + $errors[] = '#'.($r+1); + } + } + // Set photo. + if (!empty($data['photo_url']) && $customer_customer) { + try { + $customer_customer->setPhotoFromRemoteFile($data['photo_url']); + $customer_customer->save(); + } catch (\Exception $e) { + + } + } + } catch (\Exception $e) { + $errors[] = '#'.($r+1); + } + } + } + // if ($imported) { + // $flash_type = 'flash_success_floating'; + // } else { + // $flash_type = 'flash_error_floating'; + // } + + $response['result_html'] = __('Imported or updated: :imported customers.', ['imported' => $imported]); + if (count($errors)) { + $response['result_html'] .= '
'.__('Could not import the following CSV rows as they contain not enough data: :errors.', ['errors' => implode(', ', $errors)]); + } + if (count($email_conflicts)) { + $response['result_html'] .= '
'.__('Could not import the following CSV rows as emails specified in those rows belong to different existing customers: :email_conflicts.', ['email_conflicts' => implode(', ', $email_conflicts)]); + } + // \Session::flash($flash_type, __(':imported customer(s) imported, :errors error(s) occurred', ['imported' => $imported, 'errors' => $errors])); + // \Session::reflash(); + } catch (\Exception $e) { + $response['msg'] = __('Error occurred').': '.$e->getMessage(); + } + + if (!$response['msg'] && $csv) { + $response['status'] = 'success'; + } + } + break; + + default: + $response['msg'] = 'Unknown action'; + break; + } + + if ($response['status'] == 'error' && empty($response['msg'])) { + $response['msg'] = 'Unknown error occured'; + } + + return \Response::json($response); + } + + public function importParseRow($row, $mapping) { + // TODO: Find a way to call parent::importParseRow while only overriding the Customer class, + // instead of overriding the whole method here. + $data = []; + foreach ($mapping as $field_name => $field_row_i) { + if (!isset($row[$field_row_i])) { + continue; + } + $data[$field_name] = $row[$field_row_i]; + $data_value = $data[$field_name]; + + switch ($field_name) { + case 'emails': + case 'phones': + case 'websites': + case 'social_profiles': + $data[$field_name] = explode(',', $data[$field_name]); + if (is_array($data[$field_name]) + && count($data[$field_name]) == 1 + && isset($data[$field_name][0]) + && empty($data[$field_name][0]) + ) { + unset($data[$field_name][0]); + } + break; + } + + if ($field_name == 'social_profiles' && is_array($data[$field_name])) { + // Social profiles. + foreach ($data[$field_name] as $i => $value) { + preg_match("/^([^:]+):(.*)/", $value, $m); + if (!empty($m[1]) && !empty($m[2])) { + $social_name = $m[1]; + if (array_search($social_name, Customer::$social_type_names)) { + $data[$field_name][$i] = [ + 'value' => $m[2], + 'type' => array_search($social_name, Customer::$social_type_names), + ]; + } + } + } + } elseif ($field_name == 'country') { + // Country. + if (array_search($data[$field_name], Customer::$countries)) { + $data[$field_name] = array_search($data[$field_name], Customer::$countries); + } + $data[$field_name] = strtoupper(mb_substr($data[$field_name], 0, 2)); + } elseif (\Str::startsWith($field_name, CustomerField::NAME_PREFIX)) { + // Custom field. + $value = $data[$field_name]; + + $field_id = preg_replace("/^".CustomerField::NAME_PREFIX."/", '', $field_name); + $field = CustomerField::find($field_id); + + if ($field) { + $data[$field_name] = CustomerField::sanitizeValue($value, $field); + } + } + } + + return $data; + } + + public function getCustomersWithoutConvQuery() { + // TODO: Find a way to call parent::getCustomersWithoutConvQuery while only overriding the Customer class, + // instead of overriding the whole method here. + return Customer::select('customers.*') + ->leftJoin('conversations', function ($join) { + $join->on('conversations.customer_id', '=', 'customers.id'); + }) + ->leftJoin('threads', function ($join) { + $join->on('threads.created_by_customer_id', '=', 'customers.id'); + }) + ->where('conversations.customer_id', null) + ->where('threads.created_by_customer_id', null); + } + + /** + * Export. + */ + public function export(Request $request) { + // TODO: Find a way to call parent::export while only overriding the Customer class, + // instead of overriding the whole method here. + $fields = $request->fields ?? []; + $export_emails = false; + + $exportable_fields = \Crm::getExportableFields(); + + $fields_regular = []; + + foreach ($fields as $i => $field_name) { + if (array_key_exists($field_name, $exportable_fields)) { + if (!preg_match("/^".CustomerField::NAME_PREFIX."/", $field_name)) { + $fields_regular[] = $field_name; + } + } else { + unset($fields[$i]); + } + if ($field_name == 'emails') { + $export_emails = true; + if (\Helper::isMySql()) { + $fields_regular[count($fields_regular)-1] = \DB::raw("GROUP_CONCAT(emails.email SEPARATOR ', ') as emails"); + } else { + $fields_regular[count($fields_regular)-1] = \DB::raw("string_agg(emails.email, ', ') as emails"); + } + } + } + + $results = []; + + if (count($fields_regular)) { + + if (!in_array('customers.id', $fields_regular)) { + $fields_regular[] = 'customers.id'; + } + + if ($export_emails) { + $query = Customer::select($fields_regular) + ->leftJoin('emails', function ($join) { + $join->on('emails.customer_id', '=', 'customers.id'); + }) + ->groupby('customers.id'); + } else { + $query = Customer::select($fields_regular); + } + + $results = $query->get()->toArray(); + } + + // Add customer fields. + $fields_cf = []; + foreach ($fields as $field_name) { + if (preg_match("/^".CustomerField::NAME_PREFIX."/", $field_name)) { + $fields_cf[] = str_replace(CustomerField::NAME_PREFIX, '', $field_name); + } + } + if (count($fields_cf)) { + $customer_fields = CustomerCustomerField::whereIn('customer_field_id', $fields_cf) + ->get(); + + foreach ($results as $i => $row) { + foreach ($fields_cf as $cf_id) { + $results[$i][CustomerField::NAME_PREFIX.$cf_id] = ''; + foreach ($customer_fields as $cf_row) { + if ($cf_row->customer_id == $row['id'] && $cf_row->customer_field_id == $cf_id) { + $results[$i][CustomerField::NAME_PREFIX.$cf_id] = $cf_row->value; + break; + } + } + } + } + } + + foreach ($results as $i => $row) { + if (!in_array('customers.id', $fields) && isset($row['id'])) { + unset($results[$i]['id']); + } + if (!empty($row['photo_url'])) { + $results[$i]['photo_url'] = Customer::getPhotoUrlByFileName($row['photo_url']); + } + if (!empty($row['phones'])) { + $phones = json_decode($row['phones'], true); + $row['phones'] = ''; + $phones_list = []; + if (is_array($phones) && !empty($phones)) { + foreach ($phones as $phone) { + $phones_list[] = $phone['value']; + } + $results[$i]['phones'] = implode(', ', $phones_list); + } + } + if (!empty($row['websites'])) { + $websites = json_decode($row['websites'], true); + $results[$i]['websites'] = ''; + + if (is_array($websites) && !empty($websites)) { + $results[$i]['websites'] = implode(', ', $websites); + } + } + if (!empty($row['social_profiles'])) { + $social_profiles = json_decode($row['social_profiles'], true); + $row['social_profiles'] = ''; + $social_profiles_list = []; + if (is_array($social_profiles) && !empty($social_profiles)) { + foreach ($social_profiles as $social_profile) { + $sp_formatted = Customer::formatSocialProfile($social_profile); + $social_profiles_list[] = $sp_formatted['type_name'].':'.$social_profile['value']; + } + $results[$i]['social_profiles'] = implode(', ', $social_profiles_list); + } + } + } + + $filename = 'customers_'.date('Y-m-d').'.csv'; + + $encoding = $request->encoding; + $separator = $request->separator; + + if ($separator == 'TAB') { + $separator = "\t"; + } + + // Rename some fields. + foreach ($fields as $i => $field_name) { + // if (strstr($field_name, 'as emails')) { + // $field_name = 'emails'; + // } + + if (!empty($exportable_fields[$field_name])) { + $fields[$i] = $exportable_fields[$field_name]; + } + } + + $schema_insert = '"'.implode('"'.$separator.'"', $fields).'"'; + $out = $schema_insert."\n"; + + foreach($results as $row) { + $schema_insert = ''; + + foreach ($row as $row_value) { + $value_prepared = str_replace('"', '""', $row_value ?? ''); + $value_prepared = str_replace("\t", '', $value_prepared); + $schema_insert .= '"'.$value_prepared.'"'.$separator; + } + + $out .= $schema_insert."\n"; + } + + if (ob_get_contents()) { + ob_clean(); + } + + if ($encoding != 'UTF-8') { + try { + $out = iconv("UTF-8", $encoding, $out); + } catch (\Exception $e) { + // https://github.com/freescout-helpdesk/freescout/issues/2825 + $out = iconv("UTF-8", $encoding.'//IGNORE', $out); + } + if ($encoding == 'UCS-2LE') { + $out = "\xFF\xFE".$out; + } + } else { + // BOM: https://github.com/freescout-helpdesk/freescout/issues/3993 + $out = "\xEF\xBB\xBF".$out; + } + + header("Cache-Control: must-revalidate, no-cache, no-store, private"); + header("Content-Length: " . strlen($out)); + header("Content-type: application/csv; charset=UCS-2LE"); + header("Content-Disposition: attachment; filename=$filename"); + echo $out; + exit; + } +} diff --git a/src/Http/Controllers/CustomersController.php b/src/Http/Controllers/CustomersController.php new file mode 100644 index 0000000..8fd02cf --- /dev/null +++ b/src/Http/Controllers/CustomersController.php @@ -0,0 +1,386 @@ + + */ + +namespace MMF\FreescoutRestrictedCustomers\Http\Controllers; + +use App\Conversation; +use App\Email; +use Illuminate\Http\Request; +use Validator; +use App\Http\Controllers\CustomersController as BaseCustomersController; +use MMF\FreescoutRestrictedCustomers\Customer; + +class CustomersController extends BaseCustomersController { + /** + * Edit customer. + */ + public function update($id) { + // TODO: Find a way to call parent::update while only overriding the Customer class, + // instead of overriding the whole method here. + $customer = Customer::findOrFail($id); + + $customer_emails = $customer->emails; + if (count($customer_emails)) { + foreach ($customer_emails as $row) { + $emails[] = $row->email; + } + } else { + $emails = ['']; + } + + return view('customers/update', ['customer' => $customer, 'emails' => $emails]); + } + + /** + * Save customer. + * + * @param \Illuminate\Http\Request $request + * + * @return \Illuminate\Http\Response + */ + public function updateSave($id, Request $request) { + // TODO: Find a way to call parent::updateSave while only overriding the Customer class, + // instead of overriding the whole method here. + function mb_ucfirst($string) + { + return mb_strtoupper(mb_substr($string, 0, 1)).mb_strtolower(mb_substr($string, 1)); + } + + $customer = Customer::findOrFail($id); + $flash_message = ''; + + // First name or email must be specified + $validator = Validator::make($request->all(), [ + 'first_name' => 'nullable|string|max:255|required_without:emails.0', + 'last_name' => 'nullable|string|max:255', + 'city' => 'nullable|string|max:255', + 'state' => 'nullable|string|max:255', + 'zip' => 'nullable|string|max:12', + 'country' => 'nullable|string|max:2', + //'emails' => 'array|required_without:first_name', + //'emails.1' => 'nullable|email|required_without:first_name', + 'emails.*' => 'nullable|email|distinct|required_without:first_name', + 'photo_url' => 'nullable|image|mimes:jpeg,png,jpg,gif', + ]); + $validator->setAttributeNames([ + 'photo_url' => __('Photo'), + 'emails.*' => __('Email'), + ]); + + // Photo + $validator->after(function ($validator) use ($customer, $request) { + if ($request->hasFile('photo_url')) { + $path_url = $customer->savePhoto($request->file('photo_url')->getRealPath(), $request->file('photo_url')->getMimeType()); + + if ($path_url) { + $customer->photo_url = $path_url; + } else { + $validator->errors()->add('photo_url', __('Error occurred processing the image. Make sure that PHP GD extension is enabled.')); + } + } + }); + + if ($validator->fails()) { + return redirect()->route('customers.update', ['id' => $id]) + ->withErrors($validator) + ->withInput(); + } + + $new_emails = []; + $new_emails_change_customer = []; + $removed_emails = []; + + // Detect new emails added + $customer_emails = $customer->emails()->pluck('email')->toArray(); + foreach ($request->emails as $email) { + if (!in_array($email, $customer_emails)) { + $new_emails[] = $email; + } + } + + // If new email belongs to another customer, let user know about it in the flash message + foreach ($new_emails as $new_email) { + $email = Email::where('email', $new_email)->first(); + if ($email && $email->customer) { + // If customer whose email is removed does not have first name and other emails + // we have to create first name for this customer + if (!$email->customer->first_name && count($email->customer->emails) == 1) { + if ($request->first_name) { + $email->customer->first_name = $request->first_name; + } elseif ($customer->first_name) { + $email->customer->first_name = $customer->first_name; + } else { + $email->customer->first_name = mb_ucfirst($email->getNameFromEmail()); + } + $email->customer->save(); + } + + $flash_message .= __('Email :tag_email_begin:email:tag_email_end has been moved from another customer: :a_begin:customer:a_end.', [ + 'email' => $email->email, + 'tag_email_begin' => '', + 'tag_email_end' => '', + 'customer' => $email->customer->getFullName(), + 'a_begin' => '', + 'a_end' => '', + ]).' '; + + $new_emails_change_customer[] = $email; + } + } + + // Detect removed emails + foreach ($customer_emails as $email) { + if (!in_array($email, $request->emails)) { + $removed_emails[] = $email; + } + } + + $request_data = $request->all(); + + if (isset($request_data['photo_url'])) { + unset($request_data['photo_url']); + } + + $customer->setData($request_data); + // Websites + // if (!empty($request->websites)) { + // $customer->setWebsites($request->websites); + // } + $customer->save(); + + $customer->syncEmails($request->emails); + + // Update customer_id in all conversations added to the current customer. + foreach ($new_emails_change_customer as $new_email) { + if ($new_email->customer_id) { + $conversations_to_change_customer = Conversation::where('customer_id', $new_email->customer_id)->get(); + } else { + // This does not work for phone conversations. + $conversations_to_change_customer = Conversation::where('customer_email', $new_email->email)->get(); + } + foreach ($conversations_to_change_customer as $conversation) { + // We have to pass user to create line item and let others know that customer has changed. + // Conversation may be even in other mailbox where user does not have an access. + $conversation->changeCustomer($new_email->email, $customer, auth()->user()); + } + } + + // Update customer in conversations for emails removed from current customer. + foreach ($removed_emails as $removed_email) { + $email = Email::where('email', $removed_email)->first(); + if ($email) { + $conversations = Conversation::where('customer_email', $email->email)->get(); + foreach ($conversations as $conversation) { + $conversation->changeCustomer($email->email, $email->customer, auth()->user()); + } + } + } + + \Eventy::action('customer.updated', $customer); + + $flash_message = __('Customer saved successfully.').' '.$flash_message; + \Session::flash('flash_success_unescaped', $flash_message); + + \Session::flash('customer.updated', 1); + + return redirect()->route('customers.update', ['id' => $id]); + } + + /** + * View customer conversations. + * + * @param intg $id + */ + public function conversations($id) { + // TODO: Find a way to call parent::conversations while only overriding the Customer class, + // instead of overriding the whole method here. + $customer = Customer::findOrFail($id); + + $conversations = $customer->conversations() + ->where('customer_id', $customer->id) + ->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView()) + ->orderBy('created_at', 'desc') + ->paginate(Conversation::DEFAULT_LIST_SIZE); + + return view('customers/conversations', [ + 'customer' => $customer, + 'conversations' => $conversations, + ]); + } + + /** + * Customers ajax search. + */ + public function ajaxSearch(Request $request) { + // TODO: Find a way to call parent::ajaxSearch while only overriding the Customer class, + // instead of overriding the whole method here. + $response = [ + 'results' => [], + 'pagination' => ['more' => false], + ]; + + $q = $request->q; + + $join_emails = false; + if ($request->search_by == 'all' || $request->search_by == 'email' || $request->exclude_email) { + $join_emails = true; + } + + $select_list = ['customers.id', 'first_name', 'last_name']; + if ($join_emails) { + $select_list[] = 'emails.email'; + } + if ($request->show_fields == 'phone') { + $select_list[] = 'phones'; + } + $customers_query = Customer::select($select_list); + + if ($join_emails) { + if ($request->allow_non_emails) { + $customers_query->leftJoin('emails', 'customers.id', '=', 'emails.customer_id'); + } else { + $customers_query->join('emails', 'customers.id', '=', 'emails.customer_id'); + } + } + + if ($request->search_by == 'all' || $request->search_by == 'email') { + $customers_query->where('emails.email', 'like', '%'.$q.'%'); + } + if ($request->exclude_email) { + $customers_query->where('emails.email', '<>', $request->exclude_email); + } + if ($request->search_by == 'all' || $request->search_by == 'name') { + $customers_query->orWhere('first_name', 'like', '%'.$q.'%') + ->orWhere('last_name', 'like', '%'.$q.'%'); + } + if ($request->search_by == 'phone') { + $phone_numeric = \Helper::phoneToNumeric($q); + if (!$phone_numeric) { + $phone_numeric = $q; + } + $customers_query->where('customers.phones', 'like', '%'.$phone_numeric.'%'); + } + + $customers = $customers_query->paginate(20); + + foreach ($customers as $customer) { + $id = ''; + $text = ''; + + if ($request->show_fields != 'all') { + switch ($request->show_fields) { + case 'email': + $text = $customer->email; + break; + case 'name': + $text = $customer->getFullName(); + break; + case 'phone': + // Get phone which matches + $phones = $customer->getPhones(); + foreach ($phones as $phone) { + $phone_numeric = \Helper::phoneToNumeric($q); + if (strstr($phone['value'], $q) || strstr($phone['n'] ?? '', $phone_numeric)) { + $text = $phone['value']; + if ($customer->getFullName()) { + $text .= ' — '.$customer->getFullName(); + } + $id = $phone['value']; + break; + } + } + break; + default: + $text = $customer->getNameAndEmail(); + break; + } + } else { + $text = $customer->getNameAndEmail(); + } + + if (!$id) { + if (!empty($request->use_id)) { + $id = $customer->id; + } else { + $id = $customer->getMainEmail(); + } + } + $response['results'][] = [ + 'id' => $id, + 'text' => $text, + ]; + } + + $response['pagination']['more'] = $customers->hasMorePages(); + + return \Response::json($response); + } + + /** + * Ajax controller. + */ + public function ajax(Request $request) { + // TODO: Find a way to call parent::ajax while only overriding the Customer class, + // instead of overriding the whole method here. + $response = [ + 'status' => 'error', + 'msg' => '', // this is error message + ]; + + $user = auth()->user(); + + switch ($request->action) { + + // Change conversation user + case 'create': + // First name or email must be specified + $validator = Validator::make($request->all(), [ + 'first_name' => 'required|string|max:255', + 'last_name' => 'nullable|string|max:255', + 'email' => 'required|email|unique:emails,email', + ]); + + if ($validator->fails()) { + foreach ($validator->errors()->getMessages()as $errors) { + foreach ($errors as $field => $message) { + $response['msg'] .= $message.' '; + } + } + } + + if (!$response['msg']) { + $customer = Customer::create($request->email, $request->all()); + if ($customer) { + $response['email'] = $request->email; + $response['status'] = 'success'; + } + } + break; + + // Conversations navigation + case 'customers_pagination': + + $customers = app('App\Http\Controllers\ConversationsController')->searchCustomers($request, $user); + + $response['status'] = 'success'; + + $response['html'] = view('customers/partials/customers_table', [ + 'customers' => $customers, + ])->render(); + break; + + default: + $response['msg'] = 'Unknown action'; + break; + } + + if ($response['status'] == 'error' && empty($response['msg'])) { + $response['msg'] = 'Unknown error occurred'; + } + + return \Response::json($response); + } +} diff --git a/src/Mailbox.php b/src/Mailbox.php new file mode 100644 index 0000000..9428d8f --- /dev/null +++ b/src/Mailbox.php @@ -0,0 +1,19 @@ + + */ + +namespace MMF\FreescoutRestrictedCustomers; + +use MMF\FreescoutRestrictedCustomers\Customer; +use App\Mailbox as BaseMailbox; + +class Mailbox extends BaseMailbox { + /** + * Get the Customers that are linked to this Mailbox. + */ + public function customers() { + return $this->hasMany(Customer::class); + } +}