/home/edulekha/crm.edulekha.com/modules/appointly/migrations/131_version_131.php
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_Version_131 extends App_module_migration
{
public function up()
{
$CI = &get_instance();
log_message('info', 'Appointly Migration 131: Starting critical fixes for v1.3.1');
try {
// Start transaction for data integrity
$CI->db->trans_start();
// Fix database helper syntax errors first
$this->fixDatabaseHelperSyntax($CI);
// Ensure all required tables exist
$this->ensureRequiredTables($CI);
// Fix missing columns and data integrity
$this->fixMissingColumns($CI);
// Ensure proper service-staff assignments
$this->ensureServiceStaffAssignments($CI);
// Fix menu registration
$this->fixMenuRegistration($CI);
// Set default options
$this->setDefaultOptions($CI);
// Commit transaction
$CI->db->trans_complete();
if ($CI->db->trans_status() === FALSE) {
throw new Exception('Database transaction failed');
}
return true;
} catch (Exception $e) {
$CI->db->trans_rollback();
// Don't re-throw to prevent migration system from breaking
return false;
}
}
private function fixDatabaseHelperSyntax($CI)
{
$table = db_prefix() . 'appointly_appointments';
// Check if table exists first
if (!$CI->db->table_exists($table)) {
return;
}
// Fix the syntax error in database helper - ensure end_hour column exists
if (!$CI->db->field_exists('end_hour', $table)) {
$CI->db->query("ALTER TABLE `{$table}` ADD COLUMN `end_hour` varchar(191) NOT NULL DEFAULT '' AFTER `start_hour`");
}
// Ensure duration column exists
if (!$CI->db->field_exists('duration', $table)) {
$CI->db->query("ALTER TABLE `{$table}` ADD COLUMN `duration` varchar(100) DEFAULT NULL AFTER `start_hour`");
}
// Ensure status column exists with proper enum values
if (!$CI->db->field_exists('status', $table)) {
$CI->db->query("ALTER TABLE `{$table}` ADD COLUMN `status` ENUM('pending', 'cancelled', 'completed', 'no-show', 'in-progress') NOT NULL DEFAULT 'in-progress' AFTER `hash`");
}
// Ensure provider_id column exists
if (!$CI->db->field_exists('provider_id', $table)) {
$CI->db->query("ALTER TABLE `{$table}` ADD COLUMN `provider_id` int(11) DEFAULT NULL AFTER `service_id`");
}
// Ensure service_id column exists
if (!$CI->db->field_exists('service_id', $table)) {
$CI->db->query("ALTER TABLE `{$table}` ADD COLUMN `service_id` int(11) DEFAULT NULL AFTER `id`");
}
// Ensure date_created column exists
if (!$CI->db->field_exists('date_created', $table)) {
$CI->db->query("ALTER TABLE `{$table}` ADD COLUMN `date_created` datetime DEFAULT CURRENT_TIMESTAMP AFTER `id`");
}
// Fix missing critical columns that exist in production
$missing_columns = [
'files' => 'text DEFAULT NULL',
'timezone' => 'varchar(100) DEFAULT NULL',
'invoice_id' => 'int(11) DEFAULT NULL',
'invoice_date' => 'datetime DEFAULT NULL'
];
foreach ($missing_columns as $column => $definition) {
if (!$CI->db->field_exists($column, $table)) {
$CI->db->query("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}");
}
}
// Handle legacy status columns migration
$this->migrateLegacyStatusColumns($CI, $table);
}
private function ensureRequiredTables($CI)
{
// Ensure services table exists
if (!$CI->db->table_exists(db_prefix() . 'appointly_services')) {
$CI->db->query("CREATE TABLE `" . db_prefix() . "appointly_services` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(191) NOT NULL,
`description` text DEFAULT NULL,
`duration` int(11) DEFAULT 60,
`price` decimal(15,2) DEFAULT 0.00,
`color` varchar(10) DEFAULT '#28B8DA',
`active` tinyint(1) DEFAULT 1,
`buffer_before` int(11) DEFAULT 0,
`buffer_after` int(11) DEFAULT 0,
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
log_message('info', 'Appointly Migration 131: Created services table');
}
// Ensure service_staff table exists
if (!$CI->db->table_exists(db_prefix() . 'appointly_service_staff')) {
$CI->db->query("CREATE TABLE `" . db_prefix() . "appointly_service_staff` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`service_id` int(11) NOT NULL,
`staff_id` int(11) NOT NULL,
`is_provider` tinyint(1) DEFAULT 1,
`is_primary` tinyint(1) DEFAULT 0,
`working_hours` text DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `service_id` (`service_id`),
KEY `staff_id` (`staff_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
log_message('info', 'Appointly Migration 131: Created service_staff table');
}
// Ensure company_schedule table exists
if (!$CI->db->table_exists(db_prefix() . 'appointly_company_schedule')) {
$CI->db->query("CREATE TABLE `" . db_prefix() . "appointly_company_schedule` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`weekday` varchar(20) NOT NULL,
`start_time` time DEFAULT '09:00:00',
`end_time` time DEFAULT '17:00:00',
`is_enabled` tinyint(1) DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `weekday` (`weekday`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Insert default company schedule
$weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
foreach ($weekdays as $day) {
$is_enabled = in_array($day, ['Saturday', 'Sunday']) ? 0 : 1;
$CI->db->query("INSERT IGNORE INTO `" . db_prefix() . "appointly_company_schedule`
(`weekday`, `start_time`, `end_time`, `is_enabled`)
VALUES ('{$day}', '09:00:00', '17:00:00', {$is_enabled})");
}
}
// Ensure staff_working_hours table exists
if (!$CI->db->table_exists(db_prefix() . 'appointly_staff_working_hours')) {
$CI->db->query("CREATE TABLE `" . db_prefix() . "appointly_staff_working_hours` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`staff_id` int(11) NOT NULL,
`weekday` varchar(20) NOT NULL,
`start_time` time DEFAULT NULL,
`end_time` time DEFAULT NULL,
`is_enabled` tinyint(1) DEFAULT 1,
`use_company_schedule` tinyint(1) DEFAULT 1,
PRIMARY KEY (`id`),
KEY `staff_id` (`staff_id`),
KEY `weekday` (`weekday`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
}
}
private function fixMissingColumns($CI)
{
$services_table = db_prefix() . 'appointly_services';
// Check if services table exists first
if (!$CI->db->table_exists($services_table)) {
return;
}
// Add buffer columns to services table if missing
if (!$CI->db->field_exists('buffer_before', $services_table)) {
$CI->db->query("ALTER TABLE `{$services_table}` ADD COLUMN `buffer_before` int(11) DEFAULT 0 AFTER `active`");
}
if (!$CI->db->field_exists('buffer_after', $services_table)) {
$CI->db->query("ALTER TABLE `{$services_table}` ADD COLUMN `buffer_after` int(11) DEFAULT 0 AFTER `buffer_before`");
}
// Add is_primary column to service_staff table if missing
$service_staff_table = db_prefix() . 'appointly_service_staff';
if ($CI->db->table_exists($service_staff_table) && !$CI->db->field_exists('is_primary', $service_staff_table)) {
$CI->db->query("ALTER TABLE `{$service_staff_table}` ADD COLUMN `is_primary` tinyint(1) DEFAULT 0 AFTER `is_provider`");
// Set first provider per service as primary
$CI->db->query("
UPDATE {$service_staff_table} ss1
JOIN (
SELECT service_id, MIN(id) as min_id
FROM {$service_staff_table}
WHERE is_provider = 1
GROUP BY service_id
) ss2 ON ss1.service_id = ss2.service_id AND ss1.id = ss2.min_id
SET ss1.is_primary = 1
");
}
// Fix missing end_hour values by calculating from start_hour + duration
$appointments_table = db_prefix() . 'appointly_appointments';
if ($CI->db->table_exists($appointments_table) && $CI->db->table_exists($services_table)) {
$CI->db->query("UPDATE `{$appointments_table}` a
JOIN `{$services_table}` s ON a.service_id = s.id
SET a.end_hour = DATE_FORMAT(
DATE_ADD(
STR_TO_DATE(CONCAT(a.date, ' ', a.start_hour), '%Y-%m-%d %H:%i'),
INTERVAL s.duration MINUTE
), '%H:%i'
)
WHERE (a.end_hour = '' OR a.end_hour IS NULL) AND a.service_id IS NOT NULL");
}
}
private function ensureServiceStaffAssignments($CI)
{
// Check if there are any service staff assignments
$staff_assignments_count = 0;
if ($CI->db->table_exists(db_prefix() . 'appointly_service_staff')) {
$staff_assignments_count = $CI->db->count_all(db_prefix() . 'appointly_service_staff');
}
if ($staff_assignments_count == 0) {
// Create default service if none exist
$services_count = 0;
if ($CI->db->table_exists(db_prefix() . 'appointly_services')) {
$services_count = $CI->db->count_all(db_prefix() . 'appointly_services');
}
if ($services_count == 0) {
// Insert default service
$CI->db->insert(db_prefix() . 'appointly_services', [
'name' => 'General Consultation',
'description' => 'General consultation service',
'duration' => 60,
'price' => 0.00,
'color' => '#28B8DA',
'active' => 1,
'buffer_before' => 0,
'buffer_after' => 0
]);
$service_id = $CI->db->insert_id();
} else {
// Get first service
$service = $CI->db->get(db_prefix() . 'appointly_services', 1)->row();
$service_id = $service->id;
}
// Assign service to admin (staff_id = 1) using INSERT IGNORE to prevent duplicates
$CI->db->query("INSERT IGNORE INTO `" . db_prefix() . "appointly_service_staff`
(service_id, staff_id, is_provider, is_primary)
VALUES ({$service_id}, 1, 1, 1)");
}
}
private function migrateLegacyStatusColumns($CI, $table)
{
// Check if old status columns exist and migrate them
$legacy_columns = ['approved', 'finished', 'cancelled'];
$has_legacy = false;
foreach ($legacy_columns as $col) {
if ($CI->db->field_exists($col, $table)) {
$has_legacy = true;
break;
}
}
if ($has_legacy) {
// This ensures ALL appointments are properly migrated, not just pending/in-progress ones
$CI->db->query("UPDATE `{$table}` SET
status = CASE
WHEN cancelled = 1 THEN 'cancelled'
WHEN finished = 1 THEN 'completed'
WHEN approved = 1 THEN 'in-progress'
ELSE 'pending'
END
WHERE (cancelled IS NOT NULL OR finished IS NOT NULL OR approved IS NOT NULL)");
// Drop legacy columns
foreach ($legacy_columns as $col) {
if ($CI->db->field_exists($col, $table)) {
$CI->db->query("ALTER TABLE `{$table}` DROP COLUMN `{$col}`");
log_message('info', "Appointly Migration 131: Dropped legacy column {$col}");
}
}
}
}
private function fixMenuRegistration($CI)
{
// Reset menu to ensure Services menu appears
if (function_exists('update_option')) {
// Reset aside menu to force regeneration
update_option('aside_menu_active', '[]');
}
}
private function setDefaultOptions($CI)
{
// Set default booking services availability if not set
$appointments_booking_services_availability = get_option('appointments_booking_services_availability');
if (!$appointments_booking_services_availability) {
update_option('appointments_booking_services_availability', json_encode([1]));
}
// Set other default options
if (!get_option('appointly_show_clients_schedule_button')) {
add_option('appointly_show_clients_schedule_button', 0);
}
if (!get_option('appointly_tab_on_clients_page')) {
add_option('appointly_tab_on_clients_page', 0);
}
}
}