Google Calendar integration for booking notifications
- New GoogleCalendar library: createEvent/updateEvent/deleteEvent via service account, all wrapped in try/catch so failures never break booking flow - booking_process: creates worker + owner calendar events on new booking - manage_booking_process: deletes old events, creates new ones on modify - manage_booking_cancel: deletes events before cancellation - Service_model: updateBookingCalEvents() stores gcal event IDs - Admin worker form: Google Calendar ID field added - PHPMailer: enabled exceptions (was silently swallowing SMTP errors) - Config: application/config/google_calendar.php for service account path + Evelin calendar ID DB migration required: ALTER TABLE workers ADD COLUMN google_calendar_id VARCHAR(255) NULL DEFAULT NULL; ALTER TABLE bookings ADD COLUMN gcal_event_id_worker VARCHAR(255) NULL DEFAULT NULL; ALTER TABLE bookings ADD COLUMN gcal_event_id_owner VARCHAR(255) NULL DEFAULT NULL; Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||||
|
|
||||||
|
$config['gcal_service_account_path'] = APPPATH . 'config/service-account-key.json';
|
||||||
|
$config['gcal_evelin_calendar_id'] = 'katozoltan0804@gmail.com'; // Evelin's Calendar ID (e.g. her Gmail address)
|
||||||
@@ -649,7 +649,8 @@ class Admin extends CI_Controller {
|
|||||||
'is_beauty' => $_POST['is_beauty'],
|
'is_beauty' => $_POST['is_beauty'],
|
||||||
'is_barber' => $_POST['is_barber'],
|
'is_barber' => $_POST['is_barber'],
|
||||||
'service_category_id' => $_POST['service_category_id'],
|
'service_category_id' => $_POST['service_category_id'],
|
||||||
'is_active' => $_POST['is_active']
|
'is_active' => $_POST['is_active'],
|
||||||
|
'google_calendar_id' => isset($_POST['google_calendar_id']) ? $_POST['google_calendar_id'] : ''
|
||||||
);
|
);
|
||||||
$this->Service_model->updateWorker($_POST['worker_id'], $workerArray);
|
$this->Service_model->updateWorker($_POST['worker_id'], $workerArray);
|
||||||
$this->Log_model->addLog('workers', $data['currentUser']->username.' módosította a(z) '.$_POST['worker_name'].' nevű dolgozót ('.implode(',',$workerArray).')', $data['currentUser']->username);
|
$this->Log_model->addLog('workers', $data['currentUser']->username.' módosította a(z) '.$_POST['worker_name'].' nevű dolgozót ('.implode(',',$workerArray).')', $data['currentUser']->username);
|
||||||
|
|||||||
@@ -438,6 +438,28 @@ class Pages extends CI_Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Google Calendar: create events for worker and owner
|
||||||
|
$this->load->library('GoogleCalendar');
|
||||||
|
$this->config->load('google_calendar');
|
||||||
|
$newBooking = $this->Service_model->getBookingByToken($manageToken);
|
||||||
|
if($newBooking){
|
||||||
|
$gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
|
||||||
|
$gcalTitle = $worker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $bookingArray['guest_name'];
|
||||||
|
$gcalDescription = 'Guest: ' . $bookingArray['guest_name'] . "\nPhone: " . $bookingArray['guest_phone'] . "\nEmail: " . $bookingArray['guest_email'];
|
||||||
|
$gcalStart = $bookingArray['booking_date'] . 'T' . $bookingArray['booking_start_time'];
|
||||||
|
$gcalEnd = $bookingArray['booking_date'] . 'T' . $bookingArray['booking_finish_time'];
|
||||||
|
$workerEventId = null;
|
||||||
|
$ownerEventId = null;
|
||||||
|
if(!empty($worker->google_calendar_id)){
|
||||||
|
$workerEventId = $this->googlecalendar->createEvent($worker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
||||||
|
}
|
||||||
|
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
|
||||||
|
if(!empty($evelinCalId)){
|
||||||
|
$ownerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
||||||
|
}
|
||||||
|
$this->Service_model->updateBookingCalEvents($newBooking->booking_id, $workerEventId, $ownerEventId);
|
||||||
|
}
|
||||||
|
|
||||||
$content = '';
|
$content = '';
|
||||||
|
|
||||||
switch($_POST['lang']){
|
switch($_POST['lang']){
|
||||||
@@ -832,6 +854,32 @@ class Pages extends CI_Controller {
|
|||||||
if(is_object($sel)) $serviceArray[] = $sel;
|
if(is_object($sel)) $serviceArray[] = $sel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Google Calendar: delete old events, create new ones
|
||||||
|
$this->load->library('GoogleCalendar');
|
||||||
|
$this->config->load('google_calendar');
|
||||||
|
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
|
||||||
|
$oldWorker = $this->Service_model->getWorkerById($booking->worker_id);
|
||||||
|
if(!empty($booking->gcal_event_id_worker) && !empty($oldWorker->google_calendar_id)){
|
||||||
|
$this->googlecalendar->deleteEvent($oldWorker->google_calendar_id, $booking->gcal_event_id_worker);
|
||||||
|
}
|
||||||
|
if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
|
||||||
|
$this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
|
||||||
|
}
|
||||||
|
$gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $serviceArray));
|
||||||
|
$gcalTitle = $worker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $booking->guest_name;
|
||||||
|
$gcalDescription = 'Guest: ' . $booking->guest_name . "\nPhone: " . $booking->guest_phone . "\nEmail: " . $booking->guest_email;
|
||||||
|
$gcalStart = $newBookingDate . 'T' . $newStartTime;
|
||||||
|
$gcalEnd = $newBookingDate . 'T' . $finishTime;
|
||||||
|
$newWorkerEventId = null;
|
||||||
|
$newOwnerEventId = null;
|
||||||
|
if(!empty($worker->google_calendar_id)){
|
||||||
|
$newWorkerEventId = $this->googlecalendar->createEvent($worker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
||||||
|
}
|
||||||
|
if(!empty($evelinCalId)){
|
||||||
|
$newOwnerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
|
||||||
|
}
|
||||||
|
$this->Service_model->updateBookingCalEvents($booking->booking_id, $newWorkerEventId, $newOwnerEventId);
|
||||||
|
|
||||||
$manageLink = SITEURL.$lang.'/manage-booking/'.$token;
|
$manageLink = SITEURL.$lang.'/manage-booking/'.$token;
|
||||||
$totalServicePrice = 0;
|
$totalServicePrice = 0;
|
||||||
foreach($serviceArray as $s){ $totalServicePrice += $s->service_price; }
|
foreach($serviceArray as $s){ $totalServicePrice += $s->service_price; }
|
||||||
@@ -941,6 +989,17 @@ class Pages extends CI_Controller {
|
|||||||
));
|
));
|
||||||
$this->User_model->sendEmail($emailArray, $lang, '');
|
$this->User_model->sendEmail($emailArray, $lang, '');
|
||||||
|
|
||||||
|
// Google Calendar: delete events on cancellation
|
||||||
|
$this->load->library('GoogleCalendar');
|
||||||
|
$this->config->load('google_calendar');
|
||||||
|
if(!empty($booking->gcal_event_id_worker) && !empty($worker->google_calendar_id)){
|
||||||
|
$this->googlecalendar->deleteEvent($worker->google_calendar_id, $booking->gcal_event_id_worker);
|
||||||
|
}
|
||||||
|
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
|
||||||
|
if(!empty($booking->gcal_event_id_owner) && !empty($evelinCalId)){
|
||||||
|
$this->googlecalendar->deleteEvent($evelinCalId, $booking->gcal_event_id_owner);
|
||||||
|
}
|
||||||
|
|
||||||
$this->Service_model->deleteBooking($booking->booking_id);
|
$this->Service_model->deleteBooking($booking->booking_id);
|
||||||
|
|
||||||
$data['selectedLang'] = $lang;
|
$data['selectedLang'] = $lang;
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||||
|
|
||||||
|
class GoogleCalendar {
|
||||||
|
|
||||||
|
private $service = null;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
require_once FCPATH . 'vendor/autoload.php';
|
||||||
|
|
||||||
|
$CI =& get_instance();
|
||||||
|
$CI->config->load('google_calendar');
|
||||||
|
$serviceAccountPath = $CI->config->item('gcal_service_account_path');
|
||||||
|
|
||||||
|
if (!file_exists($serviceAccountPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$client = new Google_Client();
|
||||||
|
$client->setAuthConfig($serviceAccountPath);
|
||||||
|
$client->setScopes([Google_Service_Calendar::CALENDAR]);
|
||||||
|
$this->service = new Google_Service_Calendar($client);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Silently fail — calendar errors must never break the booking flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a calendar event.
|
||||||
|
* @return string|null The new event ID, or null on failure.
|
||||||
|
*/
|
||||||
|
public function createEvent($calendarId, $title, $description, $startDatetime, $endDatetime) {
|
||||||
|
if (!$this->service || empty($calendarId)) return null;
|
||||||
|
try {
|
||||||
|
$event = new Google_Service_Calendar_Event([
|
||||||
|
'summary' => $title,
|
||||||
|
'description' => $description,
|
||||||
|
'start' => ['dateTime' => $startDatetime, 'timeZone' => 'Europe/Oslo'],
|
||||||
|
'end' => ['dateTime' => $endDatetime, 'timeZone' => 'Europe/Oslo'],
|
||||||
|
]);
|
||||||
|
$result = $this->service->events->insert($calendarId, $event);
|
||||||
|
return $result->getId();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing calendar event.
|
||||||
|
* @return string|null The event ID, or null on failure.
|
||||||
|
*/
|
||||||
|
public function updateEvent($calendarId, $eventId, $title, $description, $startDatetime, $endDatetime) {
|
||||||
|
if (!$this->service || empty($calendarId) || empty($eventId)) return null;
|
||||||
|
try {
|
||||||
|
$event = $this->service->events->get($calendarId, $eventId);
|
||||||
|
$event->setSummary($title);
|
||||||
|
$event->setDescription($description);
|
||||||
|
$event->setStart(new Google_Service_Calendar_EventDateTime([
|
||||||
|
'dateTime' => $startDatetime,
|
||||||
|
'timeZone' => 'Europe/Oslo',
|
||||||
|
]));
|
||||||
|
$event->setEnd(new Google_Service_Calendar_EventDateTime([
|
||||||
|
'dateTime' => $endDatetime,
|
||||||
|
'timeZone' => 'Europe/Oslo',
|
||||||
|
]));
|
||||||
|
$result = $this->service->events->update($calendarId, $eventId, $event);
|
||||||
|
return $result->getId();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a calendar event. Silently skips if not found.
|
||||||
|
*/
|
||||||
|
public function deleteEvent($calendarId, $eventId) {
|
||||||
|
if (!$this->service || empty($calendarId) || empty($eventId)) return;
|
||||||
|
try {
|
||||||
|
$this->service->events->delete($calendarId, $eventId);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Silently skip — event may already be gone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -562,6 +562,11 @@ class Service_model extends CI_Model {
|
|||||||
return $query->num_rows() > 0 ? $query->result()[0] : false;
|
return $query->num_rows() > 0 ? $query->result()[0] : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateBookingCalEvents($booking_id, $worker_event_id, $owner_event_id){
|
||||||
|
$this->load->database();
|
||||||
|
$this->db->query("UPDATE bookings SET gcal_event_id_worker = '".$this->db->escape_str($worker_event_id)."', gcal_event_id_owner = '".$this->db->escape_str($owner_event_id)."' WHERE booking_id = '".$this->db->escape_str($booking_id)."';");
|
||||||
|
}
|
||||||
|
|
||||||
public function getWorkerSchedule($worker_id) {
|
public function getWorkerSchedule($worker_id) {
|
||||||
return $this->db
|
return $this->db
|
||||||
->where('worker_id', $worker_id)
|
->where('worker_id', $worker_id)
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ class User_model extends CI_Model {
|
|||||||
if(!empty($addressList)){
|
if(!empty($addressList)){
|
||||||
foreach($addressList as $addressListItem){
|
foreach($addressList as $addressListItem){
|
||||||
// Instantiation and passing `true` enables exceptions
|
// Instantiation and passing `true` enables exceptions
|
||||||
$mail = new PHPMailer(false);
|
$mail = new PHPMailer(true);
|
||||||
$mail->SMTPDebug = false;
|
$mail->SMTPDebug = false;
|
||||||
$mail->do_debug = 1;
|
$mail->do_debug = 1;
|
||||||
try {
|
try {
|
||||||
@@ -188,6 +188,7 @@ class User_model extends CI_Model {
|
|||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
//header('location:'.base_url().'manage-applicants/?email-sent=false&error='.$mail->ErrorInfo);
|
//header('location:'.base_url().'manage-applicants/?email-sent=false&error='.$mail->ErrorInfo);
|
||||||
|
log_message('error', 'sendEmail FAILED: ' . $mail->ErrorInfo);
|
||||||
echo "Üzenetküldési hiba. Mailer Error: {$mail->ErrorInfo}";
|
echo "Üzenetküldési hiba. Mailer Error: {$mail->ErrorInfo}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@
|
|||||||
<option value="0" <?php echo !$selectedItem->is_active?'selected':'';?>>nem</option>
|
<option value="0" <?php echo !$selectedItem->is_active?'selected':'';?>>nem</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="formRow">
|
||||||
|
<label class="formTitle">Google Calendar ID</label>
|
||||||
|
<input type="text" class="formInputBox" name="google_calendar_id" value="<?php echo isset($selectedItem->google_calendar_id) ? $selectedItem->google_calendar_id : '';?>">
|
||||||
|
</div>
|
||||||
<div class="formRow">
|
<div class="formRow">
|
||||||
<input type="submit" class="submitButton" name="updateWorker" value="Módosítás">
|
<input type="submit" class="submitButton" name="updateWorker" value="Módosítás">
|
||||||
<a href="<?php echo base_url();?>workers" class="cancelButton">Mégsem</a>
|
<a href="<?php echo base_url();?>workers" class="cancelButton">Mégsem</a>
|
||||||
|
|||||||
+3
-1
@@ -11,7 +11,9 @@
|
|||||||
"source": "https://github.com/bcit-ci/CodeIgniter"
|
"source": "https://github.com/bcit-ci/CodeIgniter"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.3.7"
|
"php": ">=5.3.7",
|
||||||
|
"google/apiclient": "^2.0",
|
||||||
|
"phpmailer/phpmailer": "^6.0"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"paragonie/random_compat": "Provides better randomness in PHP 5.x"
|
"paragonie/random_compat": "Provides better randomness in PHP 5.x"
|
||||||
|
|||||||
Reference in New Issue
Block a user