<?php
/*
 * Jetpack CRM
 * https://jetpackcrm.com
 * V3.0
 *
 * Copyright 2020 Automattic
 *
 * Date: 20/02/2019
 */

defined( 'ZEROBSCRM_PATH' ) || exit( 0 );

/*
======================================================
	Init Func
	====================================================== */

function zeroBSCRM_CustomersMetaboxSetup() {

	if ( zeroBSCRM_is_customer_edit_page() ) {

		// Customer Fields
		$zeroBS__Metabox_Contact = new zeroBS__Metabox_Contact( __FILE__ );

		// Actions
		$zeroBS__Metabox_ContactActions = new zeroBS__Metabox_ContactActions( __FILE__ );

		// Logs

			// req. for custom log types
			zeroBSCRM_setupLogTypes();

			// metabox
			$zeroBS__Metabox_ContactLogs = new zeroBS__Metabox_ContactLogs( __FILE__ );

		// Tags
		$zeroBS__Metabox_ContactTags = new zeroBS__Metabox_ContactTags( __FILE__ );

		// External sources
		// $zeroBS__Metabox_ContactExternalSources = new zeroBS__Metabox_ContactExternalSources( __FILE__ );
		$zeroBS__Metabox_ExtSource = new zeroBS__Metabox_ExtSource( __FILE__, 'contact', 'zbs-add-edit-contact-edit' );

		// Quotes, Invs, Trans
		// Don't need now we have singular :)
		// $zeroBS__MetaboxAssociated = new zeroBS__MetaboxAssociated( __FILE__ );

		// Client Portal access
		if ( zeroBSCRM_isExtensionInstalled( 'portal' ) ) {
			$zeroBS__Metabox_ContactPortal = new zeroBS__Metabox_ContactPortal( __FILE__ );
		}

		// Customer File Attachments

			// Custom file attachment boxes
		if ( zeroBSCRM_is_customer_edit_page() ) {

			/*
				old way
			$settings = zeroBSCRM_getSetting('customfields'); $cfbInd = 1;

			if (isset($settings['customersfiles']) && is_array($settings['customersfiles']) && count($settings['customersfiles']) > 0) foreach ($settings['customersfiles'] as $cfb){

				$cfbName = ''; if (isset($cfb[0])) $cfbName = $cfb[0];

				//add_meta_box('zerobs-customer-files-'.$cfbInd, $cfbName, 'zeroBS__MetaboxFilesCustom', 'zerobs_customer', 'normal', 'low',$cfbName);
				$zeroBS__Metabox_ContactCustomFiles = new zeroBS__Metabox_ContactCustomFiles( __FILE__, 'zerobs-customer-files-'.$cfbInd , $cfbName);

				$cfbInd++;
			} */

			$fileSlots = zeroBSCRM_fileSlots_getFileSlots();
			if ( count( $fileSlots ) > 0 ) {
				foreach ( $fileSlots as $fs ) {

					$zeroBS__Metabox_ContactCustomFiles = new zeroBS__Metabox_ContactCustomFiles( __FILE__, 'zerobs-customer-files-' . $fs['key'], $fs['name'] );

				}
			}
		}

		#} Social
		if ( zeroBSCRM_getSetting( 'usesocial' ) == '1' ) {
			$zeroBS__Metabox_ContactSocial = new zeroBS__Metabox_ContactSocial( __FILE__ );
		}

		#} AKA
		if ( zeroBSCRM_getSetting( 'useaka' ) == '1' ) {
			$zeroBS__Metabox_ContactAKA = new zeroBS__Metabox_ContactAKA( __FILE__ );
		}

		#} Ownership
		if ( zeroBSCRM_getSetting( 'perusercustomers' ) == '1' ) {
			$zeroBS__Metabox_Ownership = new zeroBS__Metabox_Ownership( __FILE__, ZBS_TYPE_CONTACT );
		}

		#} B2B mode (assign to co)
		if ( zeroBSCRM_getSetting( 'companylevelcustomers' ) == '1' ) {
			$zeroBS__Metabox_ContactCompany = new zeroBS__Metabox_ContactCompany( __FILE__ );
		}
	}

	// Activity box on view page
	if ( zeroBSCRM_is_customer_view_page() ) {
			$zeroBS__Metabox_Contact_Activity = new zeroBS__Metabox_Contact_Activity( __FILE__ );
		if ( zeroBSCRM_isExtensionInstalled( 'portal' ) ) {
			$zeroBS__Metabox_ContactPortal = new zeroBS__Metabox_ContactPortal( __FILE__, 'zbs-view-contact' );
		}
	}
}

	add_action( 'admin_init', 'zeroBSCRM_CustomersMetaboxSetup' );

/*
======================================================
	/ Init Func
	====================================================== */

/*
======================================================
	Declare Globals
	====================================================== */

	#} Used throughout
	// Don't know who added this, but GLOBALS are out of scope here
	// global $zbsCustomerFields,$zbsCustomerQuoteFields,$zbsCustomerInvoiceFields;

/*
======================================================
	/ Declare Globals
	====================================================== */

	// PerfTest: zeroBSCRM_performanceTest_startTimer('custmetabox');

/*
======================================================
	Customer Metabox
	====================================================== */

class zeroBS__Metabox_Contact extends zeroBS__Metabox {

	// this is for catching 'new' contacts
	private $newRecordNeedsRedir = false;

	public function __construct( $plugin_file ) {

		// set these
		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-edit';
		$this->metaboxTitle    = __( 'Contact Details', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'normal';
		$this->metaboxLocation = 'high';
		$this->saveOrder       = 1;
		$this->capabilities    = array(

			'can_hide'        => false, // can be hidden
			'areas'           => array( 'normal' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => true,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => true, // can be minimised
			'can_move'        => true, // can be moved

		);

		// call this
		$this->initMetabox();
	}

	/**
	 * This method generates HTML content for contact and metadata.
	 *
	 * It contains a series of operations to format and display contact data
	 * and metadata according to the parameters. This includes information retrieval,
	 * settings configuration, field hiding, address display and others.
	 *
	 * @param array $contact An associative array containing contact details.
	 * @param array $metabox Metabox information.
	 *
	 * @return  void
	 */
	public function html( $contact, $metabox ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		global $zbs; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		global $zbsContactEditing;  // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		global $zbsCustomerFields; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase

		if ( ! isset( $zbsContactEditing ) && isset( $contact['id'] ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
			$customer          = zeroBS_getCustomer( $contact['id'], false, false, false );
			$zbsContactEditing = $customer; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		} else {
			$customer = $zbsContactEditing; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		}

		$fields_to_hide       = $zbs->settings->get( 'fieldhides' );
		$show_id              = (int) $zbs->settings->get( 'showid' );
		$fields               = $zbsCustomerFields; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		$show_addresses       = zeroBSCRM_getSetting( 'showaddress' );
		$show_second_address  = (int) zeroBSCRM_getSetting( 'secondaddress' );
		$show_country_fields  = zeroBSCRM_getSetting( 'countries' );
		$second_address_label = zeroBSCRM_getSetting( 'secondaddresslabel' );
		if ( empty( $second_address_label ) ) {
			$second_address_label = __( 'Second Address', 'zero-bs-crm' );
		}
		?>

<script type="text/javascript">var zbscrmjs_secToken = <?php echo wp_json_encode( wp_create_nonce( 'zbscrmjs-ajax-nonce' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;</script>

		<?php
		if ( gettype( $customer ) !== 'array' ) {
			echo '<input type="hidden" name="zbscrm_newcustomer" value="1" />';
		}
		?>
			<div>
				<div class="jpcrm-form-grid" id="wptbpMetaBoxMainItem">
		<?php
		$avatar_mode = (int) zeroBSCRM_getSetting( 'avatarmode' );
		if ( $avatar_mode === 2 ) :
			?>
					<div class="jpcrm-form-group jpcrm-form-group-span-2">
						<label class="jpcrm-form-label"><?php esc_html_e( 'Profile Picture', 'zero-bs-crm' ); ?>:</label>
			<?php
							$avatar_url       = isset( $contact['id'] ) ? $zbs->DAL->contacts->getContactAvatar( $contact['id'] ) : zeroBSCRM_getDefaultContactAvatar(); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
							$empty_avatar_url = zeroBSCRM_getDefaultContactAvatar();
			?>
								<div class="zbs-text-input">
									<div class="jpcrm-customer-profile-picture-container" style="margin-right:10px;">
											<img src="<?php echo esc_attr( $avatar_url ); ?>" id="profile-picture-img" class="jpcrm-customer-profile-picture" />
											<img src="<?php echo esc_attr( $empty_avatar_url ); ?>" id="empty-profile-picture" class="jpcrm-customer-profile-picture" style="display:none;" />
									<br />
									<label for="zbsc_profile-picture-file" class="jpcrm-customer-file-upload">
											<?php esc_html_e( 'Change Picture', 'zero-bs-crm' ); ?>
											<input id="zbsc_profile-picture-file" type="file" name="zbsc_profile-picture-file" class="jpcrm-customer-input-file"/>
									</label>
									<label id="zbsc_remove-profile-picture-button" class="jpcrm-customer-file-upload-remove">
											<?php esc_html_e( 'Remove', 'zero-bs-crm' ); ?>
											<input type="hidden" id="zbsc_remove-profile-picture" name="zbsc_remove-profile-picture" value="0" />
									</label>
									</div>
								</div>
							</div>

			<?php
		endif;

		if ( $show_id === 1 && isset( $contact['id'] ) && ! empty( $contact['id'] ) ) :
			?>
							<div class="jpcrm-form-group">
								<label class="jpcrm-form-label"><?php esc_html_e( 'Contact ID', 'zero-bs-crm' ); ?>:</label>
								<b>#<?php echo isset( $contact['id'] ) ? esc_html( $contact['id'] ) : ''; ?></b>
							</div>
							<div class="jpcrm-form-group">
							</div>
			<?php
		endif;

		global $zbsFieldsEnabled; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		if ( $show_second_address === 1 ) {
			$zbsFieldsEnabled['secondaddress'] = true; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
		}
		$field_group = '';

		foreach ( $fields as $field_key => $field_value ) {
			$show_field = ! isset( $field_value['opt'] ) || isset( $zbsFieldsEnabled[ $field_value['opt'] ] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
			$show_field = isset( $fields_to_hide['customer'] )
				&& is_array( $fields_to_hide['customer'] )
				&& in_array( $field_key, $fields_to_hide['customer'] ) // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
				? false
				: $show_field;
			$show_field = isset( $field_value[0] )
				&& 'selectcountry' === $field_value[0]
				&& 0 === $show_country_fields
				? false
				: $show_field;

			if ( isset( $field_value['area'] ) && $field_value['area'] !== '' ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
				if ( $show_addresses !== 1 ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
					continue;
				} elseif ( $field_group === '' ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
					echo '<div class="jpcrm-form-grid" style="padding:0;grid-template-columns: 1fr;">';
					echo '<div class="jpcrm-form-group"><label>';
					echo esc_html__( $field_value['area'], 'zero-bs-crm' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, WordPress.WP.I18n.NonSingularStringLiteralText
					echo '</label></div>';
				} elseif ( $field_group !== $field_value['area'] ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
					echo '</div>';
					echo '<div class="jpcrm-form-grid" style="padding:0;grid-template-columns: 1fr;">';
					echo '<div class="jpcrm-form-group"><label>';
					echo $show_field ? esc_html( $second_address_label ) : '';
					echo '</label></div>';
				}
				$field_group = $field_value['area']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase, WordPress.WP.I18n.NonSingularStringLiteralText
			}

			if ( $field_group !== '' && ( ! isset( $field_value['area'] ) || $field_value['area'] === '' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
				$field_group = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
				echo '</div>';
				echo '<div class="jpcrm-form-group jpcrm-form-group-span-2">&nbsp;</div>';
			}

			if ( $show_field ) {
				if ( isset( $field_value[0] ) ) {
					if ( $field_group === 'Second Address' ) {
						$field_value[1] = str_replace( ' (' . $second_address_label . ')', '', $field_value[1] );
					}
					zeroBSCRM_html_editField( $customer, $field_key, $field_value, 'zbsc_' );
				}
			}
		}
		?>
					</div>
				</div>
		<?php
	}

	public function save_data( $contact_id, $contact ) {

		if ( ! defined( 'ZBS_C_SAVED' ) ) {

			// debug if (get_current_user_id() == 12) echo 'FIRING<br>';

			define( 'ZBS_C_SAVED', 1 );

			global $zbs;

			// check this
			if ( empty( $contact_id ) || $contact_id < 1 ) {
				$contact_id = -1;
			}

			$dataArr = zeroBS_buildContactMeta( $_POST );

			// Use the tag-class function to retrieve any tags so we can add inline.
			// Save tags against objid
			$dataArr['tags'] = zeroBSCRM_tags_retrieveFromPostBag( true, ZBS_TYPE_CONTACT );

			// owner - saved here now, rather than ownership box, to allow for pre-hook update. (as tags)
			$owner = -1;
			// Only allow an existing contact to change owners if they have permission to do so.
			if ( $contact_id > -1 ) {

				$can_edit_all_contacts = current_user_can( 'admin_zerobs_customers' ) && $zbs->settings->get( 'perusercustomers' ) == 0; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual,WordPress.WP.Capabilities.Unknown  -- capability was defined in ZeroBSCRM.Permissions.php
				$can_give_ownership    = $zbs->settings->get( 'usercangiveownership' ) == 1; // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- also above, there is the chance the numbers could be strings here, as expected elsewhere in the plugin.
				$can_change_owner      = ( $can_give_ownership || current_user_can( 'manage_options' ) || $can_edit_all_contacts );
				if ( $can_change_owner ) {

					if ( isset( $_POST['zerobscrm-owner'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- todo - noted in zero-bs-crm 2457.

						$potential_owner = (int) $_POST['zerobscrm-owner']; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- todo - noted in zero-bs-crm 2457.
						if ( $potential_owner > 0 ) {
							$owner = $potential_owner;
						}
					}
				} else {
					$owner = (int) $zbs->DAL->contacts->getContactOwner( $contact_id ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				}
			}

			// now we check whether a user with this email already exists (separate to this contact id), so we can warn them
			// ... that it wont have changed the email
			if ( ! empty( $dataArr['email'] ) ) {

				if ( ! zeroBSCRM_validateEmail( $dataArr['email'] ) ) {

					$this->update_invalid_email( $dataArr['email'] );
					$dataArr['email'] = '';

				} else {

					$potentialID = zeroBS_getCustomerIDWithEmail( $dataArr['email'] );

					if ( ! empty( $potentialID ) && $potentialID != $contact_id ) {

						// no go.
						$this->updateEmailDupeMessage( $potentialID );

						// unset email change (leave as was)
						$dataArr['email'] = zeroBS_customerEmail( $contact_id );

					}
				}
			}
			// phpcs:disable WordPress.NamingConventions.ValidVariableName -- to be refactored.
			// We have to explicitly retrieve the avatar from the DB.
			$dataArr['avatar'] = ( $contact_id !== -1 ) ? $zbs->DAL->contacts->getContactAvatar( $contact_id ) : '';
			//phpcs:enable WordPress.NamingConventions.ValidVariableName

			// make a copy for IA below (just fields)
			$contactData = $dataArr;

			// Company assignment?
			if ( isset( $_POST['zbs_company'] ) ) {
				$dataArr['companies'] = array( (int) sanitize_text_field( $_POST['zbs_company'] ) );
			}

			// Stripslashes
			// This avoids us adding `O\'toole ltd' into the db. see #1107
			// ...this is more sensitive than using zeroBSCRM_stripSlashesFromArr
			// in the long term it may make more sense to stripslashes pre insert/update in the DAL
			// in the case of contacts, there are no core fields which will be broken by stripslashes at this time (4.0.11)
			$data_array = $dataArr;
			foreach ( $dataArr as $key => $val ) {

				// op strings
				$value = $val;
				if ( is_string( $value ) ) {
					$value = stripslashes( $value );
				}

				// pass into final array
				$data_array[ $key ] = $value;

			}

			// add update directly
			$addUpdateReturn = $zbs->DAL->contacts->addUpdateContact(
				array(

					'id'            => $contact_id,
					'owner'         => $owner,
					'data'          => $data_array,
					'limitedFields' => -1,
					/*
						array(

							'email' => $userDeets['email'], // Unique Field !

							'status' => $userDeets['status'],
							'prefix' => $userDeets['prefix'],
							'fname' => $userDeets['fname'],
							'lname' => $userDeets['lname'],
							'addr1' => $userDeets['addr1'],
							'addr2' => $userDeets['addr2'],
							'city' => $userDeets['city'],
							'county' => $userDeets['county'],
							'country' => $userDeets['country'],
							'postcode' => $userDeets['postcode'],
							'secaddr1' => $userDeets['secaddr_addr1'],
							'secaddr2' => $userDeets['secaddr_addr2'],
							'seccity' => $userDeets['secaddr_city'],
							'seccounty' => $userDeets['secaddr_county'],
							'seccountry' => $userDeets['secaddr_country'],
							'secpostcode' => $userDeets['secaddr_postcode'],
							'hometel' => $userDeets['hometel'],
							'worktel' => $userDeets['worktel'],
							'mobtel' => $userDeets['mobtel'],
							'wpid'  => -1,
							'avatar' => $avatarURL,

							// Note Custom fields may be passed here, but will not have defaults so check isset()

							'tags' => $tags,

							// wh added for later use.
							'lastcontacted' => $lastcontacted,

							'companies' => $companies // array of co id's :)
					) */

				)
			);

			// Note: For NEW contacts, we make sure a global is set here, that other update funcs can catch
			// ... so it's essential this one runs first!
			// this is managed in the metabox Class :)
			if ( $contact_id == -1 && ! empty( $addUpdateReturn ) && $addUpdateReturn != -1 ) {

				$contact_id = $addUpdateReturn;
				global $zbsJustInsertedMetaboxID;
				$zbsJustInsertedMetaboxID = $contact_id;

				// set this so it redirs
				$this->newRecordNeedsRedir = true;
			}

			// success?
			if ( $addUpdateReturn != -1 && $addUpdateReturn > 0 ) {
				$this->save_profile_picture( $contact_id, $contact );
				// Update Msg
				// this adds an update message which'll go out ahead of any content
				// This adds to metabox: $this->updateMessages['update'] = zeroBSCRM_UI2_messageHTML('info olive mini zbs-not-urgent',__('Contact Updated',"zero-bs-crm"),'','address book outline','contactUpdated');
				// This adds to edit page
				$this->updateMessage( $this->newRecordNeedsRedir );

				// catch any non-critical messages
				$nonCriticalMessages = $zbs->DAL->getErrors( ZBS_TYPE_CONTACT );
				if ( is_array( $nonCriticalMessages ) && count( $nonCriticalMessages ) > 0 ) {
					$this->dalNoticeMessage( $nonCriticalMessages );
				}
			} else {

				// fail somehow
				$failMessages = $zbs->DAL->getErrors( ZBS_TYPE_CONTACT );

				// show msg (retrieved from DAL err stack)
				if ( is_array( $failMessages ) && count( $failMessages ) > 0 ) {
					$this->dalErrorMessage( $failMessages );
				} else {
					$this->dalErrorMessage( array( __( 'Insert/Update Failed with general error', 'zero-bs-crm' ) ) );
				}

				// pass the pre-fill:
				global $zbsObjDataPrefill;
				$zbsObjDataPrefill = $dataArr;

			}
		}

		return $contact;
	}

	/*
	* Saves the profile picture
	*/
	public function save_profile_picture( $contact_id, $crm_contact ) {
		global $zbs;

		$contact_dir_info    = jpcrm_storage_dir_info_for_contact( $contact_id );
		$field_key           = 'jpcrm-profile-picture';
		$is_remove_flag_set  = isset( $_POST['zbsc_remove-profile-picture'] ) && $_POST['zbsc_remove-profile-picture'] == '1';
		$remove_old_avatar   = false;
		$has_new_avatar_file =
			isset( $_FILES['zbsc_profile-picture-file'] )
			&& empty( $_FILES['zbsc_profile-picture-file']['error'] )
			&& is_uploaded_file( $_FILES['zbsc_profile-picture-file']['tmp_name'] );

		if ( $is_remove_flag_set ) {
			$zbs->DAL->contacts->addUpdateContact(
				array(
					'id'            => $contact_id,
					'limitedFields' => array(
						array(
							'key'  => 'zbsc_avatar',
							'val'  => '',
							'type' => '%s',
						),
					),
				)
			);
			$remove_old_avatar = true;
		} elseif ( $has_new_avatar_file ) {

			// verify image file type
			$allowed_image_types     = array(
				'image/jpeg' => 'jpg',
				'image/jpg'  => 'jpg',
				'image/gif'  => 'gif',
				'image/png'  => 'png',
			);
			$allowed_file_extensions = array( '.jpg', '.jpeg', '.gif', '.png' );
			$allowed_mime_types      = array( 'image/jpeg', 'image/jpg', 'image/gif', 'image/png' );
			if ( ! jpcrm_file_check_mime_extension( $_FILES['zbsc_profile-picture-file'], $allowed_file_extensions, $allowed_mime_types ) ) {

				$this->dalErrorMessage( array( __( 'Error: Profile Picture only accepts jpg, png, and gif images!', 'zero-bs-crm' ) ) );
				return;

			}

			if ( $contact_dir_info === false ) {
				$this->dalErrorMessage( array( __( 'Error while retrieving the contact\'s folder.', 'zero-bs-crm' ) ) );
				return;
			}

			$avatar_path           = $contact_dir_info['avatar']['path'];
			$contact_folder_exists = jpcrm_create_and_secure_dir_from_external_access( $avatar_path, false );

			if ( ! $contact_folder_exists ) {
				$this->dalErrorMessage( array( __( 'There was an error creating the profile picture directory.', 'zero-bs-crm' ) ) );
				return;
			}

			$zbs->load_encryption();
			$avatar_filename = sprintf(
				'avatar_%s.%s',
				$zbs->encryption->get_rand_hex( 10 ),
				$allowed_image_types[ $_FILES['zbsc_profile-picture-file']['type'] ] // extension for this filetype
			);

			if ( ! file_exists( $avatar_path . '/' . $avatar_filename )
				&& move_uploaded_file( $_FILES['zbsc_profile-picture-file']['tmp_name'], $avatar_path . '/' . $avatar_filename )
			) {
				$zbs->DAL->contacts->addUpdateContact(
					array(
						'id'            => $contact_id,
						'limitedFields' => array(
							array(
								'key'  => 'zbsc_avatar',
								'val'  => $contact_dir_info['avatar']['url'] . '/' . $avatar_filename,
								'type' => '%s',
							),
						),
					)
				);

				$remove_old_avatar = true;
			} else {
				$this->dalErrorMessage( array( __( 'There was an error updating the profile picture.', 'zero-bs-crm' ) ) );
			}
		}

		if ( $remove_old_avatar && ! empty( $crm_contact['avatar'] ) ) {
			$previous_avatar_full_path = $contact_dir_info['avatar']['path'] . '/' . basename( $crm_contact['avatar'] );
			if ( file_exists( $previous_avatar_full_path ) ) {
				unlink( $previous_avatar_full_path );
			}
		}
	}

	// This catches 'new' contacts + redirs to right url
	public function post_save_data( $objID, $obj ) {

		if ( $this->newRecordNeedsRedir ) {

			global $zbs, $zbsJustInsertedMetaboxID;
			if ( ! empty( $zbsJustInsertedMetaboxID ) && $zbsJustInsertedMetaboxID > 0 ) {

				// redir
				$zbs->new_record_edit_redirect( 'zerobs_customer', $zbsJustInsertedMetaboxID );
			}
		}
	}

	public function updateMessage( $created = false ) {
		$text = $created ? __( 'Contact Created', 'zero-bs-crm' ) : __( 'Contact Updated', 'zero-bs-crm' );
		// zbs-not-urgent means it'll auto hide after 1.5s
		$msg = zeroBSCRM_UI2_messageHTML( 'info olive mini zbs-not-urgent', $text, '', 'address book outline', 'contactUpdated' );

		// quick + dirty
		global $zbs;

		$zbs->pageMessages[] = $msg;
	}

	public function update_invalid_email( $invalid_email ) {
		global $zbs;
		$msg                 = zeroBSCRM_UI2_messageHTML(
			'info orange mini',
			sprintf( __( 'The contact email specified (%s) is not valid.', 'zero-bs-crm' ), $invalid_email ),
			'',
			'address book outline',
			'contactUpdated'
		);
		$zbs->pageMessages[] = $msg;
	}

	public function updateEmailDupeMessage( $otherContactID = -1 ) {

		$viewHTML = ' <a href="' . jpcrm_esc_link( 'view', $otherContactID, 'zerobs_customer' ) . '" target="_blank">' . __( 'View Contact', 'zero-bs-crm' ) . '</a>';

		$msg = zeroBSCRM_UI2_messageHTML( 'info orange mini', __( 'Contact email could not be updated because a contact already exists with this email address.', 'zero-bs-crm' ) . $viewHTML, '', 'address book outline', 'contactUpdated' );

		// quick + dirty
		global $zbs;

		$zbs->pageMessages[] = $msg;
	}
}

/*
======================================================
	/ Customer Metabox
	====================================================== */

/*
======================================================
	Create Actions Box
	====================================================== */

class zeroBS__Metabox_ContactActions extends zeroBS__Metabox {

	private $actions = array();

	public function __construct( $plugin_file ) {

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-actions';
		$this->metaboxTitle    = __( 'Contact Actions', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'side';
		$this->metaboxLocation = 'high';
		$this->headless        = true;
		$this->capabilities    = array(

			'can_hide'        => false, // can be hidden
			'areas'           => array( 'side' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => false, // can be minimised
			'can_move'        => false, // can be moved

		);

		// hacky id check for now:
		if ( isset( $_GET['zbsid'] ) && ! empty( $_GET['zbsid'] ) ) {
			$id = (int) sanitize_text_field( $_GET['zbsid'] );
			// call this, if actions
			$this->actions = zeroBS_contact_actions( $id );

		}
		$this->initMetabox();
	}

	public function html( $contact, $metabox ) {

		$avatarMode     = zeroBSCRM_getSetting( 'avatarmode' );
		$avatarStr      = '';
		$is_new_contact = count( $this->actions ) > 0 ? false : true;
		if ( $avatarMode !== 3 ) {

			$cID = -1;
			if ( is_array( $contact ) && isset( $contact['id'] ) ) {
				$cID = (int) $contact['id'];
			}
			$avatarStr = zeroBS_customerAvatarHTML( $cID );

		}

		?>
		<div class="zbs-generic-save-wrap">
			<div class="ui medium dividing header"><i class="save icon"></i> <?php esc_html_e( 'Contact Actions', 'zero-bs-crm' ); ?></div>
			<div class="clear"></div>
		<?php

		# https://codepen.io/kyleshockey/pen/bdeLrE
		if ( ! $is_new_contact ) {
			?>
		<script type="text/javascript">
			var zbsContactAvatarLang = <?php echo wp_json_encode( array( 'upload' => __( 'Upload Image', 'zero-bs-crm' ) ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;
		</script>
		<div class="action-wrap">
			<div class="ui dropdown jpcrm-button white-bg jpcrm-dropdown"><?php esc_html_e( 'Contact Actions', 'zero-bs-crm' ); ?><i class="fa fa-angle-down"></i>
				<div class="menu" style="margin: 4px;">
				<?php
				foreach ( $this->actions as $actKey => $action ) {

					// filter out 'edit' as on that page :)
					if ( $actKey != 'edit' ) {
						?>
				<div class="item zbs-contact-action" id="zbs-contact-action-<?php echo esc_attr( $actKey ); ?>"
						<?php
						// if url isset, pass that data-action, otherwise leave for js to attach to
						if ( ! empty( $action['url'] ) ) {
							?>
							data-action="url" data-url="<?php echo esc_attr( $action['url'] ); ?>"
							<?php
						}

						// got extra attributes?
						if ( isset( $action['extraattr'] ) && is_array( $action['extraattr'] ) ) {

								// dump extra attr into item
							foreach ( $action['extraattr'] as $k => $v ) {
								echo ' data-' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
							}
						}
						?>
						>
						<?php

						// got ico?
						if ( isset( $action['ico'] ) ) {
							echo '<i class="' . esc_attr( $action['ico'] ) . '"></i>';
						}

						// got text?
						if ( isset( $action['label'] ) ) {
							echo esc_html( $action['label'] );
						}

						?>
				</div>
						<?php
					}
				}
				?>
				</div>
		</div>
		</div>
		<script type="text/javascript">
		jQuery(function(){

			// actions drop down
			jQuery('.ui.dropdown').dropdown();

			// action items
			jQuery('.zbs-contact-action').off('click').on( 'click', function(){

				// get action type (at launch, only url)
				var actionType = jQuery(this).attr('data-action');

				if (typeof actionType != "undefined") switch (actionType){

					case 'url':

						var u = jQuery(this).attr('data-url');
						if (typeof u != "undefined" && u != '') window.location = u;

						break;


				}

			});

		});
		</script>
			<?php
		}
		?>
			<div class="zbs-contact-actions-bottom zbs-objedit-actions-bottom">
				<button class="jpcrm-button" type="button" id="zbs-edit-save"><?php $is_new_contact ? esc_html_e( 'Save', 'zero-bs-crm' ) : esc_html_e( 'Update', 'zero-bs-crm' ); ?> <?php esc_html_e( 'Contact', 'zero-bs-crm' ); ?></button>
				<div class='clear'></div>
			</div>
			<?php
	}

	public function save_data( $contact_id, $contact ) {

		// avatar changes saved by main contact save func (field editor), allowing for all-in-one creation/updates, see #AVATARSAVE

		return $contact;
	}
}

/*
======================================================
	/ Create Actions Box
	====================================================== */

/*
======================================================
	Attach (custom) fileboxes to customer metabox
	====================================================== */

class zeroBS__Metabox_ContactCustomFiles extends zeroBS__Metabox {

	public function __construct( $plugin_file, $idOverride = '', $titleOverride = '' ) {

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-custom-files';
		$this->metaboxTitle    = __( 'Other Files', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'normal';
		$this->metaboxLocation = 'low';
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'normal' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => true, // can be added as tab
			'can_minimise'    => true, // can be minimised

		);

		if ( ! empty( $idOverride ) ) {
			$this->metaboxID = $idOverride;
		}
		if ( ! empty( $titleOverride ) ) {
			$this->metaboxTitle = __( $titleOverride, 'zero-bs-crm' );
		}

		// call this
		$this->initMetabox();
	}

	public function html( $contact, $args ) {

		global $zbs;

				$html = '';

				$thisFileSlotName = '';
		if ( isset( $args['title'] ) ) {
			$thisFileSlotName = $args['title'];
		}
				$filePerma = ''; if ( isset( $thisFileSlotName ) && ! empty( $thisFileSlotName ) ) {
					// $filePerma = strtolower(str_replace(' ','_',str_replace('.','_',substr($thisFileSlotName,0,20))));
					$filePerma = $zbs->DAL->makeSlug( $thisFileSlotName );
				}

				// phpcs:disable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
				$zbsFiles = array();
				if ( $contact ) {
					// retrieve - shouldn't these vars be "other files"... confusing
					$zbsFiles = zeroBSCRM_getCustomerFiles( $contact['id'] );

					// This specifically looks for $args['title'] file
					$fileSlotSrc = zeroBSCRM_fileslots_fileInSlot( $filePerma, $contact['id'], ZBS_TYPE_CONTACT );

					// check for file + only show that
					$zbsFilesArr = array();
					if ( $fileSlotSrc !== '' && is_array( $zbsFiles ) && count( $zbsFiles ) > 0 ) {
						foreach ( $zbsFiles as $f ) {
							if ( $f['file'] === $fileSlotSrc ) {
								$zbsFilesArr[] = $f;
							}
						}
					}
					$zbsFiles = $zbsFilesArr;
				}

				// while we only have 1 file per slot, we can do this:
				// *js uses this to empty if deleted elsewhere (other metabox)
				$fileSlotURL = '';
				if ( is_array( $zbsFiles ) && count( $zbsFiles ) === 1 ) {
					$fileSlotURL = $zbsFiles[0]['url'];
				}
				// phpcs:enable WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase

				?>
							<table class="form-table wh-metatab wptbp zbsFileSlotTable" data-sloturl="<?php echo esc_attr( $fileSlotURL ); ?>" id="<?php echo esc_attr( $this->metaboxID ); ?>-tab">

							<?php

							#} Any slot filled?
							if ( is_array( $zbsFiles ) && count( $zbsFiles ) > 0 ) {
								?>
									<tr class="wh-large zbsFileSlotWrap"><th class="zbsFileSlotTitle"><label><?php echo '<span>' . esc_html( count( $zbsFiles ) ) . '</span> ' . esc_html__( 'File(s)', 'zero-bs-crm' ) . ':'; ?></label></th>
											<td class="">
											<?php
												$fileLineIndx = 1; foreach ( $zbsFiles as $zbsFile ) {

													/*
													$file = basename($zbsFile['file']);

													// if in privatised system, ignore first hash in name
													if (isset($zbsFile['priv'])){

														$file = substr($file,strpos($file, '-')+1);
													} */
													$file = zeroBSCRM_files_baseName( $zbsFile['file'], isset( $zbsFile['priv'] ) );
													echo '<div class="zbsFileLine" id="zbsFileLineCustomer' . esc_attr( $fileLineIndx ) . '"><a href="' . esc_url( $zbsFile['url'] ) . '" target="_blank">' . esc_html( $file ) . '</a> </div>';

													// if using portal.. state shown/hidden
													// this is also shown in each file slot :) if you change any of it change that too
												if ( defined( 'ZBS_CLIENTPRO_TEMPLATES' ) ) {
													if ( isset( $zbsFile['portal'] ) && $zbsFile['portal'] ) {
														echo "<p><i class='icon check circle green inverted'></i> " . esc_html__( 'Shown on Portal', 'zero-bs-crm' ) . '</p>';
													} else {
														echo "<p><i class='icon ban inverted red'></i> " . esc_html__( 'Not shown on Portal', 'zero-bs-crm' ) . '</p>';
													}
												}

													++$fileLineIndx;

												}
												?>
											</td></tr>
											<?php

							}
							?>

								<?php
								#adapted from http://code.tutsplus.com/articles/attaching-files-to-your-posts-using-wordpress-custom-meta-boxes-part-1--wp-22291

									// will be done by mainfunc wp_nonce_field(plugin_basename(__FILE__), 'zbsc_file_attachment_nonce');

									$html .= '<input type="file" id="zbsc_file_' . $filePerma . '" name="zbsc_file_' . $filePerma . '" size="25" class="zbs-dc">';

								?>
										<tr class="wh-large"><th><label><?php esc_html_e( 'Add File', 'zero-bs-crm' ); ?>:</label><br />(<?php esc_html_e( 'Optional', 'zero-bs-crm' ); ?>)<br /><?php esc_html_e( 'Accepted File Types', 'zero-bs-crm' ); ?>:<br /><?php echo esc_html( zeroBS_acceptableFileTypeListStr() ); ?></th>
											<td>
											<?php
											echo $html;
											?>
								</td></tr>

							
							</table>
							<?php

							// PerfTest: zeroBSCRM_performanceTest_finishTimer('custmetabox');
							// PerfTest: zeroBSCRM_performanceTest_debugOut();

							?>
							<script type="text/javascript">

								jQuery(function(){

								});


							</script>

							<?php

							// PerfTest: zeroBSCRM_performanceTest_finishTimer('other');
	}

	public function save_data( $contact_id, $contact ) {

					// when multiple custom file boxes, this only needs to fire once :)
		if ( zeroBSCRM_is_customer_edit_page() && ! defined( 'ZBS_CUSTOMFILES_SAVED' ) ) {

			define( 'ZBS_CUSTOMFILES_SAVED', 1 );

			global $zbsc_justUploadedCustomer, $zbs;

			$settings = $zbs->settings->get( 'customfields' );
			$cfbInd   = 1;

			$cfbsubs = array();

			if ( isset( $settings['customersfiles'] ) && is_array( $settings['customersfiles'] ) && count( $settings['customersfiles'] ) > 0 ) {
				foreach ( $settings['customersfiles'] as $cfb ) {

					$thisFileSlotName = '';
					if ( isset( $cfb[0] ) ) {
						$thisFileSlotName = $cfb[0];
					}
					$filePerma = ''; if ( isset( $thisFileSlotName ) && ! empty( $thisFileSlotName ) ) {

						// $filePerma = strtolower(str_replace(' ','_',str_replace('.','_',substr($thisFileSlotName,0,20))));
						$filePerma = $zbs->DAL->makeSlug( $thisFileSlotName );

					}

					if ( ! empty( $thisFileSlotName ) && ! empty( $filePerma ) ) {
						$cfbsubs[ $filePerma ] = $thisFileSlotName;
					}
				}
			}

			if ( count( $cfbsubs ) > 0 ) {
				foreach ( $cfbsubs as $cfSubKey => $cfSubName ) {

								/* --- security verification --- */
					if ( isset( $_POST['zbsc_file_attachment_nonce'] ) ) {
						if ( ! wp_verify_nonce( $_POST['zbsc_file_attachment_nonce'], plugin_basename( __FILE__ ) ) ) {
								return $contact_id;
						} // end if
					}

					if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
						return $contact_id;
					} // end if

					if ( ! zeroBSCRM_permsCustomers() ) {
						return $contact_id;
					}
							/* - end security verification - */

					if ( ! empty( $_FILES[ 'zbsc_file_' . $cfSubKey ]['name'] ) &&
							( ! isset( $zbsc_justUploadedCustomer ) ||
							( isset( $zbsc_justUploadedCustomer ) && $zbsc_justUploadedCustomer != $_FILES[ 'zbsc_file_' . $cfSubKey ]['name'] )
							)
							) {

						// Blocking repeat-upload bug
						$zbsc_justUploadedCustomer = $_FILES[ 'zbsc_file_' . $cfSubKey ]['name'];

						// verify file extension and mime type
						if ( jpcrm_file_check_mime_extension( $_FILES[ 'zbsc_file_' . $cfSubKey ] ) ) {

							$upload = wp_upload_bits( $_FILES[ 'zbsc_file_' . $cfSubKey ]['name'], null, file_get_contents( $_FILES[ 'zbsc_file_' . $cfSubKey ]['tmp_name'] ) );

							if ( isset( $upload['error'] ) && $upload['error'] != 0 ) {
								wp_die( 'There was an error uploading your file. The error is: ' . esc_html( $upload['error'] ) );
							} else {
								// update_post_meta($contact_id, 'zbsc_file_'.$cfSubKey, $upload);

								// v2.13 - also privatise the file (move to our asset store)
								// $upload will have 'file' and 'url'
								$fileName        = basename( $upload['file'] );
								$fileDir         = dirname( $upload['file'] );
								$privateThatFile = zeroBSCRM_privatiseUploadedFile( $fileDir, $fileName );
								if ( is_array( $privateThatFile ) && isset( $privateThatFile['file'] ) ) {

									// successfully moved to our store

										// modify URL + file attributes
										$upload['file'] = $privateThatFile['file'];
										$upload['url']  = $privateThatFile['url'];

										// add this extra identifier if in privatised sys
										$upload['priv'] = true;

								} else {

									// Could not move the file into private CRM storage.
									// Delete the public copy left by `wp_upload_bits()`.
									if ( file_exists( $upload['file'] ) ) {
										wp_delete_file( $upload['file'] );
									}
									wp_die( 'There was an error storing your uploaded file.' );

								}

								// w mod - adds to array :)
								$zbsCustomerFiles = zeroBSCRM_getCustomerFiles( $contact_id );

								if ( is_array( $zbsCustomerFiles ) ) {

									// add it
									$zbsCustomerFiles[] = $upload;

								} else {

									// first
									$zbsCustomerFiles = array( $upload );

								}

								// update_post_meta($contact_id, 'zbs_customer_files', $zbsCustomerFiles);
								zeroBSCRM_updateCustomerFiles( $contact_id, $zbsCustomerFiles );

								// this'll override any prev in that slot, too
								zeroBSCRM_fileslots_addToSlot( $cfSubKey, $upload['file'], $contact_id, ZBS_TYPE_CONTACT, true );

								// Fire any 'post-upload-processing' (e.g. CPP makes thumbnails of pdf, jpg, etc.)
								do_action( 'zbs_post_upload_contact', $upload );
							}
						} else {
							wp_die( "The file type that you've uploaded is not an accepted file format." );
						}
					} // if file

				} /// / foreach
			}
		}

			return $contact;
	}
}

/*
======================================================
	/ Attach (custom) fileboxes to customer metabox
	====================================================== */

/*
======================================================
	Attach files to customer metabox
	====================================================== */

/*
======================================================
	Contact Files Metabox
	====================================================== */

class zeroBS__Metabox_ContactFiles extends zeroBS__Metabox {

	public function __construct( $plugin_file ) {

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-files';
		$this->metaboxTitle    = __( 'Other Files', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'normal';
		$this->metaboxLocation = 'low';
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'normal' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => true,  // can/can't accept tabs onto it
			'can_become_tab'  => true, // can be added as tab
			'can_minimise'    => true, // can be minimised

		);

		// call this
		$this->initMetabox();
	}

	public function html( $contact, $metabox ) {

			global $zbs;

			$html = '';

			// wmod

					#} retrieve - shouldn't these vars be "other files"... confusing
					$zbsFiles = false;
		if ( isset( $contact['id'] ) ) {
			$zbsFiles = zeroBSCRM_getCustomerFiles( $contact['id'] );
		}

		?>
				<table class="form-table wh-metatab wptbp" id="wptbpMetaBoxMainItemFiles">

				<?php

				#} Whole file delete method could do with rewrite
				#} Also sort JS into something usable - should be ajax all this

				#} Any existing
				if ( is_array( $zbsFiles ) && count( $zbsFiles ) > 0 ) {
					?>
						<tr class="wh-large zbsFileDetails"><th class="zbsFilesTitle"><label><?php echo '<span>' . count( $zbsFiles ) . '</span> ' . esc_html__( 'File(s)', 'zero-bs-crm' ) . ':'; ?></label></th>
								<td id="zbsFileWrapOther">
									<table class="ui celled table" id="zbsFilesTable">
										<thead>
										<tr>
											<th><?php esc_html_e( 'File', 'zero-bs-crm' ); ?></th>
											<th class="collapsing center aligned"><?php esc_html_e( 'Actions', 'zero-bs-crm' ); ?></th>
										</tr>
									</thead><tbody>
											<?php
												$fileLineIndx = 1; foreach ( $zbsFiles as $zbsFile ) {

													/*
													$file = basename($zbsFile['file']);

													// if in privatised system, ignore first hash in name
													if (isset($zbsFile['priv'])){

														$file = substr($file,strpos($file, '-')+1);
													} */
													$file = zeroBSCRM_files_baseName( $zbsFile['file'], isset( $zbsFile['priv'] ) );

													$fileEditUrl = admin_url( 'admin.php?page=' . $zbs->slugs['editfile'] ) . '&customer=' . $contact['id'] . '&fileid=' . ( $fileLineIndx - 1 );

													echo '<tr class="zbsFileLineTR" id="zbsFileLineTRCustomer' . esc_attr( $fileLineIndx ) . '">';
													echo '<td><div class="zbsFileLine" id="zbsFileLineCustomer' . esc_attr( $fileLineIndx ) . '"><a href="' . esc_url( $zbsFile['url'] ) . '" target="_blank">' . esc_html( $file ) . '</a></div>';

													// if using portal.. state shown/hidden
													// this is also shown in each file slot :) if you change any of it change that too
												if ( defined( 'ZBS_CLIENTPRO_TEMPLATES' ) ) {
													if ( isset( $zbsFile['portal'] ) && $zbsFile['portal'] ) {
														echo "<p><i class='icon check circle green inverted'></i> " . esc_html__( 'Shown on Portal', 'zero-bs-crm' ) . '</p>';
													} else {
														echo "<p><i class='icon ban inverted red'></i> " . esc_html__( 'Not shown on Portal', 'zero-bs-crm' ) . '</p>';
													}
												}

													echo '</td>';
													echo '<td class="collapsing center aligned"><span class="zbsDelFile ui button basic" data-delurl="' . esc_attr( $zbsFile['url'] ) . '"><i class="trash alternate icon"></i> ' . esc_html__( 'Delete', 'zero-bs-crm' ) . '</span> <a href="' . esc_url( $fileEditUrl ) . '" target="_blank" class="ui button basic"><i class="edit icon"></i> ' . esc_html__( 'Edit', 'zero-bs-crm' ) . '</a></td></tr>';
													++$fileLineIndx;

												}
												?>
									</tbody></table>
								</td></tr>
								<?php

				}
				?>

					<?php
					#adapted from http://code.tutsplus.com/articles/attaching-files-to-your-posts-using-wordpress-custom-meta-boxes-part-1--wp-22291

						$html .= '<input type="file" id="zbsc_file_attachment" name="zbsc_file_attachment" size="25" class="zbs-dc">';

					?>
							<tr class="wh-large"><th><label><?php esc_html_e( 'Add File', 'zero-bs-crm' ); ?>:</label><br />(<?php esc_html_e( 'Optional', 'zero-bs-crm' ); ?>)<br /><?php esc_html_e( 'Accepted File Types', 'zero-bs-crm' ); ?>:<br /><?php echo esc_html( zeroBS_acceptableFileTypeListStr() ); ?></th>
								<td>
								<?php
								wp_nonce_field( plugin_basename( __FILE__ ), 'zbsc_file_attachment_nonce' );
								echo $html;
								?>
					</td></tr>

				
				</table>
				<?php

				// PerfTest: zeroBSCRM_performanceTest_finishTimer('custmetabox');
				// PerfTest: zeroBSCRM_performanceTest_debugOut();

				$jpcrm_metabox_files_lang = array(
					'error'          => __( 'Error', 'zero-bs-crm' ),
					'unabletodelete' => __( 'Unable to delete this file.', 'zero-bs-crm' ),
				);

				?>
				<script type="text/javascript">

					var zbsCustomerCurrentlyDeleting = false;
					var zbsMetaboxFilesLang = <?php echo wp_json_encode( $jpcrm_metabox_files_lang, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>;

					jQuery(function(){

						jQuery('.zbsDelFile').on( 'click', function(){

							if (!window.zbsCustomerCurrentlyDeleting){

								// blocking
								window.zbsCustomerCurrentlyDeleting = true;

								var delUrl = jQuery(this).attr('data-delurl');
								//var lineIDtoRemove = jQuery(this).closest('.zbsFileLine').attr('id');
								var lineToRemove = jQuery(this).closest('tr');

								if (typeof delUrl != "undefined" && delUrl != ''){



										// postbag!
										var data = {
										'action': 'delFile',
										'zbsfType': 'customer',
										'zbsDel':  delUrl, // could be csv, never used though
										'zbsCID': <?php echo ( ! empty( $contact['id'] ) && $contact['id'] > 0 ) ? (int) $contact['id'] : -1; ?>,
										'sec': window.zbscrmjs_secToken
										};

										// Send it Pat :D
										jQuery.ajax({
												type: "POST",
												url: ajaxurl, // admin side is just ajaxurl not wptbpAJAX.ajaxurl,
												"data": data,
												dataType: 'json',
												timeout: 20000,
												success: function(response) {

												var localLineToRemove = lineToRemove, localDelURL = delUrl;

												// visually remove
												jQuery(localLineToRemove).remove();

												// update number
												var newNumber = jQuery('#zbsFilesTable tr').length-1;
												if (newNumber > 0)
													jQuery('#wptbpMetaBoxMainItemFiles .zbsFilesTitle span').html();
												else
													jQuery('#wptbpMetaBoxMainItemFiles .zbsFileDetails').remove();


												// remove any filled slots (with this file) 
												jQuery('.zbsFileSlotTable').each(function(ind,ele){

													if (jQuery(ele).attr('data-sloturl') == localDelURL){

														jQuery('.zbsFileSlotWrap',jQuery(ele)).remove();
												
													}

												});

												// file deletion errors, show msg:
												if (typeof response.errors != "undefined" && response.errors.length > 0){

													jQuery.each(response.errors,function(ind,ele){

														jQuery('#zerobs-customer-files-box').append('<div class="ui warning message" style="margin-top:10px;">' + ele + '</div>');

													});
														 

												}


												},
												error: function(response){

												jQuery('#zerobs-customer-files-box').append('<div class="ui warning message" style="margin-top:10px;"><strong>' + window.zbsMetaboxFilesLang.error + ':</strong> ' + window.zbsMetaboxFilesLang.unabletodelete + '</div>');

												}

											});

								}

								window.zbsCustomerCurrentlyDeleting = false;

							} // / blocking

						});

					});


				</script>
				<?php

				// PerfTest: zeroBSCRM_performanceTest_finishTimer('other');
	}

	public function save_data( $contact_id, $contact ) {

		global $zbsc_justUploadedCustomer;

		if ( ! empty( $_FILES['zbsc_file_attachment']['name'] ) &&
			( ! isset( $zbsc_justUploadedCustomer ) ||
				( isset( $zbsc_justUploadedCustomer ) && $zbsc_justUploadedCustomer != $_FILES['zbsc_file_attachment']['name'] )
			)
			) {

			/* --- security verification --- */
			if ( ! wp_verify_nonce( $_POST['zbsc_file_attachment_nonce'], plugin_basename( __FILE__ ) ) ) {
				return $id;
			} // end if

			if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
				return $id;
			} // end if

			if ( ! zeroBSCRM_permsCustomers() ) {
				return $contact_id;
			}
			/* - end security verification - */

			// Blocking repeat-upload bug
			$zbsc_justUploadedCustomer = $_FILES['zbsc_file_attachment']['name'];

			// verify file extension and mime type
			if ( jpcrm_file_check_mime_extension( $_FILES['zbsc_file_attachment'] ) ) {

				$upload = wp_upload_bits( $_FILES['zbsc_file_attachment']['name'], null, file_get_contents( $_FILES['zbsc_file_attachment']['tmp_name'] ) );

				if ( isset( $upload['error'] ) && $upload['error'] != 0 ) {
					wp_die( 'There was an error uploading your file. The error is: ' . esc_html( $upload['error'] ) );
				} else {
					// update_post_meta($id, 'zbsc_file_attachment', $upload);

						// v2.13 - also privatise the file (move to our asset store)
						// $upload will have 'file' and 'url'
						$fileName        = basename( $upload['file'] );
						$fileDir         = dirname( $upload['file'] );
						$privateThatFile = zeroBSCRM_privatiseUploadedFile( $fileDir, $fileName );
					if ( is_array( $privateThatFile ) && isset( $privateThatFile['file'] ) ) {

						// successfully moved to our store

							// modify URL + file attributes
							$upload['file'] = $privateThatFile['file'];
							$upload['url']  = $privateThatFile['url'];

							// add this extra identifier if in privatised sys
							$upload['priv'] = true;

					} else {

						// Could not move the file into private CRM storage.
						// Delete the public copy left by `wp_upload_bits()`.
						if ( file_exists( $upload['file'] ) ) {
							wp_delete_file( $upload['file'] );
						}
						wp_die( 'There was an error storing your uploaded file.' );

					}

						// w mod - adds to array :)
						$zbsCustomerFiles = zeroBSCRM_getCustomerFiles( $contact_id );

					if ( is_array( $zbsCustomerFiles ) ) {

						// add it
						$zbsCustomerFiles[] = $upload;

					} else {

						// first
						$zbsCustomerFiles = array( $upload );

					}

						// update_post_meta($id, 'zbs_customer_files', $zbsCustomerFiles);
						zeroBSCRM_updateCustomerFiles( $contact_id, $zbsCustomerFiles );

						// Fire any 'post-upload-processing' (e.g. CPP makes thumbnails of pdf, jpg, etc.)
						do_action( 'zbs_post_upload_contact', $upload );
				}
			} else {
				wp_die( "The file type that you've uploaded is not an accepted file format." );
			}
		}

		return $contact;
	}
}

/*
======================================================
	/ Attach files to customer metabox
	====================================================== */

/*
======================================================
	Create Client Portal
	====================================================== */

class zeroBS__Metabox_ContactPortal extends zeroBS__Metabox {

	public function __construct( $plugin_file, $metabox_screen = 'zbs-add-edit-contact-edit' ) {

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-portal';
		$this->metaboxTitle    = __( 'Client Portal', 'zero-bs-crm' );
		$this->metaboxScreen   = $metabox_screen; // we can use anything here as is now using our func
		$this->metaboxArea     = 'side';
		$this->metaboxLocation = 'high';
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'side' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => true, // can be minimised

		);

		// call this
		$this->initMetabox();
	}

	public function html( $contact, $metabox ) {

		// PerfTest: zeroBSCRM_performanceTest_startTimer('portal-draw');

		global $plugin_page, $zbs;
		$screen = get_current_screen();

		$wp_user_id = '';
		#} Rather than reload all the time :)
		global $zbsContactEditing;

		#} retrieve
		// $zbsCustomer = get_post_meta($contact['id'], 'zbs_customer_meta', true);
		if ( ! isset( $zbsContactEditing ) && isset( $contact['id'] ) ) {
			$zbsCustomer       = zeroBS_getCustomer( $contact['id'], false, false, false );
			$zbsContactEditing = $zbsCustomer;
		} else {
			$zbsCustomer = $zbsContactEditing;
		}

		if ( isset( $zbsCustomer ) && is_array( $zbsCustomer ) && isset( $zbsCustomer['email'] ) ) {

			// check customer link to see if it exists - wh moved to dal
			$wp_user_id = zeroBSCRM_getClientPortalUserID( $contact['id'] );

			/*
			nope
			if($wp_user_id == ''){
				$wp_user_id = email_exists( $zbsCustomer['email'] );
			} */
		}

		echo '<div class="waiting-togen">';

		// get user obj
		$user_object = get_userdata( $wp_user_id );

		if ( $user_object ) {

			// a user already exists with this email

			echo '<div class="zbs-customerportal-activeuser">';

				esc_html_e( 'WordPress User Linked', 'zero-bs-crm' );
				echo ' #<span class="zbs-user-id">' . esc_html( $wp_user_id ) . '</span>:<br />';

				echo '<span class="ui label">' . esc_html( $user_object->user_email ) . '</span>';

				// wp admins get link
			if ( zeroBSCRM_isWPAdmin() ) {

				$url = admin_url( 'user-edit.php?user_id=' . $wp_user_id );
						echo '<br /><a style="font-size: 12px;color:black;font-weight:600;" href="' . esc_url( $url ) . '" target="_blank"><i class="wordpress simple icon"></i> ' . esc_html__( 'View WordPress Profile', 'zero-bs-crm' ) . '</a>'; // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText

			}

			echo '</div>';

			// user ID will now have access to this area..
			echo '<hr /><div class="zbs-customerportal-activeuser-actions">';

				echo esc_html( __( 'Client Portal Access:', 'zero-bs-crm' ) );

				$customerPortalActive = true;
			if ( zeroBSCRM_isCustomerPortalDisabled( $contact['id'] ) ) {
				$customerPortalActive = false;
			}

			if ( $customerPortalActive ) {

				// revoke/disable access
				echo ' <span class="ui green empty circular label"></span> <span class="zbs-portal-label">' . esc_html( __( 'Enabled', 'zero-bs-crm' ) ) . '</span>';

				// CRM/site admins can send a reset email as long as the CRM contact only has the CRM Customer role.
				if ( jpcrm_perms_manage_options() && jpcrm_role_check( $user_object, array(), array(), array( 'zerobs_customer' ) ) ) {

					echo '<div id="zbs-customerportal-access-actions" class="zbs-customerportal-activeuser">';

							echo '<button type="button" id="zbs-customerportal-resetpw" class="ui mini button white">' . esc_html( __( 'Reset Password', 'zero-bs-crm' ) ) . '</button>';

							echo '<button type="button" id="zbs-customerportal-toggle" data-zbsportalaction="disable" class="ui mini button white negative">' . esc_html( __( 'Disable Access', 'zero-bs-crm' ) ) . '</button>';

					echo '</div>';

				} else {

					// explainer - rarely shown
					echo '<p style="font-size: 0.9em;margin-top: 0.5em;">' . esc_html__( 'You cannot edit this user. Password resets must be done via the WP login page.', 'zero-bs-crm' ) . '</p>';

				}

				echo '<hr /><div class="zbs-customerportal-activeuser-actions">';
						printf( '<a target="_blank" href="%s" class="ui mini button white">%s</a>', esc_url( zeroBS_portal_link() ), esc_html( __( 'Preview Portal', 'zero-bs-crm' ) ) );
				echo '</div>';
			} else {

				// enable access
				echo ' <span class="ui red empty circular label"></span> <span class="zbs-portal-label">' . esc_html( __( 'Disabled', 'zero-bs-crm' ) ) . '</span>';

				// CRM/site admins can enable/disable Client Portal access as long as the CRM contact only has the CRM Customer role.
				if ( jpcrm_perms_manage_options() && jpcrm_role_check( $user_object, array(), array(), array( 'zerobs_customer' ) ) ) {

					echo '<div id="zbs-customerportal-access-actions">';
						echo '<button type="button" id="zbs-customerportal-toggle" data-zbsportalaction="enable" class="ui mini button positive">' . esc_html( __( 'Enable Access', 'zero-bs-crm' ) ) . '</button>';
					echo '</div>';

				}
			}

				echo '<input type="hidden" id="zbsportalaction-ajax-nonce" value="' . esc_attr( wp_create_nonce( 'zbsportalaction-ajax-nonce' ) ) . '" />';

			echo '</div>';

		} elseif ( is_array( $zbsCustomer ) && isset( $zbsCustomer['email'] ) && ! empty( $zbsCustomer['email'] ) ) {
			echo '<div class="no-gen" style="text-align:center">';
			echo esc_html( __( 'No WordPress User exists with this email', 'zero-bs-crm' ) );
			echo '<br/><br/>';
			if ( jpcrm_perms_manage_options() ) {
				echo '<div class="ui primary black button button-primary wp-user-generate">';
				echo esc_html( __( 'Generate WordPress User', 'zero-bs-crm' ) );
				echo '</div>';
				echo '<input type="hidden" name="newwp-ajax-nonce" id="newwp-ajax-nonce" value="' . esc_attr( wp_create_nonce( 'newwp-ajax-nonce' ) ) . '" />';
			}
			echo '</div>';
		} else {
			echo esc_html( __( 'Save your contact, or add an email to enable Client Portal functionality', 'zero-bs-crm' ) );
		}

		echo '</div>';

		?>
		<script type="text/javascript">

			jQuery(function(){

				// bind activate/deactivate
				jQuery('#zbs-customerportal-toggle').off("click").on('click',function(e){

					// action
					var action = jQuery(this).attr('data-zbsportalaction');

					// fire ajax
					var t = {
						action: "zbsPortalAction",
						portalAction: action,
						cid: <?php echo ( ! empty( $contact['id'] ) && $contact['id'] > 0 ) ? (int) $contact['id'] : -1; ?>,
						security: jQuery( '#zbsportalaction-ajax-nonce' ).val()
					}
					i = jQuery.ajax({
						url: ajaxurl,
						type: "POST",
						data: t,
						dataType: "json"
					});
					i.done(function(e) {
						//console.log(e);
						if(typeof e.success != "undefined"){

							// localise
							var cAction = action;

							if (action == 'enable'){

								// switch label
								jQuery('.ui.circular.label',jQuery('.zbs-customerportal-activeuser-actions')).removeClass('red').addClass('green');
								jQuery('.zbs-portal-label',jQuery('.zbs-customerportal-activeuser-actions')).html('<?php echo esc_html( __( 'Enabled', 'zero-bs-crm' ) ); ?>');
								jQuery('#zbs-customerportal-toggle').removeClass('positive').addClass('negative').html('<?php echo esc_html( __( 'Disable Access', 'zero-bs-crm' ) ); ?>').attr('data-zbsportalaction','disable');
								

							} else if (action == 'disable'){

								// switch label
								jQuery('.ui.circular.label',jQuery('.zbs-customerportal-activeuser-actions')).addClass('red').removeClass('green');
								jQuery('.zbs-portal-label',jQuery('.zbs-customerportal-activeuser-actions')).html('<?php echo esc_html( __( 'Disabled', 'zero-bs-crm' ) ); ?>');
								jQuery('#zbs-customerportal-toggle').removeClass('negative').addClass('positive').html('<?php echo esc_html( __( 'Enable Access', 'zero-bs-crm' ) ); ?>').attr('data-zbsportalaction','enable');
								

							}

						}
					}), i.fail(function(e) {
						//error
					});

				});

				// bind reset pw
				jQuery('#zbs-customerportal-resetpw').off("click").on('click',function(e){

					// fire ajax
					var t = {
						action: "zbsPortalAction",
						portalAction: 'resetpw',
						cid: <?php echo ( ! empty( $contact['id'] ) && $contact['id'] > 0 ) ? (int) $contact['id'] : -1; ?>,
						security: jQuery( '#zbsportalaction-ajax-nonce' ).val()
					}
					i = jQuery.ajax({
						url: ajaxurl,
						type: "POST",
						data: t,
						dataType: "json"
					});
					i.done(function(e) {
						if ( e && e.success ) {

							swal({
								titleText: <?php echo wp_json_encode( __( 'Client Portal Password Reset', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
								text: <?php echo wp_json_encode( __( 'Client Portal password has been reset for this contact, and they have been emailed with the new password.', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
								type: 'info',
							});

						} else {

							swal({
								titleText: <?php echo wp_json_encode( __( 'Client Portal Password Reset Error', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
								text: <?php echo wp_json_encode( __( 'Error: Client Portal password has not been reset for this contact.', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
								type: 'info',
							});

						}
					}), i.fail(function(e) {
						//error
					});

				});


				// bind create
				jQuery('.wp-user-generate').off("click").on('click',function(e){
					email = jQuery('#email').val();
					customerid = <?php echo ( ! empty( $contact['id'] ) && $contact['id'] > 0 ) ? (int) $contact['id'] : -1; ?>;
					if(email == ''){
						alert("The email field is blank. Please fill in the email and save");
						return false;
					}
					var t = {
						action: "zbs_new_user",
						email: email,
						cid: customerid,
						security: jQuery( '#newwp-ajax-nonce' ).val(),
					}                    
					i = jQuery.ajax({
						url: ajaxurl,
						type: "POST",
						data: t,
						dataType: "json"
					});
					i.done(function(e) {
						console.log(e);
						if(e.success){
							jQuery('.zbs-user-id').html(e.user_id);
							jQuery('.no-gen').remove();
							jQuery('.waiting-togen').html('<div class="alert alert-success">Success: ' + e.message + '</div>');
						} else {
							jQuery('.no-gen').remove();
							jQuery('.waiting-togen').html('<div class="alert alert-danger">Error: ' + e.message + '</div>');
						}
					}), i.fail(function(e) {
						//error
					});
				});

			});


		</script>
		<?php

		// PerfTest: zeroBSCRM_performanceTest_finishTimer('portal-draw');
		// PerfTest: zeroBSCRM_performanceTest_finishTimer('portal');
	}
}

/*
======================================================
	/ Create Client Portal
	====================================================== */

/*
======================================================
	Create Social Box
	====================================================== */

class zeroBS__Metabox_ContactSocial extends zeroBS__Metabox {

	public function __construct( $plugin_file ) {

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-social';
		$this->metaboxTitle    = __( 'Social Profiles', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'side';
		$this->metaboxLocation = 'high';
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'side' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => true, // can be minimised

		);

		// call this
		$this->initMetabox();
	}

	public function html( $contact, $metabox ) {

		global $plugin_page, $zbs;

		// declare + load existing
		global $zbsSocialAccountTypes;
		$zbsSocials = false;
		if ( isset( $contact['id'] ) ) {
			$zbsSocials = $zbs->DAL->contacts->getContactSocials( $contact['id'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase,WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		}

		if ( count( $zbsSocialAccountTypes ) > 0 ) {
			foreach ( $zbsSocialAccountTypes as $socialKey => $socialAccType ) {

				?>
			<div class="zbs-social-acc <?php echo esc_attr( $socialAccType['slug'] ); ?>" title="<?php echo esc_attr( $socialAccType['name'] ); ?>">
				<?php
				if ( ! empty( $zbsSocials[ $socialKey ] ) ) {

					// got acc? link to it
					$socialLink = zeroBSCRM_getSocialLink( $socialKey, $zbsSocials );

					?>
					<a href="<?php echo esc_url( $socialLink ); ?>" target="_blank" title="<?php echo esc_attr__( 'View', 'zero-bs-crm' ) . ' ' . esc_attr( $socialAccType['name'] ); ?>"><i class="fa <?php echo esc_attr( $socialAccType['fa'] ); ?>" aria-hidden="true"></i></a>
				<?php } else { ?>
					<i class="fa <?php echo esc_attr( $socialAccType['fa'] ); ?>" aria-hidden="true"></i>
				<?php } ?>
				<input
					type="text"
					class="zbs-social-acc-input zbs-dc"
					title="<?php echo esc_attr( $socialAccType['name'] ); ?>"
					name="zbs-social-<?php echo esc_attr( $socialAccType['slug'] ); ?>"
					id="zbs-social-<?php echo esc_attr( $socialAccType['slug'] ); ?>"
					value="<?php echo ! empty( $zbsSocials[ $socialKey ] ) ? esc_attr( $zbsSocials[ $socialKey ] ) : ''; ?>"
					placeholder="<?php echo esc_attr( $socialAccType['placeholder'] ); ?>"
				/>
			</div>
							<?php

			}
		}

		// ++ get counts etc.
	}

	public function save_data( $contact_id, $contact ) {

			$zbsSocials = array();

			global $zbsSocialAccountTypes;
		foreach ( $zbsSocialAccountTypes as $socialKey => $socialAccType ) {

			// set
			$zbsSocials[ $socialKey ] = false;

			// get from post if present
			if ( isset( $_POST[ 'zbs-social-' . $socialAccType['slug'] ] ) && ! empty( $_POST[ 'zbs-social-' . $socialAccType['slug'] ] ) ) {
				$zbsSocials[ $socialKey ] = sanitize_text_field( $_POST[ 'zbs-social-' . $socialAccType['slug'] ] );
			}
		}

			zeroBS_updateCustomerSocialAccounts( $contact_id, $zbsSocials );

		return $contact;
	}
}

/*
======================================================
	/ Create Social Box
	====================================================== */

/*
======================================================
	Create AKA Box
	====================================================== */

class zeroBS__Metabox_ContactAKA extends zeroBS__Metabox {

	public function __construct( $plugin_file ) {

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-aka';
		$this->metaboxTitle    = __( 'Contact Aliases (AKA)', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'side';
		$this->metaboxLocation = 'low';
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'side' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => true, // can be minimised

		);

		// call this
		$this->initMetabox();
	}

	public function html( $contact, $metabox ) {

		global $plugin_page, $zbs;
		$screen = get_current_screen();
		?>
		<div class="ui active inverted dimmer" style="display:none" id="zbs-aka-alias-loader"></div>
		<?php

		#} Rather than reload all the time :)
		global $zbsContactEditing;

		#} retrieve
		// $zbsCustomer = get_post_meta($contact['id'], 'zbs_customer_meta', true);
		if ( ! isset( $zbsContactEditing ) && isset( $contact['id'] ) ) {
			$zbsCustomer       = zeroBS_getCustomer( $contact['id'], false, false, false );
			$zbsContactEditing = $zbsCustomer;
		} else {
			$zbsCustomer = $zbsContactEditing;
		}

		if ( gettype( $zbsCustomer ) != 'array' ) {

			// new cust, can't add till saved.
			?>
			<div class="ui message"><?php esc_html_e( 'You will not be able to add an alias until you\'ve saved this contact', 'zero-bs-crm' ); ?></div>
			<?php

		} else {

			// customer saved, so proceed - aka mode

			// declare + load existing
			$customerAliases = zeroBS_getCustomerAliases( $contact['id'] );

			?>
			<div id="zbs-aka-alias-wrap">
			<?php

			// each alias: ID,aka_alias,aka_create,aka_lastupdated
			if ( is_array( $customerAliases ) && count( $customerAliases ) > 0 ) {
				foreach ( $customerAliases as $alias ) {

					?>
				<div class="zbs-aka-alias" id="zbs-aka-alias-<?php echo esc_attr( $alias['ID'] ); ?>">
					<div class="ui label"><?php echo esc_html( $alias['aka_alias'] ); ?> <button type="button" class="ui mini icon button negative zbs-aka-alias-remove" data-akaid="<?php echo esc_attr( $alias['ID'] ); ?>" title="<?php esc_attr_e( 'Remove Alias', 'zero-bs-crm' ); ?>"><i class="icon remove"></i></button></div>
				</div>
					<?php

				}
			}

			?>
			</div><div id="zbs-aka-alias-input-wrap">
				<input type="text" class="zbs-aka-alias-input" placeholder="<?php esc_attr_e( 'Add Alias.. e.g.', 'zero-bs-crm' ); ?> mike2@domain.com" />
				<div class="ui pointing label" style="display:none;margin-bottom: 1em;margin-top: 0;" id="zbs-aka-alias-input-msg"><?php esc_html_e( 'Must be a valid email', 'zero-bs-crm' ); ?></div>
						<button type="button" class="ui small black button primary" id="zbs-aka-alias-add"><?php esc_html_e( 'Add Alias', 'zero-bs-crm' ); ?></button>
			</div>

			<script type="text/javascript">
			var zbsAliasAKABlocker = false;
			jQuery(function(){

				jQuery('.zbs-aka-alias-input').on( 'keydown', function(){
							
					// hide 'must be valid email'
					jQuery('#zbs-aka-alias-input-msg').hide();

				});

				jQuery('#zbs-aka-alias-add').off('click').on( 'click', function(){

					var v = jQuery('.zbs-aka-alias-input').val();
					
					if ( typeof v === 'string' ) {
						v = v.trim();
					}

					// lazy check for now
					if (v != "" && zbscrm_JS_validateEmail(v)){

						// blocker
						if (!window.zbsAliasAKABlocker){

							// block
							window.zbsAliasAKABlocker = true;
							jQuery('#zbs-aka-alias-loader').show();

										// postbag!
										var data = {
											'action': 'addAlias',
											'cid': <?php echo ( ! empty( $contact['id'] ) && $contact['id'] > 0 ) ? (int) $contact['id'] : -1; ?>,
											'aka': v,
											'sec': window.zbscrmjs_secToken,
										};

										// Send it Pat :D
										jQuery.ajax({
												type: "POST",
												url: ajaxurl, // admin side is just ajaxurl not wptbpAJAX.ajaxurl,
												"data": data,
												dataType: 'json',
												timeout: 20000,
												success: function(response) {

												if (typeof response.res != "undefined"){

													//console.log('added:',response);

													var id = response.res;
													var alias = v;

													var lineHTML = '<div class="zbs-aka-alias" id="zbs-aka-alias-' + id + '"><div class="ui label">' + alias + ' <button type="button" class=" ui mini icon button negative zbs-aka-alias-remove" data-akaid="' + id + '""><i class="icon remove"></i></button></div></div>';

													// add to ui
													jQuery('#zbs-aka-alias-wrap').append(lineHTML);

													// empty this
													jQuery('.zbs-aka-alias-input').val('');

													// bind
													setTimeout(function(){

														zeroBSJS_bindAKAMode();

													},0);

													//unblock
													window.zbsAliasAKABlocker = false;
													jQuery('#zbs-aka-alias-loader').hide();

												} else {

													if (typeof response.fail != "undefined"){

														if (response.fail == 'existing'){

															// already in use err
															swal({
																titleText: <?php echo wp_json_encode( __( 'Error', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
																text: <?php echo wp_json_encode( __( 'This Alias is already in use by another contact.', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
																type: 'warning',
															});

														}

													} else {

														// general err
														swal({
															titleText: <?php echo wp_json_encode( __( 'Error', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
															text: <?php echo wp_json_encode( __( 'There was an error adding this alias', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
															type: 'warning',
														});

													}
													//unblock
													window.zbsAliasAKABlocker = false;
													jQuery('#zbs-aka-alias-loader').hide();
												}

												},
												error: function(response){

													// err
													swal({
														titleText: <?php echo wp_json_encode( __( 'Error', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
														text: <?php echo wp_json_encode( __( 'There was an error adding this alias', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
														type: 'warning'
													});
													//unblock
													window.zbsAliasAKABlocker = false;
													jQuery('#zbs-aka-alias-loader').hide();

												}

											});


						} // / blocker

					} // / if not empty 
					else {

						// not valid email, showxxx
						jQuery('#zbs-aka-alias-input-msg').show();



						// hide after 2s
						setTimeout(function(){
							jQuery('#zbs-aka-alias-input-msg').hide();
						},2000);

					}

				});

				
				// other bind
				zeroBSJS_bindAKAMode();


			});
	
			function zeroBSJS_bindAKAMode(){

				// hover over
				jQuery('.zbs-aka-alias').on( 'mouseenter', function () {
					jQuery(this).addClass("hovering");
				}).on( 'mouseleave', function () {
					jQuery(this).removeClass("hovering");
				});

				// remoe aka
				jQuery('.zbs-aka-alias-remove').off('click').on( 'click', function(){

					// blocker
					if (!window.zbsAliasAKABlocker){

						// block
						window.zbsAliasAKABlocker = true;
						jQuery('#zbs-aka-alias-loader').show();

						// get id
						var akaID = jQuery(this).attr('data-akaid');

						if (akaID > 0){


								// postbag!
								var data = {
								'action': 'removeAlias',
								'cid': <?php echo ( ! empty( $contact['id'] ) && $contact['id'] > 0 ) ? (int) $contact['id'] : -1; ?>,
								'akaid': akaID,
								'sec': window.zbscrmjs_secToken
								};

								// Send it Pat :D
								jQuery.ajax({
										type: "POST",
										url: ajaxurl, // admin side is just ajaxurl not wptbpAJAX.ajaxurl,
										"data": data,
										dataType: 'json',
										timeout: 20000,
										success: function(response) {

										if (typeof response.res != "undefined"){

											console.log('removed:',response);

											var lID = akaID;

											// remove from ui
											jQuery('#zbs-aka-alias-' + lID).remove();

											//unblock
											window.zbsAliasAKABlocker = false;
											jQuery('#zbs-aka-alias-loader').hide();

										} else {

											// err
											swal({
												titleText: <?php echo wp_json_encode( __( 'Error', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
												text: <?php echo wp_json_encode( __( 'There was an error removing this alias', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
												type: 'warning',
											});
											//unblock
											window.zbsAliasAKABlocker = false;
											jQuery('#zbs-aka-alias-loader').hide();
										}

										},
										error: function(response){

											// err
											swal({
												titleText: <?php echo wp_json_encode( __( 'Error', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
												text: <?php echo wp_json_encode( __( 'There was an error removing this alias', 'zero-bs-crm' ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>,
												type: 'warning',
											});
											//unblock
											window.zbsAliasAKABlocker = false;
											jQuery('#zbs-aka-alias-loader').hide();

										}

									});


						} // / akai id present

					}

				});
			}

			</script>
			<?php

		} // / if cust defined
	}
}

/*
======================================================
	/ Create AKA Box
	====================================================== */

/*
======================================================
	Create Tags Box
	====================================================== */

class zeroBS__Metabox_ContactTags extends zeroBS__Metabox_Tags {

	public function __construct( $plugin_file ) {

		$this->objTypeID       = ZBS_TYPE_CONTACT;
		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-tags';
		$this->metaboxTitle    = __( 'Contact Tags', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'side';
		$this->metaboxLocation = 'high';
		$this->showSuggestions = true;
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'side' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => true, // can be minimised

		);

		// call this
		$this->initMetabox();
	}

	// html + save dealt with by parent class :)
}

/*
======================================================
	/ Create Tags Box
	====================================================== */

/*
======================================================
	Create Logs Box
	====================================================== */

class zeroBS__Metabox_ContactLogs extends zeroBS__Metabox_LogsV2 {

	public function __construct( $plugin_file ) {

		$this->objtypeid = ZBS_TYPE_CONTACT;

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-logs';
		$this->metaboxTitle    = __( 'Activity Log', 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'normal';
		$this->metaboxLocation = 'high';
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'normal' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => true,  // can/can't accept tabs onto it
			'can_become_tab'  => true, // can be added as tab
			'can_minimise'    => true, // can be minimised
			'hide_on_new'     => true, // no point in adding logs while adding contact, add after creation :)

		);

		// call this
		$this->initMetabox();
	}

	// html + save dealt with by parent class :)
}

/*
======================================================
	/ Create Logs Box
	====================================================== */

/*
======================================================
	"Contacts at Company" Metabox
	====================================================== */

class zeroBS__Metabox_ContactCompany extends zeroBS__Metabox {

	public function __construct( $plugin_file ) {

		# (language switch)
		$companyOrOrg = zeroBSCRM_getSetting( 'coororg' );
		$companyLabel = jpcrm_label_company();

		$this->objType         = 'contact';
		$this->metaboxID       = 'zerobs-customer-company';
		$this->metaboxTitle    = __( $companyLabel, 'zero-bs-crm' );
		$this->metaboxScreen   = 'zbs-add-edit-contact-edit'; // 'zerobs_edit_contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'side';
		$this->metaboxLocation = 'core';
		$this->capabilities    = array(

			'can_hide'        => true, // can be hidden
			'areas'           => array( 'side' ), // areas can be dragged to - normal side = only areas currently
			'can_accept_tabs' => false,  // can/can't accept tabs onto it
			'can_become_tab'  => false, // can be added as tab
			'can_minimise'    => true, // can be minimised

		);

		// call this
		$this->initMetabox();
	}

	public function html( $contact, $metabox ) {

		global $plugin_page, $zbs;

		// PerfTest: zeroBSCRM_performanceTest_startTimer('companydropdown');

		# (language switch)
		$companyOrOrg = zeroBSCRM_getSetting( 'coororg' );
		$companyLabel = jpcrm_label_company();

		#} Typeahead instead
		echo "<div id='zbscompnew'>";
		echo '<span>' . esc_html( sprintf( __( 'Type to assign %s', 'zero-bs-crm' ), jpcrm_label_company() ) ) . '</span>';

		#} Co Name Default
		$coName = '';
		$coID   = '';
		if ( 'contact' == $this->objType ) {
			$coID = -1;
			if ( is_array( $contact ) && isset( $contact['id'] ) ) {
				$coID = zeroBS_getCustomerCompanyID( $contact['id'] );
			}
			if ( ! empty( $coID ) ) {
				$co = zeroBS_getCompany( $coID );

				// 3.0
				if ( isset( $co ) && isset( $co['name'] ) ) {
					$coName = $co['name'];
				}

				// < 3.0
				if ( isset( $co ) && isset( $co['meta'] ) && isset( $co['meta']['coname'] ) ) {
					$coName = $co['meta']['coname'];
				}

				if ( empty( $coName ) && isset( $co['coname'] ) ) {
					$coName = $co['coname'];
				}
			}
		}

		#} Output

		if ( get_option( 'permalink_structure' ) == '' ) {
			echo "<div class='ui message red'>" . esc_html__( 'You are using Plain Permalinks. Please set your permalinks to %postname% by visiting ', 'zero-bs-crm' ) . "<a href='" . esc_url( admin_url( 'options-permalink.php' ) ) . "'>" . esc_html__( 'here', 'zero-bs-crm' ) . '</a></div>';
		} else {
			echo zeroBSCRM_CompanyTypeList( 'zbscrmjs_customer_setCompany', $coName, true, 'zbscrmjs_customer_changedCompany' );
		}
		#} Hidden input (real input) & Callback func
		?>
		<input type="hidden" name="zbs_company" id="zbs_company" value="<?php echo esc_attr( $coID ); ?>" />
		<script type="text/javascript">

		// custom fuction to copy company details from typeahead company deets
		function zbscrmjs_customer_setCompany(obj){

			if (typeof obj.id != "undefined"){

				// set vals
				jQuery("#zbs_company").val(obj.id);

				// set dirty
				zbscrm_JS_addDirty('zbs-company');

			} 

		}

		// custom fuction to copy company details from typeahead company deets
		// this one fires on any change, but here we're just using to catch empties :)
		// in fact, this one now overrides above^ 
		function zbscrmjs_customer_changedCompany(newval){

			if (typeof newval == "undefined" || newval == ''){

				// set vals
				jQuery("#zbs_company").val('');

				// set dirty
				zbscrm_JS_addDirty('zbs-company');

			}

		}

		</script>

	
		</div>
		<?php

		// PerfTest: zeroBSCRM_performanceTest_finishTimer('companydropdown');
	}

	public function save_data( $contact_id, $contact ) {

		return $contact;
	}
}

/*
======================================================
	/ "Contacts at Company" Metabox Related Funcs
	====================================================== */

/*
======================================================
	Contact Activity Metabox
	====================================================== */
class zeroBS__Metabox_Contact_Activity extends zeroBS__Metabox {

	public function __construct( $plugin_file ) {

		$this->metaboxID       = 'zbs-contact-activity-metabox';
		$this->metaboxTitle    = __( 'Activity', 'zero-bs-crm' );
		$this->metaboxIcon     = 'heartbeat';
		$this->metaboxScreen   = 'zbs-view-contact'; // we can use anything here as is now using our func
		$this->metaboxArea     = 'side';
		$this->metaboxLocation = 'high';

		// call this
		$this->initMetabox();
	}

	public function html( $obj, $metabox ) {

			global $zbs, $zeroBSCRM_logTypes;

			$objid = -1;
		if ( is_array( $obj ) && isset( $obj['id'] ) ) {
			$objid = $obj['id'];
		}

			// output any pinned logs
			$pinned_logs = $zbs->DAL->logs->getLogsForObj(
				array(

					'objtype'     => ZBS_TYPE_CONTACT,
					'objid'       => $objid,
					'only_pinned' => true,
					'incMeta'     => true,
					'sortByField' => 'zbsl_created',
					'sortOrder'   => 'DESC',
					'page'        => -1,
					'perPage'     => -1,
					'ignoreowner' => zeroBSCRM_DAL2_ignoreOwnership( ZBS_TYPE_CONTACT ),

				)
			);

		if ( is_array( $pinned_logs ) && count( $pinned_logs ) > 0 ) {

			$pinned_log_count = 0;

			echo '<h4 style="text-align:right" class="ui top attached header" id="jpcrm-pinned-logs-header">' . esc_attr__( 'Pinned Logs', 'zero-bs-crm' ) . '</h4>';
			echo '<div class="jpcrm-pinned-logs ui green attached segment">';
			foreach ( $pinned_logs as $log ) {

				if ( is_array( $log ) && isset( $log['created'] ) ) {

					if ( $pinned_log_count > 0 ) {

						?>
							<div class="ui divider"></div>
							<?php

					}

					echo '<div class="jpcrm-pinned-log">';

						// ico?
						$ico = '';
					$logKey  = strtolower( str_replace( ' ', '_', str_replace( ':', '_', $log['type'] ) ) );
					if ( isset( $zeroBSCRM_logTypes['zerobs_customer'][ $logKey ] ) ) {
						$ico = $zeroBSCRM_logTypes['zerobs_customer'][ $logKey ]['ico'];
					}
						// these are FA ico's at this point

						// compile this first, so can catch default (empty types)
						$logTitle = '';
					if ( ! empty( $ico ) ) {
						$logTitle .= '<i class="fa ' . $ico . '"></i> ';
					}
						// DAL 2 saves type as permalinked
					if ( isset( $zeroBSCRM_logTypes['zerobs_customer'][ $logKey ] ) ) {
						$logTitle .= __( $zeroBSCRM_logTypes['zerobs_customer'][ $logKey ]['label'], 'zero-bs-crm' );
					}

					?>

						<?php
						// short desc
						if ( isset( $log['shortdesc'] ) && ! empty( $log['shortdesc'] ) ) {
							?>
							<h4 class="jpcrm-pinned-log-shortdesc">
								<?php echo $logTitle . ' | ' . esc_html( $log['shortdesc'] ); ?>
							</h4>
							<?php } ?>

							<?php
							// long desc
							if ( isset( $log['longdesc'] ) && ! empty( $log['longdesc'] ) ) {
								?>
							<div class="jpcrm-pinned-log-longdesc">
								<?php echo wp_kses( html_entity_decode( $log['longdesc'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ), $zbs->acceptable_restricted_html ); ?>
							</div>
							<?php } ?>

							<div class="jpcrm-pinned-log-author-date-meta">
							<?php
							$meta_string = '';
							if ( ! empty( $log['author'] ) ) {
								$meta_string = $log['author'];
							}
							if ( ! empty( $log['createduts'] ) && $log['createduts'] > 0 ) {
								if ( ! empty( $meta_string ) ) {
									$meta_string .= ' &mdash; ';
								}
								$meta_string .= jpcrm_uts_to_date_str( $log['createduts'] );
							}

							echo esc_html( $meta_string );

							?>
							</div>

						</div>
						<?php

						++$pinned_log_count;

				}
			}

			?>
				</div>
				<?php

		}

			// normal activity output
			echo '<div class="zbs-activity">';
				echo '<div class="">';
					$zbsCustomerActivity = zeroBSCRM_getContactLogs( $objid, true, 100, 0, '', false );
					zeroBSCRM_html_contactTimeline( $objid, $zbsCustomerActivity, $obj );
				echo '</div>';
			echo '</div>';
	}

	// nothing to save here.
	public function save_data( $objID, $obj ) {
		return $obj;
	}
}

/*
======================================================
	/ Contact Activity Metabox
	====================================================== */
