/home/edulekha/crm.edulekha.com/modules/appointly/helpers/appointly_helper.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
/**
* Generate user-friendly appointment URL
*
* @param string $hash The appointment hash
* @return string The user-friendly URL
*/
if (!function_exists('appointly_get_appointment_url')) {
function appointly_get_appointment_url($hash)
{
return site_url('appointly/appointment/' . $hash);
}
}
/**
* Fetches from database all staff assigned customers
* If admin fetches all customers.
*
* @return array
*/
if (!function_exists('appointly_get_staff_customers')) {
function appointly_get_staff_customers()
{
$CI = &get_instance();
$staffCanViewAllClients = staff_can('view', 'customers');
$CI->db->select(
'firstname, lastname, ' . db_prefix() . 'contacts.id as contact_id, ' . get_sql_select_client_company()
);
$CI->db->where(db_prefix() . 'clients.active', '1');
$CI->db->join(db_prefix() . 'clients', db_prefix() . 'clients.userid=' . db_prefix() . 'contacts.userid', 'left');
$CI->db->select(db_prefix() . 'clients.userid as client_id');
if (! $staffCanViewAllClients) {
$CI->db->where(
'(' . db_prefix() . 'clients.userid IN (SELECT customer_id FROM ' . db_prefix() . 'customer_admins WHERE staff_id=' .
get_staff_user_id() . '))'
);
}
$result = $CI->db->get(db_prefix() . 'contacts')->result_array();
foreach ($result as &$contact) {
if ($contact['company'] == $contact['firstname'] . ' ' . $contact['lastname']) {
$contact['company'] = _l('appointments_individual_contact');
} else {
$contact['company'] = "" . _l('appointments_company_for_select') . "(" . $contact['company'] . ")";
}
}
if ($CI->db->affected_rows() !== 0) {
return $result;
} else {
return [];
}
}
}
/**
* Fetch current appointment data.
*
* @param [string] $appointment_id
*
* @return array
*/
if (!function_exists('fetch_appointment_data')) {
function fetch_appointment_data($id)
{
$CI = &get_instance();
$CI->db->select('*');
$CI->db->from(db_prefix() . 'appointly_appointments');
$CI->db->where('id', $id);
$appointment = $CI->db->get()->row_array();
// Get service details
if (isset($appointment['service_id']) && $appointment['service_id']) {
$CI->db->select('id, duration, name as service_name, color');
$CI->db->from(db_prefix() . 'appointly_services');
$CI->db->where('id', $appointment['service_id']);
$service = $CI->db->get()->row_array();
if ($service) {
// Merge service details into appointment
$appointment = array_merge($appointment, [
'service_duration' => $service['duration'],
'service_name' => $service['service_name'],
'service_color' => $service['color']
]);
}
}
// Get provider details
if (isset($appointment['provider_id']) && $appointment['provider_id']) {
$CI->db->select('firstname, lastname, staffid');
$CI->db->from(db_prefix() . 'staff');
$CI->db->where('staffid', $appointment['provider_id']);
$provider = $CI->db->get()->row_array();
if ($provider) {
// Merge provider details into appointment
$appointment = array_merge($appointment, [
'provider_firstname' => $provider['firstname'],
'provider_lastname' => $provider['lastname']
]);
}
}
// Get attendees as an array of staff IDs
$CI->db->select('staff_id');
$CI->db->from(db_prefix() . 'appointly_attendees');
$CI->db->where('appointment_id', $id);
$attendees_result = $CI->db->get()->result_array();
$attendees_ids = [];
foreach ($attendees_result as $attendee) {
$attendees_ids[] = $attendee['staff_id'];
}
$appointment['attendees'] = $attendees_ids;
return $appointment;
}
}
/**
* Convert dates for database insertion
*
* @param string $date
*
* @return array
*/
if (!function_exists('convertDateForDatabase')) {
function convertDateForDatabase($date)
{
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
// It's just a date without time - DON'T default to 00:00
return [
'date' => $date,
'start_hour' => '', // Let the calling function handle time
];
}
// Handle date with potential time component
try {
$datetime = new DateTime($date);
return [
'date' => $datetime->format('Y-m-d'),
'start_hour' => $datetime->format('H:i'),
];
} catch (Exception $e) {
// Fallback if date parsing fails
$parsed_date = to_sql_date($date, true);
return [
'date' => date('Y-m-d', strtotime($parsed_date)),
'start_hour' => date('H:i', strtotime($parsed_date)),
];
}
}
}
/**
* Convert dates for database insertion
*
* @param string $date
*
* @param $time
*
* @return array
*/
if (!function_exists('convertDateForValidation')) {
function convertDateForValidation($date, $time)
{
$date = to_sql_date($date, true);
$dt = 'H:i';
if ($time == '12') {
$dt = 'g:i A';
}
$toTime = strtotime($date);
return [
'date' => date('Y-m-d', $toTime),
'start_hour' => date($dt, $toTime),
];
}
}
/**
* Send email and push notifications for newly created recurring appointment
*
* @param string $appointment_id
*
* @return void
*/
if (!function_exists('newRecurringAppointmentNotifications')) {
function newRecurringAppointmentNotifications($appointment_id)
{
$CI = &get_instance();
$CI->load->model('appointly/appointly_model', 'apm');
// Get full appointment data with all necessary fields for merge field replacement
$appointment = $CI->apm->get_appointment_data($appointment_id);
if (!$appointment) {
return;
}
$notified_users = [];
$attendees = $appointment['attendees'];
foreach ($attendees as $staff) {
if ($staff['staffid'] === get_staff_user_id()) {
continue;
}
add_notification([
'description' => 'appointment_recurring_re_created',
'touserid' => $staff['staffid'],
'fromcompany' => true,
'link' => 'appointly/appointments/view?appointment_id=' . $appointment_id,
]);
$notified_users[] = $staff['staffid'];
send_mail_template(
'appointly_appointment_recurring_recreated_to_staff',
'appointly',
array_to_object($appointment),
array_to_object($staff)
);
}
pusher_trigger_notification(array_unique($notified_users));
$template = mail_template(
'appointly_appointment_recurring_recreated_to_contacts',
'appointly',
array_to_object($appointment)
);
@$template->send();
}
}
/**
* Helper function to handle reminder fields
*
* @param array $data
*
* @return array
*/
if (!function_exists('handleDataReminderFields')) {
function handleDataReminderFields($data)
{
(isset($data['by_email']) && $data['by_email'] == 'on')
? $data['by_email'] = '1'
: $data['by_email'] = null;
(isset($data['by_sms']) && $data['by_sms'] == 'on')
? $data['by_sms'] = '1'
: $data['by_sms'] = null;
if ($data['by_email'] === null && $data['by_sms'] === null) {
$data['reminder_before'] = null;
$data['reminder_before_type'] = null;
}
if (isset($data['by_email']) || isset($data['by_sms'])) {
if ($data['reminder_before'] == '') {
$data['reminder_before'] = '30';
}
}
return $data;
}
}
/**
* Helper redirect function with alert message.
*
* @param [string] $type 'success' | 'danger'
* @param [string] $message
*/
if (!function_exists('redirect_after_event')) {
function appointly_redirect_after_event($type, $message, $path = null)
{
$CI = &get_instance();
$CI->session->set_flashdata('message-' . $type, $message);
if ($path) {
redirect('admin/appointly/' . $path);
} else {
redirect('admin/appointly/appointments');
}
}
}
/**
* Helper function to get contact specific data.
*
* @param [string] $contact_id
*
* @return array
*/
if (!function_exists('get_appointment_contact_details')) {
function get_appointment_contact_details($contact_id)
{
if (!$contact_id) {
return null;
}
$CI = &get_instance();
$CI->db->select('email, userid, phonenumber as phone, CONCAT(firstname, " " , lastname) AS full_name');
$CI->db->where('id', $contact_id);
$contact = $CI->db->get(db_prefix() . 'contacts')->row_array();
if (!$contact) {
return null;
}
$contact['company_name'] = get_company_name($contact['userid']);
return $contact;
}
}
/**
* Get staff.
*
* @param [string] $staffid
*
* @return array
*/
if (!function_exists('appointly_get_staff')) {
function appointly_get_staff($staffid)
{
$CI = &get_instance();
$CI->db->where('staffid', $staffid);
return $CI->db->get(db_prefix() . 'staff')->row_array();
}
}
/**
* Include appointment view
*
* @param $path
* @param $name
*
* @return mixed
*/
if (!function_exists('include_appointment_view')) {
function include_appointment_view($path, $name, $variables = [])
{
// Extract variables to make them available in the included file
if (!empty($variables)) {
extract($variables);
}
return require 'modules/appointly/views/' . $path . '/' . $name . '.php';
}
}
/**
* Get projects summary
*
* @return array
*/
if (! function_exists('get_appointments_summary')) {
function get_appointments_summary($googleSync = null)
{
$CI = &get_instance();
$CI->load->database();
// Check permissions first
if (!staff_can('view', 'appointments')) {
return []; // No permissions = no appointments
}
// Apply permission-based filtering properly
$CI->db->select('*');
$CI->db->from(db_prefix() . 'appointly_appointments');
// Apply filtering for non-admin staff
if (!is_admin()) {
$CI->db->where('(created_by=' . get_staff_user_id()
. ' OR provider_id=' . get_staff_user_id()
. ' OR id IN (SELECT appointment_id FROM ' . db_prefix() . 'appointly_attendees WHERE staff_id=' . get_staff_user_id() . '))');
}
$appointments = $CI->db->get()->result_array();
// Ensure we have a valid array result
if (!is_array($appointments)) {
$appointments = [];
}
// Initialize data with zeros
$data = [
'total_appointments' => 0,
'upcoming' => [
'total' => 0,
'name' => _l('appointment_upcoming'),
'color' => '#0284c7', // Blue
],
'not_approved' => [
'total' => 0,
'name' => _l('appointment_not_approved'),
'color' => '#eab308', // Yellow
],
'cancelled' => [
'total' => 0,
'name' => _l('appointment_cancelled'),
'color' => '#ef4444', // Red
],
'no_show' => [
'total' => 0,
'name' => _l('appointment_no_show'),
'color' => '#f97316', // Orange
],
'finished' => [
'total' => 0,
'name' => _l('appointment_finished'),
'color' => '#22c55e', // Green
],
];
// Only process if we actually have appointments and the result is a valid array
if (is_array($appointments) && count($appointments) > 0) {
$data['total_appointments'] = count($appointments);
foreach ($appointments as $appointment) {
// Using the status ENUM field instead of individual boolean fields
if ($appointment['status'] == 'cancelled') {
$data['cancelled']['total']++;
} elseif ($appointment['status'] == 'no-show') {
$data['no_show']['total']++;
} elseif ($appointment['status'] == 'pending') {
$data['not_approved']['total']++;
} elseif ($appointment['status'] == 'completed') {
$data['finished']['total']++;
} elseif ($appointment['status'] == 'in-progress') {
// Check if the appointment is in the future (upcoming) or in the past (should be no-show)
if (strtotime($appointment['date'] . ' ' . $appointment['start_hour']) < time()) {
// If it's in the past, increment no_show
$data['no_show']['total']++;
} else {
// If it's in the future, increment upcoming
$data['upcoming']['total']++;
}
}
}
}
// Only add Google sync count if it's a valid positive number
if ($googleSync && is_numeric($googleSync) && $googleSync > 0) {
$data['total_appointments'] += (int)$googleSync;
}
return $data;
}
}
if (!function_exists('get_appointly_staff_userrole')) {
function get_appointly_staff_userrole($role_id)
{
$CI = &get_instance();
$CI->db->select('name');
$CI->db->where('roleid', $role_id);
$result = $CI->db->get(db_prefix() . 'roles')->row_array();
if ($result !== null) {
return $result['name'];
}
return null;
}
}
/**
*
* Get contact user id from contacts table
* Used for when creating new task in appointments.
*
* @param $contact_id
*
* @return mixed
*/
if (!function_exists('appointly_get_contact_customer_id')) {
function appointly_get_contact_customer_id($contact_id)
{
$CI = &get_instance();
$CI->db->select('userid');
$CI->db->where('id', $contact_id);
$result = $CI->db->get(db_prefix() . 'contacts')->row_array();
if ($result !== null) {
return $result['userid'];
}
return null;
}
}
/**
* Get table filters
*
* @return array
*/
if (!function_exists('get_appointments_table_filters')) {
function get_appointments_table_filters()
{
return [
[
'id' => 'all',
'status' => 'All',
],
[
'id' => 'pending',
'status' => _l('appointment_status_pending'),
],
[
'id' => 'in-progress',
'status' => _l('appointment_status_in-progress'),
],
[
'id' => 'cancelled',
'status' => _l('appointment_cancelled'),
],
[
'id' => 'completed',
'status' => _l('appointment_completed'),
],
[
'id' => 'no-show',
'status' => _l('appointment_status_no-show'),
],
[
'id' => 'today',
'status' => _l('appointment_today'),
],
[
'id' => 'tomorrow',
'status' => _l('appointment_tomorrow'),
],
[
'id' => 'this_week',
'status' => _l('appointment_this_week'),
],
[
'id' => 'next_week',
'status' => _l('appointment_next_week'),
],
[
'id' => 'this_month',
'status' => _l('appointment_this_month'),
],
[
'id' => 'upcoming',
'status' => _l('appointment_upcoming'),
],
// Assignment-based filters
[
'id' => 'my_appointments',
'status' => _l('appointment_my_appointments'),
],
[
'id' => 'assigned_to_me',
'status' => _l('appointment_assigned_to_me'),
],
// Source-based filters
[
'id' => 'internal',
'status' => _l('appointment_internal'),
],
[
'id' => 'external',
'status' => _l('appointment_external'),
],
[
'id' => 'recurring',
'status' => _l('appointment_recurring'),
],
[
'id' => 'lead_related',
'status' => _l('appointment_lead_related'),
],
[
'id' => 'internal_staff',
'status' => _l('appointment_internal_staff'),
],
];
}
}
/**
* Get staff or contact email.
*
* @param $id
* @param string $type
*
* @return mixed
*/
if (!function_exists('appointly_get_user_email')) {
function appointly_get_user_email($id, $type = 'staff')
{
$CI = &get_instance();
$CI->db->select('email');
$table = 'staff';
$selector = 'staffid';
if ($type == 'contact') {
$table = 'contacts';
$selector = 'id';
}
$CI->db->where($selector, $id);
$result = $CI->db->get(db_prefix() . $table)->row_array();
if ($result !== null) {
return $result['email'];
}
return null;
}
}
/**
* Insert new appointment to google calendar.
*
* @param array $data The appointment data
* @param array $attendees The attendees data
*
* @return array
*/
if (!function_exists('insertAppointmentToGoogleCalendar')) {
function insertAppointmentToGoogleCalendar($data, $attendees)
{
// First check if Google Calendar integration is enabled and authenticated
if (!appointlyGoogleAuth()) {
return [];
}
try {
// Validate required data fields
if (empty($data['date'])) {
log_message('error', 'Google Calendar: Missing required date field');
return [];
}
if (empty($data['subject'])) {
log_message('info', 'Google Calendar: Missing subject field, using default');
$data['subject'] = 'CRM Appointment';
}
// Format dates properly for Google Calendar API
if (isset($data['start_hour'])) {
// If start_hour is provided, use it with the date
// Check if the date already includes time (contains space)
if (strpos($data['date'], ' ') !== false) {
// Extract just the date part
$datePart = trim(explode(' ', $data['date'])[0]);
$dateStr = $datePart . ' ' . (strpos($data['start_hour'], ':') !== false ? $data['start_hour'] : $data['start_hour'] . ':00');
} else {
// Use date as is
$dateStr = $data['date'] . ' ' . (strpos($data['start_hour'], ':') !== false ? $data['start_hour'] : $data['start_hour'] . ':00');
}
$startDateTime = new DateTime($dateStr);
} else {
// Fall back to the original method
$startDateTime = new DateTime(to_sql_date($data['date'], true));
}
// Calculate end time based on duration
$endDateTime = clone $startDateTime;
$duration = 60; // Default 60 minutes
// For internal_staff meetings, use appointment duration
if (isset($data['source']) && $data['source'] == 'internal_staff' && isset($data['duration']) && $data['duration'] > 0) {
$duration = $data['duration'];
}
// For other appointments, try to get duration from service
else if (isset($data['service_id'])) {
$CI = &get_instance();
$CI->load->model('appointly/service_model');
$service = $CI->service_model->get($data['service_id']);
if ($service && isset($service->duration) && $service->duration > 0) {
$duration = $service->duration;
}
}
// Use end_hour if available instead of calculating
if (!empty($data['end_hour'])) {
try {
$endDateStr = $data['date'] . ' ' . (strpos($data['end_hour'], ':') !== false ? $data['end_hour'] : $data['end_hour'] . ':00');
$tempEndDateTime = new DateTime($endDateStr);
// Validate that the provided end time is after start time
if ($tempEndDateTime > $startDateTime) {
$endDateTime = $tempEndDateTime;
} else {
log_message('info', 'Google Calendar: Provided end_hour (' . $data['end_hour'] . ') is before start_hour (' . $data['start_hour'] . '), recalculating from duration');
// Fall back to duration-based calculation
$endDateTime->add(new DateInterval('PT' . $duration . 'M'));
}
} catch (Exception $e) {
// log_message('warning', 'Google Calendar: Failed to parse end_hour (' . $data['end_hour'] . '), using duration-based calculation: ' . $e->getMessage());
// If end_hour parsing fails, fall back to duration-based calculation
$endDateTime->add(new DateInterval('PT' . $duration . 'M'));
}
} else {
// Add duration minutes to end time
$endDateTime->add(new DateInterval('PT' . $duration . 'M'));
}
// Final validation that end time is after start time
if ($endDateTime <= $startDateTime) {
log_message('error', 'Google Calendar: End time (' . $endDateTime->format('Y-m-d H:i:s') . ') is still not after start time (' . $startDateTime->format('Y-m-d H:i:s') . ') even after recalculation');
return [];
}
$gmail_guests = [];
// Process attendees (staff)
if (!empty($attendees) && is_array($attendees)) {
foreach ($attendees as $attendee) {
$email = appointly_get_user_email($attendee);
if ($email) {
$gmail_guests[] = ['email' => $email];
}
}
}
// Add contact/client to attendees
if (!empty($data['contact_id']) && isset($data['source']) && $data['source'] != 'lead_related') {
$email = appointly_get_user_email($data['contact_id'], 'contact');
if ($email) {
$gmail_guests[] = ['email' => $email];
}
} else {
if (isset($data['email']) && filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$gmail_guests[] = ['email' => $data['email']];
}
}
// Clear any invalid emails
$gmail_guests = array_filter($gmail_guests, function ($v) {
return !empty($v['email']) && filter_var($v['email'], FILTER_VALIDATE_EMAIL);
});
// Get timezone from settings or appointment
$timezone = !empty($data['timezone'])
? $data['timezone']
: get_option('default_timezone');
// Prepare event data for Google Calendar
$params = [
'summary' => $data['subject'] ?? 'CRM Appointment',
'location' => $data['address'] ?? '',
'description' => $data['description'] ?? '',
'start' => [
'dateTime' => $startDateTime->format('Y-m-d\TH:i:s'),
'timeZone' => $timezone
],
'end' => [
'dateTime' => $endDateTime->format('Y-m-d\TH:i:s'),
'timeZone' => $timezone
],
'attendees' => $gmail_guests,
'reminders' => [
'useDefault' => false,
'overrides' => [
[
'method' => 'popup',
'minutes' => (int) get_option('appointly_google_meet_reminder_minutes') ?: 30
]
]
]
];
// Send request to Google Calendar API
$response = get_instance()->googlecalendar->addEvent($params);
if ($response) {
$result = [
'google_event_id' => $response->getId(),
'htmlLink' => $response->getHtmlLink(),
'google_added_by_id' => get_staff_user_id()
];
if ($response->getHangoutLink()) {
$result['hangoutLink'] = $response->getHangoutLink();
}
return $result;
}
return [];
} catch (Exception $e) {
log_message('error', 'Google Calendar Integration Error: ' . $e->getMessage());
return [];
}
}
}
/**
* @param $data
*
* @return array
* @throws \Exception
*/
if (!function_exists('updateAppointmentToGoogleCalendar')) {
/**
* @throws Exception
*/
function updateAppointmentToGoogleCalendar($data)
{
// First check if Google Calendar integration is enabled and authenticated
if (!appointlyGoogleAuth()) {
return [];
}
try {
// Validate required parameters
if (empty($data['google_event_id'])) {
return [];
}
// Debug log to troubleshoot time issues
// Format dates properly for Google Calendar API
if (isset($data['start_hour'])) {
// If start_hour is provided, use it with the date
// Check if the date already includes time (contains space)
if (strpos($data['date'], ' ') !== false) {
// Extract just the date part
$datePart = trim(explode(' ', $data['date'])[0]);
$dateStr = $datePart . ' ' . (strpos($data['start_hour'], ':') !== false ? $data['start_hour'] : $data['start_hour'] . ':00');
} else {
// Use date as is
$dateStr = $data['date'] . ' ' . (strpos($data['start_hour'], ':') !== false ? $data['start_hour'] : $data['start_hour'] . ':00');
}
$startDateTime = new DateTime($dateStr);
} else {
// Fall back to the original method
$startDateTime = new DateTime(to_sql_date($data['date'], true));
}
// Calculate end time based on duration
$endDateTime = clone $startDateTime;
$duration = 60; // Default 60 minutes
// For internal_staff meetings, use appointment duration
if (isset($data['source']) && $data['source'] == 'internal_staff' && isset($data['duration']) && $data['duration'] > 0) {
$duration = $data['duration'];
}
// For other appointments, try to get duration from service
else if (isset($data['service_id'])) {
$CI = &get_instance();
$CI->load->model('appointly/service_model');
$service = $CI->service_model->get($data['service_id']);
if ($service && isset($service->duration) && $service->duration > 0) {
$duration = $service->duration;
}
}
// Use end_hour if available instead of calculating
if (isset($data['end_hour']) && !empty($data['end_hour'])) {
try {
$endDateStr = $data['date'] . ' ' . (strpos($data['end_hour'], ':') !== false ? $data['end_hour'] : $data['end_hour'] . ':00');
$endDateTime = new DateTime($endDateStr);
} catch (Exception $e) {
// If end_hour parsing fails, fall back to duration-based calculation
$endDateTime->add(new DateInterval('PT' . $duration . 'M'));
}
} else {
// Add duration minutes to end time
$endDateTime->add(new DateInterval('PT' . $duration . 'M'));
}
// Validate that end time is after start time
if ($endDateTime <= $startDateTime) {
return [];
}
// Log the final times
// Process attendees
$gmail_guests = [];
if (isset($data['attendees'])) {
$gmail_attendees = $data['attendees'];
foreach ($gmail_attendees as $attendee) {
$email = appointly_get_user_email($attendee);
if ($email) {
$gmail_guests[] = ['email' => $email];
}
}
}
// Add contact/client to attendees
if (!empty($data['contact_id']) && isset($data['source']) && $data['source'] != 'lead_related') {
$email = appointly_get_user_email($data['contact_id'], 'contact');
if ($email) {
$gmail_guests[] = ['email' => $email];
}
} elseif (isset($data['selected_contact']) && isset($data['source']) && $data['source'] != 'lead_related') {
$email = appointly_get_user_email($data['selected_contact'], 'contact');
if ($email) {
$gmail_guests[] = ['email' => $email];
}
} elseif (isset($data['source']) && $data['source'] != 'lead_related') {
if (isset($data['email']) && filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
$gmail_guests[] = ['email' => $data['email']];
}
}
// Clear empty emails
$gmail_guests = array_filter($gmail_guests, function ($v) {
return !empty($v['email']) && filter_var($v['email'], FILTER_VALIDATE_EMAIL);
});
// Get timezone from settings or appointment
$timezone = isset($data['timezone']) && !empty($data['timezone'])
? $data['timezone']
: get_option('default_timezone');
// Prepare event data for Google Calendar
$params = [
'summary' => $data['subject'],
'location' => $data['address'] ?? '',
'description' => $data['description'] ?? '',
'start' => [
'dateTime' => $startDateTime->format('Y-m-d\TH:i:s'),
'timeZone' => $timezone
],
'end' => [
'dateTime' => $endDateTime->format('Y-m-d\TH:i:s'),
'timeZone' => $timezone
],
'attendees' => $gmail_guests
];
// Send request to Google Calendar API
$response = get_instance()->googlecalendar->updateEvent($data['google_event_id'], $params);
if ($response) {
$return_data = [];
if (isset($response['hangoutLink'])) {
$return_data['google_meet_link'] = $response['hangoutLink'];
}
$return_data['google_event_id'] = $response['id'];
$return_data['htmlLink'] = $response['htmlLink'];
return $return_data;
} else {
return [];
}
} catch (Exception $e) {
return [];
}
}
}
/**
* @param $aRow
* @return bool
*/
if (! function_exists('isSynceGoogleMeeting')) {
function isSynceGoogleMeeting($aRow): bool
{
return $aRow['source'] !== 'google';
}
}
/**
* Check if user is authenticated with Google calendar
* Refresh access token.
*
* @return bool
*/
/**
* Check if user is authenticated with Google calendar
* Refresh access token.
*
* @return bool
*/
if (! function_exists('appointlyGoogleAuth')) {
function appointlyGoogleAuth()
{
$CI = &get_instance();
$CI->load->model('appointly/googlecalendar');
$account = $CI->googlecalendar->getAccountDetails();
if (! $account) return false;
$newToken = '';
if ($account) {
$account = $account[0];
$currentToken = [
'access_token' => $account->access_token,
'expires_in' => $account->expires_in
];
$CI->googleplus
->client
->setAccessToken($currentToken);
$refreshToken = $account->refresh_token;
// renew 5 minutes before token expire
if ($account->expires_in <= time() + 300) {
if ($CI->googleplus->isAccessTokenExpired()) {
$CI->googleplus
->client
->setAccessToken($currentToken);
}
if ($refreshToken) {
// { "error": "invalid_grant", "error_description": "Token has been expired or revoked." }
try {
$newToken = $CI->googleplus->client->refreshToken($refreshToken);
} catch (Exception $e) {
if ($e->getCode() === 400) {
return false;
}
if ($e->getCode() === 401) {
return false;
}
}
$CI->googleplus
->client
->setAccessToken($newToken);
if ($newToken) {
$CI->googlecalendar->saveNewTokenValues($newToken);
}
}
} else {
try {
$newToken = $CI->googleplus->client->refreshToken($refreshToken);
} catch (Exception $e) {
if ($e->getCode() === 400) {
return false;
}
if ($e->getCode() === 401) {
return false;
}
}
}
$CI->googleplus
->client
->setAccessToken(($newToken !== '') ? $newToken : $account->access_token);
}
if ($CI->googleplus->client->getAccessToken()) {
return $CI->googleplus->client->getAccessToken();
}
return false;
}
}
/**
* @return array
*/
if (!function_exists('getAppointlyUserMeta')) {
function getAppointlyUserMeta($data = [])
{
$data['appointly_show_summary'] = get_meta('staff', get_staff_user_id(), 'appointly_show_summary');
$data['appointly_default_table_filter'] = get_meta(
'staff',
get_staff_user_id(),
'appointly_default_table_filter'
);
return $data;
}
}
/**
* Handle appointly user meta
*
* @param $meta
*
* @return void
*/
if (!function_exists('handleAppointlyUserMeta')) {
function handleAppointlyUserMeta($meta)
{
foreach ($meta as $key => $value) {
update_meta('staff', get_staff_user_id(), $key, $value);
}
}
}
/**
* Get appointment default feedbacks.
*
* @return array
*/
if (!function_exists('getAppointmentsFeedbacks')) {
function getAppointmentsFeedbacks()
{
return [
['value' => '0', 'name' => _l('ap_feedback_not_sure')],
['value' => '1', 'name' => _l('ap_feedback_the_worst')],
['value' => '2', 'name' => _l('ap_feedback_bad')],
['value' => '3', 'name' => _l('ap_feedback_not_bad')],
['value' => '4', 'name' => _l('ap_feedback_good')],
['value' => '5', 'name' => _l('ap_feedback_very_good')],
['value' => '6', 'name' => _l('ap_feedback_extremely_good')],
];
}
}
/**
* Renders appointment feedbacks html
*
* @param $appointment
* @param bool $fallback
*
* @return string
*/
if (!function_exists('renderAppointmentFeedbacks')) {
function renderAppointmentFeedbacks($appointment, $fallback = false)
{
$appointmentFeedbacks = getAppointmentsFeedbacks();
if ($fallback && is_string($appointment)) {
$CI = &get_instance();
$appointment = $CI->apm->get_appointment_data($appointment);
}
$html = '<div class="col-lg-12 col-xs-12 mtop20 text-center" id="feedback_wrapper">';
$html .= '<span class="label label-default" style="line-height: 30px;">' . _l(
'appointment_feedback_label'
) . '</span><br>';
if ($appointment['feedback'] !== null && ! is_staff_logged_in()) {
$html = '<span class="label label-primary" style="line-height: 30px;">' . _l(
'appointment_feedback_label_current'
) . '</span><br>';
}
if ($fallback) {
$html = '<span class="label label-success" style="line-height: 30px;">' . _l(
'appointment_feedback_label_added'
) . '</span><br>';
}
$savedFeedbacks = json_decode(get_option('appointly_default_feedbacks'));
$count = 0;
foreach ($appointmentFeedbacks as $feedback) {
if ($savedFeedbacks !== null) {
if (! in_array($feedback['value'], $savedFeedbacks)) {
continue;
}
}
$rating_class = '';
if ($appointment['feedback'] >= $feedback['value']) {
$rating_class = 'star_rated';
}
$onClick = '';
if (!is_staff_logged_in()) {
$onClick = 'onclick="handle_appointment_feedback(this)"';
}
$html .= '<span ' . $onClick . ' data-count="' . $count++ . '"
data-rating="' . $feedback['value'] . '" data-toggle="tooltip"
title="' . $feedback['name'] . '" class="feedback_star text-center ' . $rating_class . '"><i
class="fa fa-star" aria-hidden="true"></i></span>';
}
if (! is_bool($appointment['feedback_comment'])) {
if ($appointment['feedback_comment'] !== null) {
$html .= '<div class="col-md-12 text-center mtop5" id="feedback_comment_area">';
$html .= '<h6>' . $appointment['feedback_comment'] . '</h6>';
$html .= '</div>';
$html .= '<div class="clearfix"></div>';
}
}
if ($appointment['feedback'] !== null && !is_staff_logged_in()) {
echo '<div>';
}
$html .= '</div>';
return $html;
}
}
/**
* Render appointments timezone.
*
* @param $appointment
*
* @return string
* @throws \Exception
*/
if (!function_exists('render_appointments_timezone')) {
/**
* @throws Exception
*/
function render_appointments_timezone($appointment)
{
$CI = &get_instance();
$CI->load->model('appointly/appointly_model', 'apm');
$timezone_info = $CI->apm->get_appointment_timezone_info($appointment);
return sprintf(
'<i data-toggle="tooltip" title="%s (GMT %s)" class="fa fa-globe timezone" aria-hidden="true"></i>',
$timezone_info['timezone'],
$timezone_info['offset']
);
}
}
if (!function_exists('get_service_name')) {
function get_service_name($service_id)
{
$CI = &get_instance();
$CI->load->model('appointly/service_model', 'sm');
return $CI->sm->get_service_name($service_id);
}
}
/**
* Check and render appointment status with HTML.
*
* @param array $aRow
* @param string $format 'text' or 'html'
*
* @return string
*/
if (!function_exists('checkAppointlyStatus')) {
function checkAppointlyStatus($aRow, $format = 'text')
{
// Check if appointment is in the past
$is_past = false;
if (!empty($aRow['date']) && !empty($aRow['start_hour'])) {
// Make sure we have valid date and time values
$date_str = trim($aRow['date']);
$time_str = trim($aRow['start_hour']);
// Format the datetime properly
if (strpos($date_str, ' ') !== false) {
// Date already contains time
$appointment_datetime = strtotime($date_str);
} else {
// Combine date and time
$appointment_datetime = strtotime($date_str . ' ' . $time_str);
}
// Only mark as past if we have a valid timestamp
if ($appointment_datetime !== false) {
$current_datetime = time();
$is_past = $appointment_datetime < $current_datetime;
}
}
// Prepare the status text and CSS class
$status_text = '';
$status_class = '';
switch ($aRow['status']) {
case 'pending':
$status_text = _l('appointment_pending_approval');
$status_class = 'warning'; // Yellow for pending
break;
case 'cancelled':
$status_text = _l('appointment_cancelled');
$status_class = 'danger'; // Red for cancelled
break;
case 'completed':
$status_text = _l('appointment_finished');
$status_class = 'success'; // Green for completed
break;
case 'finished':
$status_text = _l('appointment_finished');
$status_class = 'success'; // Green for finished
break;
case 'approved':
$status_text = _l('appointment_approved');
$status_class = 'success'; // Green for approved
break;
case 'no-show':
$status_text = _l('appointment_no_show');
$status_class = 'danger'; // Red for no-show
break;
case 'in-progress':
$status_text = _l('appointment_ongoing');
$status_class = 'info'; // Blue for in-progress
break;
default:
$status_text = _l('appointment_upcoming');
$status_class = 'info'; // Default blue
break;
}
// Return formatted output based on requested format
if ($format === 'html') {
return '<span class="label label-' . $status_class . '">' . $status_text . '</span>';
} else {
return $status_text;
}
}
}
/**
* Check if an appointment is upcoming (future in-progress appointment)
*
* @param array $aRow
* @return bool
*/
if (!function_exists('isAppointmentUpcoming')) {
function isAppointmentUpcoming($aRow)
{
// Only in-progress appointments can be "upcoming"
if ($aRow['status'] !== 'in-progress') {
return false;
}
// Check if appointment is in the future
if (!empty($aRow['date']) && !empty($aRow['start_hour'])) {
$date_str = trim($aRow['date']);
$time_str = trim($aRow['start_hour']);
// Format the datetime properly
if (strpos($date_str, ' ') !== false) {
$appointment_datetime = strtotime($date_str);
} else {
$appointment_datetime = strtotime($date_str . ' ' . $time_str);
}
if ($appointment_datetime !== false) {
return $appointment_datetime > time();
}
}
return false;
}
}
/**
* Get module version.
*
* @return string
*/
if (!function_exists('get_appointly_version')) {
function get_appointly_version()
{
get_instance()->db->where('module_name', 'appointly');
$version = get_instance()->db->get(db_prefix() . 'modules');
if ($version->num_rows() > 0) {
return _l('appointly_current_version') . $version->row('installed_version');
}
return _l('appointment_unknown');
}
}
/**
* @param $source
* @param string $externalLinks
* @param array $row
* @return array
*/
function checkAppointtlySource($source, string $externalLinks, array $row): array
{
if ($source == 'google_calendar') {
$row[] = '<div class="text-center tw-flex tw-flex-wrap tw-items-center tw-justify-between">'
. _l('appointments_source_google_imported') .
$externalLinks . '</div>';
}
if ($source == 'outlook_imported') {
$row[] = '<div class="text-center tw-flex tw-flex-wrap tw-items-center tw-justify-between">'
. _l('appointments_source_outlook_imported') .
$externalLinks . '</div>';
}
if ($source == 'external') {
$row[] = '<div class="text-center tw-flex tw-flex-wrap tw-items-center tw-justify-between">'
. _l(
'appointments_source_external_label'
) .
$externalLinks . '</div>';
}
if ($source == 'internal') {
$row[] = '<div class="text-center tw-flex tw-flex-wrap tw-items-center tw-justify-between">'
. _l(
'appointments_source_internal_label'
) .
$externalLinks . '</div>';
}
if ($source == 'lead_related') {
$row[] = '<div class="text-center tw-flex tw-flex-wrap tw-items-center tw-justify-between">'
. _l(
'lead'
) . $externalLinks . '</div>';
}
return $row;
}
function has_appointly_permission($permission)
{
$CI = &get_instance();
if (is_admin()) {
return true;
}
if (!is_staff_member()) {
return false;
}
$staff_id = get_staff_user_id();
$CI->load->model('staff_model');
$permissions = $CI->staff_model->get_staff_permissions($staff_id);
foreach ($permissions as $perm) {
if ($perm['permission_name'] === $permission) {
return true;
}
}
return false;
}
if (!function_exists('createFilters')) {
function createFilters()
{
$CI = &get_instance();
$filters = [];
// Check if we're specifically filtering for Google Calendar synced events
$isGoogleCalendarFilter = !empty($CI->input->post('google_calendar_synced')) ||
($CI->input->post('custom_view') === 'google_calendar_synced');
// Check if we're on the "All" filter
$isAllFilter = true;
foreach (
[
'status_pending',
'status_in-progress',
'status_completed',
'status_cancelled',
'status_no-show',
'internal',
'external',
'lead_related',
'upcoming',
'no-show',
'recurring',
'internal_staff',
'google_calendar_synced',
'today',
'tomorrow',
'this_week',
'next_week',
'this_month',
'my_appointments',
'assigned_to_me'
] as $filter
) {
if (!empty($CI->input->post($filter))) {
$isAllFilter = false;
break;
}
}
if (!empty($CI->input->post('custom_view'))) {
$isAllFilter = false;
}
// If filtering specifically for Google Calendar events, only use that filter
if ($isGoogleCalendarFilter) {
$filters[] = 'AND (' . db_prefix() . 'appointly_appointments.source = "google_calendar")';
return $filters;
}
// For all other filters, use only the new status field (not legacy fields)
$filterConditions = [
// Status filters - using the ENUM status field
'status_pending' => 'AND (' . db_prefix() . 'appointly_appointments.status = "pending")',
'status_in-progress' => 'AND (' . db_prefix() . 'appointly_appointments.status = "in-progress")',
'status_completed' => 'AND (' . db_prefix() . 'appointly_appointments.status = "completed")',
'status_cancelled' => 'AND (' . db_prefix() . 'appointly_appointments.status = "cancelled")',
// Match summary logic: explicit no-show status OR past in-progress appointments
'status_no-show' => 'AND (' . db_prefix() . 'appointly_appointments.status = "no-show" OR (' . db_prefix() . 'appointly_appointments.status = "in-progress" AND CONCAT(' . db_prefix() . 'appointly_appointments.date, " ", ' . db_prefix() . 'appointly_appointments.start_hour) < NOW()))',
// Time-based filters - Added new time-based filters
'today' => 'AND ' . db_prefix() . 'appointly_appointments.date = CURDATE()',
'tomorrow' => 'AND ' . db_prefix() . 'appointly_appointments.date = DATE_ADD(CURDATE(), INTERVAL 1 DAY)',
'this_week' => 'AND WEEK(' . db_prefix() . 'appointly_appointments.date, 1) = WEEK(CURDATE(), 1) AND YEAR(' . db_prefix() . 'appointly_appointments.date) = YEAR(CURDATE())',
'next_week' => 'AND WEEK(' . db_prefix() . 'appointly_appointments.date, 1) = WEEK(DATE_ADD(CURDATE(), INTERVAL 1 WEEK), 1) AND YEAR(' . db_prefix() . 'appointly_appointments.date) = YEAR(DATE_ADD(CURDATE(), INTERVAL 1 WEEK))',
'this_month' => 'AND MONTH(' . db_prefix() . 'appointly_appointments.date) = MONTH(CURDATE()) AND YEAR(' . db_prefix() . 'appointly_appointments.date) = YEAR(CURDATE())',
'upcoming' => 'AND (' . db_prefix() . 'appointly_appointments.status = "in-progress" AND CONCAT(' . db_prefix() . 'appointly_appointments.date, " ", ' . db_prefix() . 'appointly_appointments.start_hour) > NOW())',
// Match summary logic: no-show status OR past in-progress appointments
'no-show' => 'AND (' . db_prefix() . 'appointly_appointments.status = "no-show" OR (' . db_prefix() . 'appointly_appointments.status = "in-progress" AND CONCAT(' . db_prefix() . 'appointly_appointments.date, " ", ' . db_prefix() . 'appointly_appointments.start_hour) < NOW()))',
// Assignment-based filters
'my_appointments' => 'AND ' . db_prefix() . 'appointly_appointments.created_by = ' . get_staff_user_id(),
'assigned_to_me' => 'AND ' . db_prefix() . 'appointly_appointments.provider_id = ' . get_staff_user_id() . ' OR ' . db_prefix() . 'appointly_appointments.id IN (SELECT appointment_id FROM ' . db_prefix() . 'appointly_attendees WHERE staff_id = ' . get_staff_user_id() . ')',
// Source-based filters
'internal' => 'AND (' . db_prefix() . 'appointly_appointments.source = "internal")',
'external' => 'AND (' . db_prefix() . 'appointly_appointments.source = "external")',
'lead_related' => 'AND (' . db_prefix() . 'appointly_appointments.source = "lead_related")',
'internal_staff' => 'AND (' . db_prefix() . 'appointly_appointments.source = "internal_staff")',
// Other filters
'recurring' => 'AND ' . db_prefix() . 'appointly_appointments.recurring = 1',
];
foreach ($filterConditions as $key => $condition) {
if ($CI->input->post($key)) {
$filters[] = $condition;
}
}
// Handle custom_view for all the new filters
$custom_view = $CI->input->post('custom_view');
if ($custom_view && isset($filterConditions[$custom_view])) {
$filters[] = $filterConditions[$custom_view];
}
// For non-Google filters (except ALL), exclude Google Calendar appointments
// Only exclude Google Calendar appointments when a specific filter is active (not "All")
if (!$isGoogleCalendarFilter && !$isAllFilter && get_option('appointments_googlesync_show_in_table')) {
$filters[] = 'AND ' . db_prefix() . 'appointly_appointments.source != "google_calendar"';
}
return $filters;
}
}
if (!function_exists('get_staff_role')) {
function get_staff_role($staff_id)
{
$CI = &get_instance();
// Get staff member's role ID
$CI->db->select('role');
$CI->db->where('staffid', $staff_id);
$staff = $CI->db->get(db_prefix() . 'staff')->row();
if (!$staff || !$staff->role) {
return false;
}
// Get role details
$CI->db->where('roleid', $staff->role);
$role = $CI->db->get(db_prefix() . 'roles')->row_array();
return $role ?: false;
}
}
if (!function_exists('get_timezones_list')) {
function get_timezones_list()
{
// Get timezones from the core
$timezones = \app\services\Timezones::get();
// Instead of returning the nested array structure, return a flat array
// with identifiers as keys and display names as values
$flat_timezones = [];
foreach ($timezones as $region => $list) {
if (is_array($list)) {
foreach ($list as $timezone) {
$flat_timezones[$timezone] = $timezone;
}
}
}
return $flat_timezones;
}
}
function getStaffProfileImage($staffid)
{
// Default placeholder URL - always return this if anything goes wrong
$placeholder = base_url('assets/images/user-placeholder.jpg');
$CI = &get_instance();
$CI->load->model('staff_model');
$staff = $CI->staff_model->get($staffid);
// If staff doesn't exist or has no profile image, return placeholder
if (!$staff || empty($staff->profile_image)) {
return $placeholder;
}
// Check if path already contains full URL
if (str_starts_with($staff->profile_image, 'http')) {
return $staff->profile_image;
}
// Construct the file path exactly like Perfex CRM core does
$profileImagePath = 'uploads/staff_profile_images/' . $staffid . '/thumb_' . $staff->profile_image;
// Check if the file actually exists on disk
if (file_exists($profileImagePath)) {
return base_url($profileImagePath);
}
// File doesn't exist, return placeholder to avoid 404 errors
return $placeholder;
}
/**
* Get base currency for the appointly module
* This avoids conflicts with the core function in sales_helper.php
* @return object
*/
function appointly_get_base_currency()
{
$CI = &get_instance();
if (!class_exists('currencies_model', false)) {
$CI->load->model('currencies_model');
}
return $CI->currencies_model->get_base_currency();
}
if (!function_exists('appointly_get_service_staff')) {
/**
* Get staff members assigned to a service
* Common helper used by public controllers
*
* @param int $service_id The service ID
* @return array Response with service details and assigned staff
*/
function appointly_get_service_staff($service_id)
{
$CI = &get_instance();
if (!$service_id) {
return [
'success' => false,
'message' => _l('service_id_required')
];
}
// Load required models
if (!class_exists('Service_model', false)) {
$CI->load->model('appointly/service_model');
}
if (!isset($CI->apm)) {
$CI->load->model('appointly/appointly_model', 'apm');
}
// Get service details
$service = $CI->service_model->get($service_id);
if (!$service) {
return [
'success' => false,
'message' => _l('service_not_found')
];
}
// Get staff members assigned to this service
$staff_members = [];
// Get staff IDs from the service_staff table
$CI->db->select('staff_id');
$CI->db->from(db_prefix() . 'appointly_service_staff');
$CI->db->where('service_id', $service_id);
$query = $CI->db->get();
$staff_ids = [];
if ($query && $query->num_rows() > 0) {
$staff_ids = array_column($query->result_array(), 'staff_id');
}
// Now fetch the staff data
if (!empty($staff_ids)) {
$CI->db->select('staffid, firstname, lastname, email, phonenumber, profile_image');
$CI->db->from(db_prefix() . 'staff');
$CI->db->where_in('staffid', $staff_ids);
$CI->db->where('active', 1);
$staff_members = $CI->db->get()->result_array();
// Process profile images
foreach ($staff_members as &$staff) {
$staff['profile_image'] = $staff['profile_image']
? staff_profile_image_url($staff['staffid'])
: base_url('assets/images/user-placeholder.jpg');
}
}
// Get working hours
$working_hours = [];
// Fall back to company schedule
$company_schedule = $CI->apm->get_company_schedule();
if (!empty($company_schedule)) {
foreach ($company_schedule as $day) {
// Check if weekday key exists to prevent PHP warnings
if (!isset($day['weekday'])) {
continue; // Skip this entry if weekday is not set
}
$day_number = getWorkingDayNumber($day['weekday']);
$working_hours[$day_number] = [
'enabled' => isset($day['is_enabled']) && (bool) $day['is_enabled'],
'start_time' => $day['start_time'] ?? '09:00',
'end_time' => $day['end_time'] ?? '17:00'
];
}
} else {
// Default working hours if nothing is configured
$working_hours = [
1 => ['enabled' => true, 'start_time' => '09:00', 'end_time' => '17:00'],
2 => ['enabled' => true, 'start_time' => '09:00', 'end_time' => '17:00'],
3 => ['enabled' => true, 'start_time' => '09:00', 'end_time' => '17:00'],
4 => ['enabled' => true, 'start_time' => '09:00', 'end_time' => '17:00'],
5 => ['enabled' => true, 'start_time' => '09:00', 'end_time' => '17:00'],
6 => ['enabled' => false, 'start_time' => '09:00', 'end_time' => '17:00'],
7 => ['enabled' => false, 'start_time' => '09:00', 'end_time' => '17:00']
];
}
// Format working hours for frontend
$formatted_hours = [];
foreach ($working_hours as $day_number => $hours) {
$day_name = '';
switch ($day_number) {
case 1:
$day_name = 'Monday';
break;
case 2:
$day_name = 'Tuesday';
break;
case 3:
$day_name = 'Wednesday';
break;
case 4:
$day_name = 'Thursday';
break;
case 5:
$day_name = 'Friday';
break;
case 6:
$day_name = 'Saturday';
break;
case 7:
$day_name = 'Sunday';
break;
}
if ($hours['enabled']) {
$formatted_hours[] = [
'day' => $day_name,
'day_key' => strtolower($day_name),
'start' => $hours['start_time'],
'end' => $hours['end_time']
];
} else {
$formatted_hours[] = [
'day' => $day_name,
'day_key' => strtolower($day_name),
'start' => null,
'end' => null
];
}
}
// Get busy times (already booked appointments)
$busy_times = $CI->apm->get_busy_times_by_service($service_id);
// Prepare and log the final result
$result = [
'success' => true,
'data' => [
'service' => $service,
'staff' => $staff_members,
'working_hours' => $working_hours,
'formatted_hours' => $formatted_hours,
'busy_times' => $busy_times
]
];
return $result;
}
}
if (!function_exists('appointly_get_staff_schedule')) {
/**
* Get staff working schedule
* Common helper used by both admin and public controllers
*
* @param int $staff_id The staff ID to get schedule for
* @return array Response with working hours
*/
function appointly_get_staff_schedule($staff_id)
{
$CI = &get_instance();
if (!$staff_id) {
return [
'success' => false,
'message' => _l('appointly_staff_id_required')
];
}
// Load appointly model if not loaded
if (!isset($CI->apm)) {
$CI->load->model('appointly/appointly_model', 'apm');
}
// Get staff working hours from the staff_working_hours table
$working_hours = $CI->apm->get_staff_working_hours($staff_id);
if (empty($working_hours)) {
// Fallback to company schedule
$working_hours = $CI->apm->get_company_schedule();
if (empty($working_hours)) {
return [
'success' => false,
'message' => _l('appointly_no_working_hours')
];
}
}
$formatted_hours = [];
$schedule = [];
// Format working hours for frontend
foreach ($working_hours as $day => $hours) {
$day_number = getWorkingDayNumber($day);
$schedule[$day_number] = [
'enabled' => $hours['is_available'] ?? $hours['is_enabled'] ?? false,
'start_time' => $hours['start_time'],
'end_time' => $hours['end_time']
];
if ($schedule[$day_number]['enabled']) {
$formatted_hours[] = [
'day' => _l('appointly_day_' . strtolower($day)),
'day_key' => strtolower($day),
'start' => date('H:i', strtotime($hours['start_time'])),
'end' => date('H:i', strtotime($hours['end_time']))
];
}
}
return [
'success' => true,
'data' => [
'schedule' => $schedule,
'formatted_hours' => $formatted_hours
]
];
}
}
/**
* Helper function to convert weekday name to day number
*
* @param string $day_name Weekday name
* @return int Day number (1-7)
*/
function getWorkingDayNumber($day_name)
{
// Handle null or empty day_name to prevent warnings
if (empty($day_name)) {
return 1; // Default to Monday if day_name is not provided
}
$days = [
'monday' => 1,
'tuesday' => 2,
'wednesday' => 3,
'thursday' => 4,
'friday' => 5,
'saturday' => 6,
'sunday' => 7
];
return $days[strtolower($day_name)] ?? 1;
}
/**
* Helper function to get service duration from services array
*
* @param int $service_id
* @param array $services
* @return int Duration in minutes
*/
function get_service_duration($service_id, $services)
{
if (!$service_id || !$services) {
return 60; // Default to 60 minutes
}
foreach ($services as $service) {
if ($service['id'] == $service_id) {
return $service['duration'];
}
}
return 60; // Default to 60 minutes if not found
}
/**
* Get blocked days for date picker
*
* Returns the blocked days in a format that can be used by the datepicker
*
* @return array Array of blocked days formatted for datepicker
*/
function get_appointly_blocked_days()
{
$blocked_days = get_option('appointly_blocked_days');
if (empty($blocked_days)) {
return [];
}
$blocked_dates = json_decode($blocked_days, true);
if (!is_array($blocked_dates)) {
return [];
}
// Format dates for the datepicker - Use YYYY-MM-DD format for consistency
$formatted_dates = [];
foreach ($blocked_dates as $date) {
// Make sure the date is properly formatted (Y-m-d)
if (!empty($date) && strtotime($date)) {
// Store in YYYY-MM-DD format for consistent date handling
$formatted_dates[] = date('Y-m-d', strtotime($date));
}
}
return $formatted_dates;
}
function generate_appointment_ics_content($appointment)
{
// Parse appointment date and time
$start_datetime = new DateTime($appointment['date'] . ' ' . $appointment['start_hour']);
$end_datetime = clone $start_datetime;
// Calculate end time from start_hour and end_hour or use duration
if (!empty($appointment['end_hour'])) {
$end_datetime = new DateTime($appointment['date'] . ' ' . $appointment['end_hour']);
} else {
// Add duration (default 1 hour if not specified)
$duration_minutes = !empty($appointment['duration']) ? (int)$appointment['duration'] : 60;
$end_datetime->add(new DateInterval('PT' . $duration_minutes . 'M'));
}
// Format dates for ICS (UTC format)
$start_datetime->setTimezone(new DateTimeZone('UTC'));
$end_datetime->setTimezone(new DateTimeZone('UTC'));
$dtstart = $start_datetime->format('Ymd\THis\Z');
$dtend = $end_datetime->format('Ymd\THis\Z');
$dtstamp = gmdate('Ymd\THis\Z');
// Generate unique ID
$uid = 'appointment-' . $appointment['id'] . '-' . time() . '@' . $_SERVER['HTTP_HOST'];
// Prepare description
$description = '';
if (!empty($appointment['description'])) {
$description = escape_ics_text($appointment['description']);
}
// Prepare location
$location = '';
if (!empty($appointment['location'])) {
$location = escape_ics_text($appointment['location']);
}
// Prepare organizer (provider)
$organizer = '';
if (!empty($appointment['provider_id'])) {
$CI = &get_instance();
$CI->load->model('staff_model');
$provider = $CI->staff_model->get($appointment['provider_id']);
if ($provider) {
$organizer = 'ORGANIZER;CN=' . escape_ics_text($provider->firstname . ' ' . $provider->lastname) . ':MAILTO:' . $provider->email;
}
}
// Prepare attendees
$attendees = '';
// Get staff attendees
$CI = &get_instance();
$CI->load->model('appointly/appointly_attendees_model', 'atm');
$staff_attendees = $CI->atm->get($appointment['id']);
foreach ($staff_attendees as $attendee) {
$attendees .= 'ATTENDEE;CN=' . escape_ics_text($attendee['firstname'] . ' ' . $attendee['lastname']) . ':MAILTO:' . $attendee['email'] . "\r\n";
}
// Add client/contact as attendee
if ($appointment['source'] === 'external') {
if (!empty($appointment['email']) && !empty($appointment['name'])) {
$attendees .= 'ATTENDEE;CN=' . escape_ics_text($appointment['name']) . ':MAILTO:' . $appointment['email'] . "\r\n";
}
} elseif ($appointment['source'] === 'internal' && !empty($appointment['contact_id'])) {
// Get contact details
$CI->load->model('clients_model');
$contact = $CI->clients_model->get_contact($appointment['contact_id']);
if ($contact) {
$attendees .= 'ATTENDEE;CN=' . escape_ics_text($contact->firstname . ' ' . $contact->lastname) . ':MAILTO:' . $contact->email . "\r\n";
}
}
// Build ICS content
$ics = "BEGIN:VCALENDAR\r\n";
$ics .= "VERSION:2.0\r\n";
$ics .= "PRODID:-//Appointly//Appointment Calendar//EN\r\n";
$ics .= "CALSCALE:GREGORIAN\r\n";
$ics .= "METHOD:PUBLISH\r\n";
$ics .= "BEGIN:VEVENT\r\n";
$ics .= "UID:" . $uid . "\r\n";
$ics .= "DTSTAMP:" . $dtstamp . "\r\n";
$ics .= "DTSTART:" . $dtstart . "\r\n";
$ics .= "DTEND:" . $dtend . "\r\n";
$ics .= "SUMMARY:" . escape_ics_text($appointment['subject']) . "\r\n";
if ($description) {
$ics .= "DESCRIPTION:" . $description . "\r\n";
}
if ($location) {
$ics .= "LOCATION:" . $location . "\r\n";
}
if ($organizer) {
$ics .= $organizer . "\r\n";
}
if ($attendees) {
$ics .= $attendees;
}
// Add reminder (using system setting)
$reminderMinutes = (int) get_option('appointly_google_meet_reminder_minutes') ?: 30;
$ics .= "BEGIN:VALARM\r\n";
$ics .= "TRIGGER:-PT{$reminderMinutes}M\r\n";
$ics .= "ACTION:DISPLAY\r\n";
$ics .= "DESCRIPTION:Appointment Reminder\r\n";
$ics .= "END:VALARM\r\n";
$ics .= "STATUS:CONFIRMED\r\n";
$ics .= "SEQUENCE:0\r\n";
$ics .= "END:VEVENT\r\n";
$ics .= "END:VCALENDAR\r\n";
return $ics;
}
/**
* Escape text for ICS format
*
* @param string $text Text to escape
* @return string Escaped text
*/
function escape_ics_text($text)
{
// Remove HTML tags
$text = strip_tags($text);
// Escape special characters
$text = str_replace(array("\\", ";", ",", "\n", "\r"), array("\\\\", "\\;", "\\,", "\\n", ""), $text);
// Limit line length (ICS spec recommends 75 characters)
$text = wordwrap($text, 73, "\r\n ", true);
return $text;
}