<?php
defined('BASEPATH') OR exit('No direct script access allowed');

class App extends CI_Controller
{
	protected $activeGoalDefinitionsCache = null;
	protected $employeeGroupMembersIndexCache = null;
	protected $goalMetricEmployeeCache = array();
	protected $goalMetricAllEmployeesCache = array();
	protected $goalMetricDailyEmployeeCache = array();
	protected $goalMetricRankingCache = array();
	protected $groupGoalMetricCache = array();
	protected $groupGoalMetricRankingCache = array();

	public function __construct()
	{
		parent::__construct();
		$this->load->model('App_write_model');
		$this->load->model('Gala_read_model');
		$this->load->model('Tasks_model');
		$this->auto_snapshot_tick();
		$this->auto_task_assignment_tick();
	}

	public function index()
	{
		redirect('visor');
	}

	public function home()
	{
		if (!$this->is_authenticated())
		{
			redirect('login');
		}

		$this->ensure_active_authenticated_user();

		$auth_user = $this->get_auth_user();
		$employee_id = $auth_user ? (int)$auth_user['employee_id'] : 0;
		$task_preview = $this->has_permission('tareas.view') ? array_slice($this->Tasks_model->get_assignments_for_employee($employee_id, 'open'), 0, 3) : array();
		$task_summary = $this->has_permission('tareas.view') ? $this->Tasks_model->get_assignment_summary_for_employee($employee_id) : array(
			'pending' => 0,
			'overdue' => 0,
			'completed' => 0,
			'total_open' => 0,
		);
		$goal_cards = $this->has_permission('metas.view') ? $this->evaluate_goal_cards_for_employee($employee_id) : array();
		$goal_summary = $this->has_permission('metas.view') ? $this->build_employee_goal_summary($goal_cards) : array(
			'completed' => 0,
			'total' => 0,
		);
		$completed_today_tasks = $this->has_permission('tareas.view') ? $this->Tasks_model->get_assignments_for_employee($employee_id, 'completed') : array();
		$menu_overview_cards = array(
			array(
				'label' => 'Metas',
				'completed' => isset($goal_summary['completed']) ? (int)$goal_summary['completed'] : 0,
				'total' => isset($goal_summary['total']) ? (int)$goal_summary['total'] : 0,
			),
			array(
				'label' => 'Tareas',
				'completed' => count($completed_today_tasks),
				'total' => (int)(isset($task_summary['total_open']) ? $task_summary['total_open'] : 0) + count($completed_today_tasks),
			),
		);

		$this->render('app/home', array(
			'page_title' => 'Inicio',
			'current_module' => '',
			'task_preview' => $task_preview,
			'task_summary' => $task_summary,
			'menu_overview_cards' => $menu_overview_cards,
		));
	}

	public function dashboard()
	{
		$this->require_permission('dashboard.view');

		$search = trim((string)$this->input->get('q'));
		$visor_status = trim((string)$this->input->get('visor_status'));
		$goal_status = trim((string)$this->input->get('goal_status'));
		$group_id = (int)$this->input->get('group_id');

		if ($visor_status === '')
		{
			$visor_status = 'active';
		}
		if ($goal_status === '')
		{
			$goal_status = 'all';
		}

		$gala_employees = $this->Gala_read_model->get_employees_for_visor();
		$app_users = $this->App_write_model->get_all_app_users_index();
		$groups = $this->App_write_model->get_employee_groups(TRUE);
		$group_members_index = $this->App_write_model->get_employee_group_members_index();
		$employee_groups_index = $this->build_employee_groups_index($groups, $group_members_index);

		$rows = array();
		$summary = array(
			'total' => 0,
			'active_visor' => 0,
			'completed' => 0,
			'partial' => 0,
			'no_goals' => 0,
			'employees_with_open_tasks' => 0,
			'tasks_completed_today' => 0,
		);

		foreach ($gala_employees as $employee)
		{
			$employee_id = (int)$employee['person_id'];
			$app_user = isset($app_users[$employee_id]) ? $app_users[$employee_id] : null;
			$is_active_in_visor = !$app_user || !isset($app_user['is_enabled']) || (int)$app_user['is_enabled'] === 1;
			$group_memberships = isset($employee_groups_index[$employee_id]) ? $employee_groups_index[$employee_id] : array();

			if ($search !== '' && stripos((string)$employee['full_name'], $search) === FALSE)
			{
				continue;
			}

			if ($visor_status === 'active' && !$is_active_in_visor)
			{
				continue;
			}

			if ($visor_status === 'inactive' && $is_active_in_visor)
			{
				continue;
			}

			if ($group_id > 0)
			{
				$belongs_to_group = FALSE;
				foreach ($group_memberships as $membership)
				{
					if ((int)$membership['id'] === $group_id)
					{
						$belongs_to_group = TRUE;
						break;
					}
				}
				if (!$belongs_to_group)
				{
					continue;
				}
			}

			$goal_cards = $this->evaluate_goal_cards_for_employee($employee_id);
			$goal_summary = $this->build_employee_goal_summary($goal_cards);
			$task_summary = $this->Tasks_model->get_assignment_summary_for_employee($employee_id);
			$open_tasks = $this->Tasks_model->get_assignments_for_employee($employee_id, 'open');
			$completed_today_tasks = $this->Tasks_model->get_assignments_for_employee($employee_id, 'completed');
			$tasks_completed_today = count($completed_today_tasks);

			if ($goal_status !== 'all' && $goal_summary['status_key'] !== $goal_status)
			{
				continue;
			}

			$rows[] = array(
				'employee_id' => $employee_id,
				'full_name' => isset($employee['full_name']) ? $employee['full_name'] : 'Empleado',
				'is_active_in_gala' => !empty($employee['is_active_in_gala']),
				'is_active_in_visor' => $is_active_in_visor,
				'groups' => $group_memberships,
				'goal_cards' => $goal_cards,
				'goal_summary' => $goal_summary,
				'task_summary' => $task_summary,
				'task_open_preview' => array_slice($open_tasks, 0, 3),
				'task_completed_today_preview' => array_slice($completed_today_tasks, 0, 3),
				'tasks_completed_today' => $tasks_completed_today,
			);

			$summary['total']++;
			if ($is_active_in_visor)
			{
				$summary['active_visor']++;
			}
			$summary[$goal_summary['status_key']]++;
			if (!empty($task_summary['total_open']))
			{
				$summary['employees_with_open_tasks']++;
			}
			$summary['tasks_completed_today'] += $tasks_completed_today;
		}

		usort($rows, function ($a, $b) {
			if ($a['goal_summary']['progress_percent'] === $b['goal_summary']['progress_percent'])
			{
				return strcasecmp($a['full_name'], $b['full_name']);
			}

			return ($a['goal_summary']['progress_percent'] < $b['goal_summary']['progress_percent']) ? 1 : -1;
		});

		$this->render('app/dashboard', array(
			'page_title' => 'Dashboard',
			'current_module' => 'dashboard',
			'hide_main_menu' => TRUE,
			'employees' => $rows,
			'groups' => $groups,
			'filters' => array(
				'q' => $search,
				'visor_status' => $visor_status,
				'goal_status' => $goal_status,
				'group_id' => $group_id,
			),
			'summary' => $summary,
		));
	}

	public function dashboard_empleado($employee_id)
	{
		$this->require_permission('dashboard.view');

		$employee_id = (int)$employee_id;
		$employee = $this->find_gala_employee($employee_id);
		if (!$employee)
		{
			show_404();
		}

		$app_user = $this->App_write_model->get_app_user($employee_id);
		$is_active_in_visor = !$app_user || !isset($app_user['is_enabled']) || (int)$app_user['is_enabled'] === 1;
		$groups = $this->App_write_model->get_employee_groups(TRUE);
		$group_members_index = $this->App_write_model->get_employee_group_members_index();
		$employee_groups_index = $this->build_employee_groups_index($groups, $group_members_index);
		$group_memberships = isset($employee_groups_index[$employee_id]) ? $employee_groups_index[$employee_id] : array();
		$goal_cards = $this->evaluate_goal_cards_for_employee($employee_id);
		$goal_summary = $this->build_employee_goal_summary($goal_cards);
		$report_data = $this->build_goal_report_for_employee($employee_id);
		$active_tab = trim((string)$this->input->get('tab'));
		$active_tab = $active_tab === 'tareas' ? 'tareas' : 'metas';
		$task_summary = $this->Tasks_model->get_assignment_summary_for_employee($employee_id);
		$task_open_assignments = $this->Tasks_model->get_assignments_for_employee($employee_id, 'open');
		$task_report = $this->build_task_report_for_employee($employee_id);
		$sales_today_total = $this->Gala_read_model->get_vendor_sales_total_for_today($employee_id);
		$sales_month_total = $this->Gala_read_model->get_vendor_sales_total_for_current_month($employee_id);
		$sales_today_count = $this->Gala_read_model->count_vendor_sales_for_today($employee_id);
		$vendor_rank = $this->Gala_read_model->get_vendor_sales_rank_for_today($employee_id);

		$this->render('app/dashboard_empleado', array(
			'page_title' => 'Detalle de empleado',
			'current_module' => 'dashboard',
			'hide_main_menu' => TRUE,
			'employee' => array(
				'employee_id' => $employee_id,
				'full_name' => isset($employee['full_name']) ? $employee['full_name'] : 'Empleado',
				'is_active_in_gala' => !empty($employee['is_active_in_gala']),
				'is_active_in_visor' => $is_active_in_visor,
			),
			'groups' => $group_memberships,
			'goal_cards' => $goal_cards,
			'goal_summary' => $goal_summary,
			'report_data' => $report_data,
			'active_tab' => $active_tab,
			'task_summary' => $task_summary,
			'task_open_assignments' => $task_open_assignments,
			'task_report' => $task_report,
			'sales_today_total' => $sales_today_total,
			'sales_month_total' => $sales_month_total,
			'sales_today_count' => $sales_today_count,
			'vendor_rank' => $vendor_rank,
		));
	}

	public function login()
	{
		if ($this->is_authenticated())
		{
			$this->redirect_to_first_allowed_route();
		}

		$this->form_validation->set_rules('username', 'Usuario', 'trim|required');
		$this->form_validation->set_rules('password', 'Contrasena', 'required');

		$data = array(
			'error' => '',
			'username' => (string)$this->input->post('username'),
			'continue' => (string)$this->input->get_post('continue'),
			'company_name' => $this->get_company_name(),
		);

		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$user = $this->Gala_read_model->authenticate_employee(
				(string)$this->input->post('username'),
				(string)$this->input->post('password')
			);

			if ($user && $this->App_write_model->can_app_user_login($user['person_id']))
			{
				$this->App_write_model->run_migration();
				$this->App_write_model->touch_app_user_login($user['person_id'], array(
					'username' => isset($user['username']) ? $user['username'] : '',
					'full_name' => isset($user['full_name']) ? $user['full_name'] : '',
				));

				$this->session->sess_regenerate(TRUE);
				$this->session->set_userdata('visor_auth', array(
					'employee_id' => (int)$user['person_id'],
					'username' => isset($user['username']) ? $user['username'] : '',
					'full_name' => isset($user['full_name']) ? $user['full_name'] : '',
					'email' => isset($user['email']) ? $user['email'] : '',
					'logged_in_at' => date('Y-m-d H:i:s'),
				));

				$continue = trim((string)$this->input->post('continue'));
				if ($continue !== '')
				{
					redirect($continue);
				}

				redirect('inicio');
			}

			$data['error'] = 'Usuario o contrasena invalida, o acceso bloqueado en Visor.';
		}

		$this->load->view('auth/login', $data);
	}

	public function logout()
	{
		$this->session->unset_userdata('visor_auth');
		$this->session->sess_destroy();
		redirect('visor');
	}

	public function cron_snapshots()
	{
		if (!$this->input->is_cli_request())
		{
			show_404();
		}

		$this->App_write_model->run_migration();
		$employees = $this->Gala_read_model->get_employees_for_visor();
		if (empty($employees))
		{
			return;
		}

		$timezone = new DateTimeZone('America/Caracas');
		$today = new DateTime('now', $timezone);
		$month_start = new DateTime($today->format('Y-m-01'), $timezone);
		$month_end = new DateTime($today->format('Y-m-t'), $timezone);

		foreach ($employees as $employee)
		{
			$employee_id = isset($employee['person_id']) ? (int)$employee['person_id'] : 0;
			if ($employee_id <= 0)
			{
				continue;
			}
			$this->ensure_employee_goal_snapshots($employee_id, $month_start, $month_end, $today, TRUE);
		}
	}

	public function visor()
	{
		$auth_user = $this->get_auth_user();
		$rate = $this->Gala_read_model->get_current_exchange_rate();
		$reference = '';
		$lookup_result = null;
		$error = '';
		$history = $this->get_visor_history();

		if ($this->input->method(TRUE) === 'POST')
		{
			$reference = trim((string)$this->input->post('reference'));
			if ($reference === '')
			{
				$error = 'Ingresa un codigo para consultar.';
			}
			else
			{
				$lookup_result = $this->Gala_read_model->lookup_item_for_visor($reference, 1);
				if ($lookup_result)
				{
					$history = $this->push_visor_history($lookup_result);
				}
				else
				{
					$error = 'No encontramos un articulo con esa referencia.';
				}
			}
		}

		$this->render('app/visor', array(
			'page_title' => 'Visor',
			'current_module' => 'visor',
			'visor_standalone' => TRUE,
			'exchange_rate' => $rate,
			'app_name' => $this->App_write_model->get_setting('app_name', 'Visor'),
			'company_name' => $this->get_company_name(),
			'discount_percent' => (float)$this->App_write_model->get_setting('discount_percent', '0'),
			'discount_percent_divisa' => (float)$this->App_write_model->get_setting('discount_percent_divisa', $this->App_write_model->get_setting('discount_percent', '0')),
			'discount_percent_bs' => (float)$this->App_write_model->get_setting('discount_percent_bs', $this->App_write_model->get_setting('discount_percent', '0')),
			'user_preferences' => $this->App_write_model->get_user_preferences($auth_user ? (int)$auth_user['employee_id'] : 0),
			'reference' => '',
			'lookup_result' => $lookup_result,
			'lookup_error' => $error,
			'history' => $history,
		));
	}

	public function visor_clear_history()
	{
		$this->session->unset_userdata('visor_history');
		redirect('visor');
	}

	public function ventas()
	{
		$this->require_permission('ventas.view');
		$auth_user = $this->get_auth_user();
		$sales_limit = max(1, (int)$this->App_write_model->get_setting('sales_daily_limit', '3'));
		$employee_id = $auth_user ? (int)$auth_user['employee_id'] : 0;
		$sales_today = $this->Gala_read_model->get_latest_vendor_sales_for_today($employee_id, $sales_limit);
		$total_sales_today = $this->Gala_read_model->count_vendor_sales_for_today($employee_id);
		$vendor_rank = $this->Gala_read_model->get_vendor_sales_rank_for_today($employee_id);
		$this->render('app/ventas', array(
			'page_title' => 'Ventas',
			'current_module' => 'ventas',
			'hide_main_menu' => TRUE,
			'can_view_rank' => $this->has_permission('ventas.rank.view'),
			'sales_limit' => $sales_limit,
			'sales_today' => $sales_today,
			'total_sales_today' => $total_sales_today,
			'vendor_rank' => $this->has_permission('ventas.rank.view') ? $vendor_rank : null,
			'motivational_message' => $this->pick_sales_message($total_sales_today),
		));
	}

	public function metas()
	{
		$this->require_permission('metas.view');
		$auth_user = $this->get_auth_user();
		$employee_id = $auth_user ? (int)$auth_user['employee_id'] : 0;
		$goal_cards = $this->evaluate_goal_cards_for_employee($employee_id);

		$this->render('app/metas', array(
			'page_title' => 'Metas',
			'current_module' => 'metas',
			'hide_main_menu' => TRUE,
			'can_view_report' => $this->has_permission('metas.report.view'),
			'goal_cards' => $goal_cards,
		));
	}

	public function metas_reporte()
	{
		$this->require_permissions(array('metas.view', 'metas.report.view'));
		$auth_user = $this->get_auth_user();
		$employee_id = $auth_user ? (int)$auth_user['employee_id'] : 0;
		$report_data = $this->build_goal_report_for_employee($employee_id);

		$this->render('app/metas_reporte', array(
			'page_title' => 'Reporte de metas',
			'current_module' => 'metas',
			'hide_main_menu' => TRUE,
			'report_data' => $report_data,
		));
	}

	public function notificar()
	{
		$this->require_permission('notificar.view');
		$auth_user = $this->get_auth_user();
		$success = '';
		$error = '';

		$this->form_validation->set_rules('message', 'Mensaje', 'trim|required');
		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$this->App_write_model->run_migration();
			$employee_id = $auth_user ? (int)$auth_user['employee_id'] : 0;
			$employee_name = $auth_user && !empty($auth_user['full_name']) ? $auth_user['full_name'] : 'Empleado';
			$message = trim((string)$this->input->post('message'));
			if ($message !== '' && $this->App_write_model->create_notification($employee_id, $employee_name, $message))
			{
				$success = 'Notificacion enviada.';
			}
			else
			{
				$error = 'No pudimos guardar la notificacion.';
			}
		}
		elseif ($this->input->method(TRUE) === 'POST')
		{
			$error = 'Debes escribir una notificacion.';
		}

		$this->render('app/notificar', array(
			'page_title' => 'Notificar',
			'current_module' => 'notificar',
			'hide_main_menu' => TRUE,
			'flash_message' => $success,
			'flash_error' => $error,
		));
	}

	public function personalizar()
	{
		$this->require_permission('personalizar.view');
		$this->render('app/personalizar', array(
			'page_title' => 'Personalizar',
			'current_module' => 'personalizar',
			'hide_main_menu' => TRUE,
			'can_view_personalizar_visor' => $this->has_permission('personalizar.visor.view'),
			'can_view_personalizar_aplicacion' => $this->has_permission('personalizar.aplicacion.view'),
		));
	}

	public function personalizar_visor()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.visor.view'));
		$this->render('app/personalizar_visor', array(
			'page_title' => 'Personalizar Visor',
			'current_module' => 'personalizar',
			'hide_main_menu' => TRUE,
		));
	}

	public function personalizar_aplicacion()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.aplicacion.view'));
		$this->render('app/personalizar_aplicacion', array(
			'page_title' => 'Personalizar Aplicacion',
			'current_module' => 'personalizar',
			'hide_main_menu' => TRUE,
		));
	}

	public function personalizar_visor_product_name()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.visor.view'));
		$this->render_preference_pair_page('Nombre de Producto', 'personalizar/visor', 'personalizar/visor/nombre-producto', array(
			array('name' => 'visor_product_name_size', 'label' => 'Tamano', 'type' => 'number'),
			array('name' => 'visor_product_name_color', 'label' => 'Color', 'type' => 'color'),
		));
	}

	public function personalizar_visor_price_divisa()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.visor.view'));
		$this->render_preference_pair_page('Precio de Divisa', 'personalizar/visor', 'personalizar/visor/precio-divisa', array(
			array('name' => 'visor_price_divisa_size', 'label' => 'Tamano', 'type' => 'number'),
			array('name' => 'visor_price_divisa_color', 'label' => 'Color', 'type' => 'color'),
		));
	}

	public function personalizar_visor_price_bs()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.visor.view'));
		$this->render_preference_pair_page('Precio Bolivares', 'personalizar/visor', 'personalizar/visor/precio-bolivares', array(
			array('name' => 'visor_price_bs_size', 'label' => 'Tamano', 'type' => 'number'),
			array('name' => 'visor_price_bs_color', 'label' => 'Color', 'type' => 'color'),
		));
	}

	public function personalizar_visor_text()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.visor.view'));
		$this->render_preference_pair_page('Texto del Visor', 'personalizar/visor', 'personalizar/visor/texto', array(
			array('name' => 'visor_text_size', 'label' => 'Tamano', 'type' => 'number'),
			array('name' => 'visor_text_color', 'label' => 'Color', 'type' => 'color'),
		));
	}

	public function personalizar_visor_button()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.visor.view'));
		$this->render_preference_pair_page('Boton del Visor', 'personalizar/visor', 'personalizar/visor/boton', array(
			array('name' => 'visor_button_size', 'label' => 'Tamano', 'type' => 'number'),
			array('name' => 'visor_button_color', 'label' => 'Color', 'type' => 'color'),
		));
	}

	public function personalizar_visor_background()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.visor.view'));
		$this->render_preference_pair_page('Fondo del Visor', 'personalizar/visor', 'personalizar/visor/fondo', array(
			array('name' => 'visor_background_color', 'label' => 'Fondo', 'type' => 'color'),
			array('name' => 'visor_box_color', 'label' => 'Cajas', 'type' => 'color'),
		));
	}

	public function personalizar_application_text()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.aplicacion.view'));
		$this->render_preference_pair_page('Texto de Aplicacion', 'personalizar/aplicacion', 'personalizar/aplicacion/texto', array(
			array('name' => 'application_text_size', 'label' => 'Tamano', 'type' => 'number'),
			array('name' => 'application_text_color', 'label' => 'Color', 'type' => 'color'),
		));
	}

	public function personalizar_application_button()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.aplicacion.view'));
		$this->render_preference_pair_page('Boton de Aplicacion', 'personalizar/aplicacion', 'personalizar/aplicacion/boton', array(
			array('name' => 'application_button_size', 'label' => 'Tamano', 'type' => 'number'),
			array('name' => 'application_button_color', 'label' => 'Color', 'type' => 'color'),
			array('name' => 'application_menu_button_color', 'label' => 'Color del menu', 'type' => 'color'),
			array('name' => 'application_menu_active_color', 'label' => 'Color del menu activo', 'type' => 'color'),
		));
	}

	public function personalizar_application_background()
	{
		$this->require_permissions(array('personalizar.view', 'personalizar.aplicacion.view'));
		$this->render_preference_pair_page('Fondo de Aplicacion', 'personalizar/aplicacion', 'personalizar/aplicacion/fondo', array(
			array('name' => 'application_background_color', 'label' => 'Fondo', 'type' => 'color'),
			array('name' => 'application_box_color', 'label' => 'Cajas', 'type' => 'color'),
		));
	}

	public function configuracion()
	{
		$this->require_permission('configuracion.view');
		$this->render('app/configuracion', array(
			'page_title' => 'Configuracion',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'can_view_config_descuentos' => $this->has_permission('configuracion.descuentos.view'),
			'can_view_config_grupos' => $this->has_permission('configuracion.grupos.view'),
			'can_view_config_metas' => $this->has_permission('configuracion.metas.view'),
			'can_view_config_tareas' => $this->has_permission('configuracion.tareas.view'),
			'can_view_config_notificaciones' => $this->has_permission('configuracion.notificaciones.view'),
			'can_view_config_ventas' => $this->has_permission('configuracion.ventas.view'),
			'can_view_config_empleados' => $this->has_permission('configuracion.empleados.view'),
		));
	}

	public function configuracion_notificaciones()
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.notificaciones.view'));
		$this->App_write_model->run_migration();
		$auth_user = $this->get_auth_user();
		$flash_message = '';
		$flash_error = '';
		$active_tab = trim((string)$this->input->get('tab'));
		$active_tab = $active_tab === 'reviewed' ? 'reviewed' : 'pending';

		if ($this->input->method(TRUE) === 'POST')
		{
			$action = trim((string)$this->input->post('action_type'));
			if ($action === 'review')
			{
				$notification_id = (int)$this->input->post('notification_id');
				$reviewer_id = $auth_user ? (int)$auth_user['employee_id'] : 0;
				$reviewer_name = $auth_user && !empty($auth_user['full_name']) ? $auth_user['full_name'] : 'Empleado';
				if ($notification_id > 0 && $this->App_write_model->mark_notification_reviewed($notification_id, $reviewer_id, $reviewer_name))
				{
					$flash_message = 'Notificacion marcada como revisada.';
					$active_tab = 'reviewed';
				}
				else
				{
					$flash_error = 'No pudimos actualizar la notificacion.';
				}
			}
		}

		$notifications = $this->App_write_model->get_notifications();
		$filtered = array();
		foreach ($notifications as $notification)
		{
			$is_reviewed = !empty($notification['is_reviewed']);
			if ($active_tab === 'reviewed' && $is_reviewed)
			{
				$filtered[] = $notification;
			}
			if ($active_tab === 'pending' && !$is_reviewed)
			{
				$filtered[] = $notification;
			}
		}
		$this->render('app/configuracion_notificaciones', array(
			'page_title' => 'Notificaciones',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'notifications' => $filtered,
			'active_tab' => $active_tab,
			'flash_message' => $flash_message,
			'flash_error' => $flash_error,
		));
	}

	public function configuracion_grupos()
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.grupos.view'));
		$this->App_write_model->run_migration();
		$groups = $this->App_write_model->get_employee_groups();
		$members_index = $this->App_write_model->get_employee_group_members_index();
		$employees_index = $this->index_option_rows($this->Gala_read_model->get_employees_for_visor(), 'person_id');
		$decorated = array();

		foreach ($groups as $group)
		{
			$group_id = isset($group['id']) ? (int)$group['id'] : 0;
			$member_ids = isset($members_index[$group_id]) ? $members_index[$group_id] : array();
			$member_names = array();
			foreach ($member_ids as $member_id)
			{
				if (isset($employees_index[$member_id]['full_name']))
				{
					$member_names[] = $employees_index[$member_id]['full_name'];
				}
			}

			$group['member_count'] = count($member_ids);
			$group['member_names'] = array_slice($member_names, 0, 3);
			$decorated[] = $group;
		}

		$this->render('app/configuracion_grupos', array(
			'page_title' => 'Grupos',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'groups' => $decorated,
		));
	}

	public function configuracion_grupo_nuevo()
	{
		$this->handle_employee_group_form();
	}

	public function configuracion_grupo_editar($group_id)
	{
		$this->handle_employee_group_form((int)$group_id);
	}

	public function configuracion_descuentos()
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.descuentos.view'));
		$this->form_validation->set_rules('discount_percent_divisa', 'Descuento en divisa', 'trim|required|numeric');
		$this->form_validation->set_rules('discount_percent_bs', 'Descuento en bolivares', 'trim|required|numeric');

		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$discount_divisa = number_format((float)$this->input->post('discount_percent_divisa'), 2, '.', '');
			$discount_bs = number_format((float)$this->input->post('discount_percent_bs'), 2, '.', '');
			$this->App_write_model->save_setting('discount_percent_divisa', $discount_divisa);
			$this->App_write_model->save_setting('discount_percent_bs', $discount_bs);
			$this->App_write_model->save_setting('discount_percent', $discount_divisa);
			redirect('configuracion/descuentos');
		}

		$this->render('app/configuracion_descuentos', array(
			'page_title' => 'Descuentos',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'discount_percent_divisa' => (float)$this->App_write_model->get_setting('discount_percent_divisa', $this->App_write_model->get_setting('discount_percent', '0')),
			'discount_percent_bs' => (float)$this->App_write_model->get_setting('discount_percent_bs', $this->App_write_model->get_setting('discount_percent', '0')),
		));
	}

	public function configuracion_metas()
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.metas.view'));
		$this->form_validation->set_rules('goal_global_daily_enabled', 'Meta global diaria activa', 'trim');
		$this->form_validation->set_rules('goal_global_monthly_enabled', 'Meta global mensual activa', 'trim');
		$this->form_validation->set_rules('goal_global_daily_amount', 'Meta global diaria', 'trim|required|numeric');
		$this->form_validation->set_rules('goal_global_monthly_amount', 'Meta global mensual', 'trim|required|numeric');

		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$this->App_write_model->save_setting('goal_global_daily_enabled', $this->input->post('goal_global_daily_enabled') ? '1' : '0');
			$this->App_write_model->save_setting('goal_global_monthly_enabled', $this->input->post('goal_global_monthly_enabled') ? '1' : '0');
			$this->App_write_model->save_setting('goal_global_daily_amount', number_format((float)$this->input->post('goal_global_daily_amount'), 2, '.', ''));
			$this->App_write_model->save_setting('goal_global_monthly_amount', number_format((float)$this->input->post('goal_global_monthly_amount'), 2, '.', ''));
			redirect('configuracion/metas');
		}

		$this->render('app/configuracion_metas', array(
			'page_title' => 'Metas',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'goal_global_daily_enabled' => $this->App_write_model->get_setting('goal_global_daily_enabled', '0') === '1',
			'goal_global_monthly_enabled' => $this->App_write_model->get_setting('goal_global_monthly_enabled', '0') === '1',
			'goal_global_daily_amount' => (float)$this->App_write_model->get_setting('goal_global_daily_amount', '0'),
			'goal_global_monthly_amount' => (float)$this->App_write_model->get_setting('goal_global_monthly_amount', '0'),
			'goal_definitions' => $this->decorate_goal_definitions($this->App_write_model->get_goal_definitions(TRUE)),
		));
	}

	public function configuracion_meta_nueva()
	{
		$this->handle_goal_definition_form();
	}

	public function configuracion_meta_editar($goal_id)
	{
		$this->handle_goal_definition_form((int)$goal_id);
	}

	public function configuracion_ventas()
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.ventas.view'));
		$this->form_validation->set_rules('sales_daily_limit', 'Cantidad de ventas diarias', 'trim|required|integer|greater_than[0]');

		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$this->App_write_model->save_setting('sales_daily_limit', (string)max(1, (int)$this->input->post('sales_daily_limit')));
			$this->App_write_model->save_setting('sales_messages', json_encode($this->normalize_message_list($this->input->post('sales_messages'))));
			redirect('configuracion/ventas');
		}

		$this->render('app/configuracion_ventas', array(
			'page_title' => 'Ventas',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'sales_daily_limit' => max(1, (int)$this->App_write_model->get_setting('sales_daily_limit', '3')),
			'sales_messages' => $this->read_sales_messages(),
		));
	}

	public function configuracion_empleados()
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.empleados.view'));
		$this->App_write_model->run_migration();
		$gala_employees = $this->Gala_read_model->get_employees_for_visor();
		$app_users = $this->App_write_model->get_all_app_users_index();
		$employees = array();

		foreach ($gala_employees as $employee)
		{
			$employee_id = (int)$employee['person_id'];
			$app_user = isset($app_users[$employee_id]) ? $app_users[$employee_id] : array();
			$employees[] = array(
				'employee_id' => $employee_id,
				'full_name' => isset($employee['full_name']) ? $employee['full_name'] : '',
				'is_active' => empty($app_user) ? TRUE : !isset($app_user['is_enabled']) || (int)$app_user['is_enabled'] === 1,
			);
		}

		$this->render('app/configuracion_empleados', array(
			'page_title' => 'Empleados',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'employees' => $employees,
		));
	}

	public function configuracion_empleado($employee_id)
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.empleados.view'));
		$this->App_write_model->run_migration();
		$employee_id = (int)$employee_id;
		$employee = $this->find_gala_employee($employee_id);
		if (!$employee)
		{
			show_404();
		}

		$current = $this->App_write_model->get_app_user($employee_id);
		$current_permissions = $current && !empty($current['permissions_json'])
			? $this->App_write_model->get_app_user_permissions($employee_id)
			: $this->get_default_permissions();
		$permissions = $this->build_permission_state($current_permissions);
		$is_enabled = !$current || !isset($current['is_enabled']) || (int)$current['is_enabled'] === 1;

		if ($this->input->method(TRUE) === 'POST')
		{
			$posted_permissions = $this->input->post('permissions');
			$permission_state = $this->build_permission_state($this->decode_permission_post(is_array($posted_permissions) ? $posted_permissions : array()));
			$auth_user = $this->get_auth_user();

			if ($auth_user && (int)$auth_user['employee_id'] === $employee_id)
			{
				$permission_state['configuracion.view'] = TRUE;
				$permission_state['configuracion.empleados.view'] = TRUE;
				$is_enabled = TRUE;
			}
			else
			{
				$is_enabled = $this->input->post('is_enabled') ? TRUE : FALSE;
			}

			$this->App_write_model->save_app_user_config($employee_id, array(
				'username' => isset($employee['username']) ? $employee['username'] : '',
				'full_name' => isset($employee['full_name']) ? $employee['full_name'] : '',
			), array(
				'is_enabled' => $is_enabled ? 1 : 0,
				'permissions' => $permission_state,
			));

			redirect('configuracion/empleados/'.$employee_id);
		}

		$this->render('app/configuracion_empleado', array(
			'page_title' => 'Empleado',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'employee' => $employee,
			'is_enabled' => $is_enabled,
			'permission_labels' => $this->get_permission_labels(),
			'permission_groups' => $this->get_permission_groups(),
			'permissions' => $permissions,
			'is_self' => $this->get_auth_user() && (int)$this->get_auth_user()['employee_id'] === $employee_id,
		));
	}

	private function require_permission($key)
	{
		if (!$this->is_authenticated())
		{
			$continue = current_url();
			if ($this->input->server('QUERY_STRING'))
			{
				$continue .= '?'.$this->input->server('QUERY_STRING');
			}
			redirect('login?continue='.rawurlencode($continue));
		}

		$this->ensure_active_authenticated_user();

		if (!$this->has_permission($key))
		{
			show_error('No tienes acceso a esta seccion de Visor.', 403, 'Acceso denegado');
		}
	}

	private function require_permissions($keys)
	{
		foreach ((array)$keys as $key)
		{
			$this->require_permission($key);
		}
	}

	private function has_permission($key)
	{
		$auth_user = $this->get_auth_user();
		if (!$auth_user)
		{
			return FALSE;
		}

		$app_user = $this->App_write_model->get_app_user((int)$auth_user['employee_id']);
		if ($app_user && !empty($app_user['is_admin']))
		{
			return TRUE;
		}

		$permissions = $this->App_write_model->get_app_user_permissions((int)$auth_user['employee_id']);
		$defaults = $this->get_default_permissions();
		if (array_key_exists($key, $permissions))
		{
			return !empty($permissions[$key]);
		}

		if (array_key_exists($key, $defaults))
		{
			return !empty($defaults[$key]);
		}

		foreach ($this->get_permission_fallback_chain($key) as $fallback_key)
		{
			if (array_key_exists($fallback_key, $permissions))
			{
				return !empty($permissions[$fallback_key]);
			}
			if (array_key_exists($fallback_key, $defaults))
			{
				return !empty($defaults[$fallback_key]);
			}
		}

		return FALSE;
	}

	private function get_default_permissions()
	{
		return array(
			'dashboard.view' => TRUE,
			'notificar.view' => TRUE,
			'tareas.view' => TRUE,
			'ventas.view' => TRUE,
			'ventas.rank.view' => TRUE,
			'metas.view' => TRUE,
			'metas.report.view' => TRUE,
			'personalizar.view' => TRUE,
			'personalizar.visor.view' => TRUE,
			'personalizar.aplicacion.view' => TRUE,
			'configuracion.view' => FALSE,
			'configuracion.descuentos.view' => FALSE,
			'configuracion.grupos.view' => FALSE,
			'configuracion.metas.view' => FALSE,
			'configuracion.tareas.view' => FALSE,
			'configuracion.notificaciones.view' => FALSE,
			'configuracion.ventas.view' => FALSE,
			'configuracion.empleados.view' => FALSE,
		);
	}

	private function get_permission_labels()
	{
		return array(
			'dashboard.view' => 'Puede ver Dashboard',
			'notificar.view' => 'Puede ver Notificar',
			'tareas.view' => 'Puede ver Tareas',
			'ventas.view' => 'Puede ver Ventas',
			'ventas.rank.view' => 'Ventas - Puede ver Posicion',
			'metas.view' => 'Puede ver Metas',
			'metas.report.view' => 'Metas - Puede ver boton Reporte',
			'personalizar.view' => 'Puede ver Personalizar',
			'personalizar.visor.view' => 'Personalizar - Visor',
			'personalizar.aplicacion.view' => 'Personalizar - Aplicacion',
			'configuracion.view' => 'Puede ver Configuracion',
			'configuracion.descuentos.view' => 'Configuracion - Descuentos',
			'configuracion.grupos.view' => 'Configuracion - Grupos',
			'configuracion.metas.view' => 'Configuracion - Metas',
			'configuracion.tareas.view' => 'Configuracion - Tareas',
			'configuracion.notificaciones.view' => 'Configuracion - Notificaciones',
			'configuracion.ventas.view' => 'Configuracion - Ventas',
			'configuracion.empleados.view' => 'Configuracion - Empleados',
		);
	}

	private function get_permission_groups()
	{
		return array(
			'General' => array(
				'dashboard.view',
				'notificar.view',
			),
			'Tareas' => array(
				'tareas.view',
			),
			'Ventas' => array(
				'ventas.view',
				'ventas.rank.view',
			),
			'Metas' => array(
				'metas.view',
				'metas.report.view',
			),
			'Personalizar' => array(
				'personalizar.view',
				'personalizar.visor.view',
				'personalizar.aplicacion.view',
			),
			'Configuracion' => array(
				'configuracion.view',
				'configuracion.descuentos.view',
				'configuracion.grupos.view',
				'configuracion.metas.view',
				'configuracion.tareas.view',
				'configuracion.notificaciones.view',
				'configuracion.ventas.view',
				'configuracion.empleados.view',
			),
		);
	}

	private function get_permission_fallback_chain($key)
	{
		$fallbacks = array(
			'ventas.rank.view' => array('ventas.view'),
			'metas.report.view' => array('metas.view'),
			'personalizar.visor.view' => array('personalizar.view'),
			'personalizar.aplicacion.view' => array('personalizar.view'),
			'configuracion.descuentos.view' => array('configuracion.view'),
			'configuracion.grupos.view' => array('configuracion.view'),
			'configuracion.metas.view' => array('configuracion.view'),
			'configuracion.tareas.view' => array('configuracion.view'),
			'configuracion.notificaciones.view' => array('configuracion.view'),
			'configuracion.ventas.view' => array('configuracion.view'),
			'configuracion.empleados.view' => array('configuracion.view'),
		);

		return isset($fallbacks[$key]) ? $fallbacks[$key] : array();
	}

	private function handle_goal_definition_form($goal_id = null)
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.metas.view'));
		$this->App_write_model->run_migration();
		$goal = $goal_id ? $this->App_write_model->get_goal_definition((int)$goal_id) : null;
		if ($goal_id && !$goal)
		{
			show_404();
		}
		if ($goal && !isset($goal['group_ids']))
		{
			$group_ids = $this->App_write_model->get_goal_definition_group_ids((int)$goal['id']);
			if (empty($group_ids) && !empty($goal['group_id']))
			{
				$group_ids = array((int)$goal['group_id']);
			}
			$goal['group_ids'] = $group_ids;
		}

		$options = $this->get_goal_form_options();
		$this->form_validation->set_rules('name', 'Nombre', 'trim|required');
		$this->form_validation->set_rules('period_type', 'Periodo', 'trim|required');
		$this->form_validation->set_rules('scope_type', 'Filtro', 'trim|required');
		$this->form_validation->set_rules('metric_type', 'Medicion', 'trim|required');
		$this->form_validation->set_rules('goal_method', 'Metodo', 'trim|required');
		$this->form_validation->set_rules('display_mode', 'Estilo', 'trim|required');
		$this->form_validation->set_rules('employee_mode', 'Aplicacion', 'trim|required');
		$this->form_validation->set_rules('goal_amount', 'Monto meta', 'trim|numeric');
		$this->form_validation->set_rules('goal_percent', 'Porcentaje', 'trim|numeric');
		$this->form_validation->set_rules('sort_order', 'Orden', 'trim|integer');
		$goal_error = '';

		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$scope_type = trim((string)$this->input->post('scope_type'));
			$scope_id = null;
			switch ($scope_type)
			{
				case 'category':
					$scope_id = $this->input->post('scope_category_id');
					break;
				case 'item':
					$scope_id = $this->input->post('scope_item_id');
					break;
				case 'manufacturer':
					$scope_id = $this->input->post('scope_manufacturer_id');
					break;
				case 'supplier':
					$scope_id = $this->input->post('scope_supplier_id');
					break;
				default:
					$scope_id = null;
					break;
			}

			$run_once = $this->input->post('run_once') ? 1 : 0;
			$run_once_mode = $run_once ? trim((string)$this->input->post('run_once_mode')) : null;
			$custom_period_start_date = null;
			$custom_period_end_date = null;
			if ($this->input->post('period_type') === 'custom')
			{
				$custom_period_start_date = trim((string)$this->input->post('custom_period_start_date'));
				$custom_period_end_date = trim((string)$this->input->post('custom_period_end_date'));
				if ($custom_period_start_date === '' || $custom_period_end_date === '')
				{
					$goal_error = 'Debes indicar fecha inicial y fecha final para el periodo personalizado.';
				}
				elseif ($custom_period_end_date < $custom_period_start_date)
				{
					$goal_error = 'La fecha final del periodo no puede ser menor que la fecha inicial.';
				}
				if ($run_once && $run_once_mode === 'period')
				{
					$run_once_mode = 'custom';
				}
			}
			$run_once_start_date = null;
			$run_once_end_date = null;
			if ($run_once && $run_once_mode === 'custom')
			{
				$run_once_start_date = $this->input->post('period_type') === 'custom'
					? $custom_period_start_date
					: trim((string)$this->input->post('run_once_start_date'));
				$run_once_end_date = $this->input->post('period_type') === 'custom'
					? $custom_period_end_date
					: trim((string)$this->input->post('run_once_end_date'));
				if ($run_once_start_date === '' || $run_once_end_date === '')
				{
					$goal_error = 'Debes indicar fecha inicial y fecha final para la duracion personalizada.';
				}
				elseif ($run_once_end_date < $run_once_start_date)
				{
					$goal_error = 'La fecha final no puede ser menor que la fecha inicial.';
				}
			}

 			$employee_mode = trim((string)$this->input->post('employee_mode'));
			$selected_group_ids = array_values(array_filter(array_map('intval', (array)$this->input->post('group_ids')), function ($group_id) {
				return $group_id > 0;
			}));
			if ($goal_error === '' && $employee_mode === 'group' && empty($selected_group_ids))
			{
				$goal_error = 'Debes seleccionar al menos un grupo para esta meta.';
			}
			if ($goal_error === '')
			{
				$payload = array(
					'name' => $this->input->post('name'),
					'is_active' => $this->input->post('is_active') ? 1 : 0,
					'period_type' => $this->input->post('period_type'),
					'custom_period_start_date' => $custom_period_start_date,
					'custom_period_end_date' => $custom_period_end_date,
					'scope_type' => $scope_type,
					'scope_id' => $scope_id,
					'metric_type' => $this->input->post('metric_type'),
					'goal_method' => $this->input->post('goal_method'),
					'goal_amount' => (float)$this->input->post('goal_amount'),
					'goal_percent' => (float)$this->input->post('goal_percent'),
					'display_mode' => $this->input->post('display_mode'),
					'employee_mode' => $employee_mode,
					'employee_id' => $employee_mode === 'employee' ? (int)$this->input->post('employee_id') : null,
					'group_id' => $employee_mode === 'group' && !empty($selected_group_ids) ? (int)$selected_group_ids[0] : null,
					'target_scope' => $employee_mode === 'group' ? trim((string)$this->input->post('target_scope')) : 'individual',
					'sort_order' => (int)$this->input->post('sort_order'),
					'run_once' => $run_once,
					'run_once_mode' => $run_once_mode,
					'run_once_start_date' => $run_once_start_date,
					'run_once_end_date' => $run_once_end_date,
				);

				$saved_goal_id = $this->App_write_model->save_goal_definition($payload, $goal_id);
				if ($saved_goal_id && $employee_mode === 'group')
				{
					$this->App_write_model->replace_goal_definition_groups($saved_goal_id, $selected_group_ids);
				}
				elseif ($saved_goal_id)
				{
					$this->App_write_model->replace_goal_definition_groups($saved_goal_id, array());
				}
				redirect('configuracion/metas');
			}
		}

		$this->render('app/configuracion_meta_form', array(
			'page_title' => $goal ? 'Editar meta' : 'Nueva meta',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'goal' => $goal,
			'goal_options' => $options,
			'goal_error' => $goal_error,
		));
	}

	private function get_goal_form_options()
	{
		$categories = $this->Gala_read_model->get_goal_categories();
		$items = $this->Gala_read_model->get_goal_items();
		$manufacturers = $this->Gala_read_model->get_goal_manufacturers();
		$suppliers = $this->Gala_read_model->get_goal_suppliers();
		$employees = $this->Gala_read_model->get_employees_for_visor();

		return array(
			'period_types' => array(
				'daily' => 'Diaria',
				'weekly' => 'Semanal',
				'biweekly' => 'Quincenal',
				'monthly' => 'Mensual',
				'custom' => 'Personalizado',
			),
			'scope_types' => array(
				'general' => 'Monto general',
				'category' => 'Categoria',
				'item' => 'Articulo',
				'manufacturer' => 'Fabricante',
				'supplier' => 'Proveedor',
			),
			'metric_types' => array(
				'amount' => 'Monto vendido',
				'quantity' => 'Cantidad vendida',
			),
			'goal_methods' => array(
				'fixed' => 'Monto fijo',
				'last_year_same_period' => 'Ano pasado',
				'last_year_same_period_plus_percent' => 'Ano pasado + porcentaje',
				'competitive_leader' => 'Competitivo contra lider',
				'competitive_target' => 'Competitivo por monto objetivo',
			),
			'display_modes' => array(
				'bar' => 'Barra',
				'number' => 'Numero',
				'rank' => 'Puesto',
			),
			'employee_modes' => array(
				'global' => 'Todos',
				'employee' => 'Empleado especifico',
				'group' => 'Grupo',
			),
			'target_scopes' => array(
				'individual' => 'Meta individual del grupo',
				'group_total' => 'Total del grupo',
			),
			'run_once_modes' => array(
				'period' => 'Solo este periodo',
				'custom' => 'Personalizado',
			),
			'categories' => $categories,
			'items' => $items,
			'manufacturers' => $manufacturers,
			'suppliers' => $suppliers,
			'employees' => $employees,
			'groups' => $this->App_write_model->get_employee_groups(TRUE),
		);
	}

	private function decorate_goal_definitions($definitions)
	{
		$period_labels = array(
			'daily' => 'Diaria',
			'weekly' => 'Semanal',
			'biweekly' => 'Quincenal',
			'monthly' => 'Mensual',
			'custom' => 'Personalizado',
		);
		$scope_labels = array(
			'general' => 'General',
			'category' => 'Categoria',
			'item' => 'Articulo',
			'manufacturer' => 'Fabricante',
			'supplier' => 'Proveedor',
		);
		$metric_labels = array(
			'amount' => 'Monto vendido',
			'quantity' => 'Cantidad vendida',
		);
		$method_labels = array(
			'fixed' => 'Monto fijo',
			'last_year_same_period' => 'Ano pasado',
			'last_year_same_period_plus_percent' => 'Ano pasado + porcentaje',
			'competitive_leader' => 'Competitivo contra lider',
			'competitive_target' => 'Competitivo por monto objetivo',
		);
		$display_labels = array(
			'bar' => 'Barra',
			'number' => 'Numero',
			'rank' => 'Puesto',
		);

		$categories = $this->index_option_rows($this->Gala_read_model->get_goal_categories(), 'id');
		$items = $this->index_option_rows($this->Gala_read_model->get_goal_items(), 'item_id');
		$manufacturers = $this->index_option_rows($this->Gala_read_model->get_goal_manufacturers(), 'id');
		$suppliers = $this->index_option_rows($this->Gala_read_model->get_goal_suppliers(), 'id');
		$employees = $this->index_option_rows($this->Gala_read_model->get_employees_for_visor(), 'person_id');
		$groups = $this->index_option_rows($this->App_write_model->get_employee_groups(), 'id');
		$goal_group_ids_index = $this->App_write_model->get_goal_definition_groups_index();

		foreach ($definitions as &$definition)
		{
			$definition['group_ids'] = isset($goal_group_ids_index[(int)$definition['id']]) ? $goal_group_ids_index[(int)$definition['id']] : array();
			if (empty($definition['group_ids']) && !empty($definition['group_id']))
			{
				$definition['group_ids'] = array((int)$definition['group_id']);
			}
			$definition['period_label'] = isset($period_labels[$definition['period_type']]) ? $period_labels[$definition['period_type']] : $definition['period_type'];
			$definition['scope_label'] = isset($scope_labels[$definition['scope_type']]) ? $scope_labels[$definition['scope_type']] : $definition['scope_type'];
			$definition['metric_label'] = isset($metric_labels[$definition['metric_type']]) ? $metric_labels[$definition['metric_type']] : $definition['metric_type'];
			$definition['method_label'] = isset($method_labels[$definition['goal_method']]) ? $method_labels[$definition['goal_method']] : $definition['goal_method'];
			$definition['display_label'] = isset($display_labels[$definition['display_mode']]) ? $display_labels[$definition['display_mode']] : $definition['display_mode'];
			$definition['scope_value_label'] = $this->resolve_goal_scope_value_label($definition, $categories, $items, $manufacturers, $suppliers);
			if (!empty($definition['employee_mode']) && $definition['employee_mode'] === 'employee' && !empty($definition['employee_id']) && isset($employees[(int)$definition['employee_id']]))
			{
				$definition['employee_label'] = $employees[(int)$definition['employee_id']]['full_name'];
			}
			elseif (!empty($definition['employee_mode']) && $definition['employee_mode'] === 'group' && !empty($definition['group_ids']))
			{
				$group_names = array();
				$group_color = null;
				foreach ($definition['group_ids'] as $selected_group_id)
				{
					$selected_group_id = (int)$selected_group_id;
					if (!isset($groups[$selected_group_id]))
					{
						continue;
					}
					$group_names[] = $groups[$selected_group_id]['name'];
					if ($group_color === null && !empty($groups[$selected_group_id]['color']))
					{
						$group_color = $groups[$selected_group_id]['color'];
					}
				}
				$definition['employee_label'] = 'Grupos: '.(!empty($group_names) ? implode(', ', $group_names) : 'Sin grupos');
				$definition['group_color'] = $group_color ?: '#4d74c3';
			}
			else
			{
				$definition['employee_label'] = 'Todos';
				$definition['group_color'] = null;
			}
			$definition['run_once_label'] = empty($definition['run_once'])
				? 'Repetible'
				: (!empty($definition['run_once_mode']) && $definition['run_once_mode'] === 'custom'
					? 'Una vez: '.(!empty($definition['run_once_start_date']) ? $definition['run_once_start_date'] : '?').' al '.(!empty($definition['run_once_end_date']) ? $definition['run_once_end_date'] : '?')
					: 'Una vez: solo este periodo');
			if (isset($definition['period_type']) && $definition['period_type'] === 'custom' && !empty($definition['custom_period_start_date']) && !empty($definition['custom_period_end_date']))
			{
				$definition['period_label'] .= ' ('.$definition['custom_period_start_date'].' al '.$definition['custom_period_end_date'].')';
			}
		}
		unset($definition);

		return $definitions;
	}

	private function resolve_goal_scope_value_label($definition, $categories, $items, $manufacturers, $suppliers)
	{
		$scope_type = isset($definition['scope_type']) ? $definition['scope_type'] : 'general';
		$scope_id = isset($definition['scope_id']) ? (int)$definition['scope_id'] : 0;
		if ($scope_type === 'general' || $scope_id <= 0)
		{
			return 'Todo';
		}

		switch ($scope_type)
		{
			case 'category':
				return isset($categories[$scope_id]['name']) ? $categories[$scope_id]['name'] : 'Categoria';
			case 'item':
				return isset($items[$scope_id]['name']) ? $items[$scope_id]['name'] : 'Articulo';
			case 'manufacturer':
				return isset($manufacturers[$scope_id]['name']) ? $manufacturers[$scope_id]['name'] : 'Fabricante';
			case 'supplier':
				return isset($suppliers[$scope_id]['name']) ? $suppliers[$scope_id]['name'] : 'Proveedor';
			default:
				return 'Todo';
		}
	}

	private function index_option_rows($rows, $key)
	{
		$indexed = array();
		foreach ($rows as $row)
		{
			if (!isset($row[$key]))
			{
				continue;
			}
			$indexed[(int)$row[$key]] = $row;
		}

		return $indexed;
	}

	private function evaluate_goal_cards_for_employee($employee_id, $reference_date = null, $snapshot_index = null)
	{
		$definitions = $this->get_active_goal_definitions_cached();
		$cards = array();
		foreach ($definitions as $definition)
		{
			if (!$this->goal_applies_to_employee($definition, $employee_id) || !$this->goal_is_currently_available($definition, $reference_date))
			{
				continue;
			}

			$card = null;
			if ($snapshot_index !== null && $reference_date !== null)
			{
				list($date_start, $date_end) = $this->get_goal_period_range(isset($definition['period_type']) ? $definition['period_type'] : 'daily', $definition, $reference_date);
				$snapshot_key = $this->build_goal_snapshot_key((int)$definition['id'], $date_start, $date_end);
				if (isset($snapshot_index[$snapshot_key]))
				{
					$card = $this->build_goal_card_from_snapshot($definition, $snapshot_index[$snapshot_key], $employee_id, $reference_date, $date_start, $date_end);
				}
			}

			if ($card === null)
			{
				$card = $this->evaluate_single_goal_definition($definition, $employee_id, $reference_date);
			}
			if ($card)
			{
				$cards[] = $card;
			}
		}

		return $cards;
	}

	private function goal_is_currently_available($definition, $reference_date = null)
	{
		if (empty($definition['run_once']) || empty($definition['run_once_mode']) || $definition['run_once_mode'] !== 'custom')
		{
			return TRUE;
		}

		$today = $reference_date ? date('Y-m-d', strtotime($reference_date)) : date('Y-m-d');
		$start_date = !empty($definition['run_once_start_date']) ? (string)$definition['run_once_start_date'] : '';
		$end_date = !empty($definition['run_once_end_date']) ? (string)$definition['run_once_end_date'] : '';

		if ($start_date !== '' && $today < $start_date)
		{
			return FALSE;
		}

		if ($end_date !== '' && $today > $end_date)
		{
			return FALSE;
		}

		return TRUE;
	}

	private function build_goal_snapshot_key($goal_id, $period_start, $period_end)
	{
		return implode('|', array((int)$goal_id, (string)$period_start, (string)$period_end));
	}

	private function auto_snapshot_tick()
	{
		if ($this->input->is_cli_request())
		{
			return;
		}

		$this->App_write_model->run_migration();
		$route_method = $this->router->method;
		if (in_array($route_method, array('visor', 'login', 'logout'), TRUE))
		{
			return;
		}
		if (!$this->is_authenticated())
		{
			return;
		}
		$enabled = (string)$this->App_write_model->get_setting('goal_snapshot_auto_enabled', '1');
		if ($enabled !== '1')
		{
			return;
		}

		$now = time();
		$last_run_at = (int)$this->App_write_model->get_setting('goal_snapshot_last_run_at', 0);
		if ($last_run_at > 0 && ($now - $last_run_at) < 1800)
		{
			return;
		}

		$lock_until = (int)$this->App_write_model->get_setting('goal_snapshot_lock_until', 0);
		if ($lock_until > $now)
		{
			return;
		}
		$this->App_write_model->save_setting('goal_snapshot_lock_until', $now + 120);

		$timezone = new DateTimeZone('America/Caracas');
		$today = new DateTime('now', $timezone);
		$today_key = $today->format('Y-m-d');
		$last_date = (string)$this->App_write_model->get_setting('goal_snapshot_last_date', '');
		$offset = (int)$this->App_write_model->get_setting('goal_snapshot_last_offset', 0);

		if ($last_date === $today_key && $offset === 0)
		{
			return;
		}

		$employees = $this->Gala_read_model->get_employees_for_visor();
		if (empty($employees))
		{
			$this->App_write_model->save_setting('goal_snapshot_last_date', $today_key);
			$this->App_write_model->save_setting('goal_snapshot_last_offset', 0);
			$this->App_write_model->save_setting('goal_snapshot_last_run_at', $now);
			$this->App_write_model->save_setting('goal_snapshot_lock_until', 0);
			return;
		}

		if ($last_date !== $today_key)
		{
			$offset = 0;
		}

		$batch_size = 2;
		$batch = array_slice($employees, $offset, $batch_size);
		if (empty($batch))
		{
			$this->App_write_model->save_setting('goal_snapshot_last_date', $today_key);
			$this->App_write_model->save_setting('goal_snapshot_last_offset', 0);
			$this->App_write_model->save_setting('goal_snapshot_last_run_at', $now);
			$this->App_write_model->save_setting('goal_snapshot_lock_until', 0);
			return;
		}

		$month_start = new DateTime($today->format('Y-m-01'), $timezone);
		$month_end = new DateTime($today->format('Y-m-t'), $timezone);

		foreach ($batch as $employee)
		{
			$employee_id = isset($employee['person_id']) ? (int)$employee['person_id'] : 0;
			if ($employee_id <= 0)
			{
				continue;
			}
			$this->ensure_employee_goal_snapshots($employee_id, $month_start, $month_end, $today, TRUE);
		}

		$offset += count($batch);
		if ($offset >= count($employees))
		{
			$this->App_write_model->save_setting('goal_snapshot_last_date', $today_key);
			$this->App_write_model->save_setting('goal_snapshot_last_offset', 0);
		}
		else
		{
			$this->App_write_model->save_setting('goal_snapshot_last_offset', $offset);
		}

		$this->App_write_model->save_setting('goal_snapshot_last_run_at', $now);
		$this->App_write_model->save_setting('goal_snapshot_lock_until', 0);
	}

	private function auto_task_assignment_tick()
	{
		if ($this->input->is_cli_request())
		{
			return;
		}

		$route_method = $this->router->method;
		if (in_array($route_method, array('visor', 'login', 'logout'), TRUE))
		{
			return;
		}

		if (!$this->is_authenticated())
		{
			return;
		}

		$this->Tasks_model->run_migration();
		$enabled = (string)$this->App_write_model->get_setting('task_auto_enabled', '1');
		if ($enabled !== '1')
		{
			return;
		}

		$now = time();
		$last_run_at = (int)$this->App_write_model->get_setting('task_last_run_at', 0);
		if ($last_run_at > 0 && ($now - $last_run_at) < 300)
		{
			return;
		}

		$lock_until = (int)$this->App_write_model->get_setting('task_lock_until', 0);
		if ($lock_until > $now)
		{
			return;
		}

		$this->App_write_model->save_setting('task_lock_until', $now + 90);
		$this->Tasks_model->sync_due_assignments();
		$this->App_write_model->save_setting('task_last_run_at', $now);
		$this->App_write_model->save_setting('task_lock_until', 0);
	}

	private function build_goal_card_from_snapshot($definition, $snapshot, $employee_id, $reference_date, $date_start, $date_end)
	{
		$selected_group_ids = $this->get_goal_selected_group_ids($definition);
		$employee_group_id = !empty($selected_group_ids) ? $this->resolve_employee_matching_group_id($employee_id, $selected_group_ids) : 0;
		$is_group_total = !empty($definition['employee_mode']) && $definition['employee_mode'] === 'group'
			&& !empty($employee_group_id) && isset($definition['target_scope']) && $definition['target_scope'] === 'group_total';

		return array(
			'id' => (int)$definition['id'],
			'name' => isset($definition['name']) ? $definition['name'] : 'Meta',
			'display_mode' => isset($definition['display_mode']) ? $definition['display_mode'] : 'bar',
			'current_value' => isset($snapshot['current_value']) ? (float)$snapshot['current_value'] : 0.0,
			'target_value' => isset($snapshot['target_value']) ? (float)$snapshot['target_value'] : 0.0,
			'progress_ratio' => isset($snapshot['progress_ratio']) ? (float)$snapshot['progress_ratio'] : 0.0,
			'is_completed' => !empty($snapshot['is_completed']),
			'rank_position' => isset($snapshot['rank_position']) ? (int)$snapshot['rank_position'] : 0,
			'metric_label' => isset($definition['metric_type']) && $definition['metric_type'] === 'quantity' ? 'Cantidad' : 'Monto',
			'target_label' => $this->build_goal_target_label($definition),
			'group_color' => isset($definition['group_color']) ? $definition['group_color'] : null,
			'group_total_label' => $is_group_total ? $this->build_group_total_label($definition, $selected_group_ids) : '',
			'group_segments' => array(),
			'current_group_id' => $employee_group_id,
		);
	}

	private function goal_applies_to_employee($definition, $employee_id)
	{
		if (empty($definition['employee_mode']) || $definition['employee_mode'] === 'global')
		{
			return TRUE;
		}

		if ($definition['employee_mode'] === 'group')
		{
			$group_ids = $this->get_goal_selected_group_ids($definition);
			$group_members_index = $this->get_employee_group_members_index_cached();
			if (empty($group_ids))
			{
				return FALSE;
			}
			foreach ($group_ids as $group_id)
			{
				$members = isset($group_members_index[(int)$group_id]) ? $group_members_index[(int)$group_id] : array();
				if (in_array((int)$employee_id, $members, TRUE))
				{
					return TRUE;
				}
			}
			return FALSE;
		}

		return (int)$definition['employee_id'] === (int)$employee_id;
	}

	private function handle_employee_group_form($group_id = null)
	{
		$this->require_permissions(array('configuracion.view', 'configuracion.grupos.view'));
		$this->App_write_model->run_migration();
		$group = $group_id ? $this->App_write_model->get_employee_group((int)$group_id) : null;
		if ($group_id && !$group)
		{
			show_404();
		}

		$employees = $this->Gala_read_model->get_employees_for_visor();
		$selected_members = $group ? $this->App_write_model->get_employee_group_members((int)$group['id']) : array();

		$this->form_validation->set_rules('name', 'Nombre', 'trim|required');
		$this->form_validation->set_rules('sort_order', 'Orden', 'trim|integer');

		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$saved_group_id = $this->App_write_model->save_employee_group(array(
				'name' => $this->input->post('name'),
				'is_active' => $this->input->post('is_active') ? 1 : 0,
				'color' => $this->input->post('color'),
				'sort_order' => (int)$this->input->post('sort_order'),
			), $group_id);
			$this->App_write_model->replace_employee_group_members($saved_group_id, $this->input->post('members'));
			redirect('configuracion/grupos/'.$saved_group_id);
		}

		$this->render('app/configuracion_grupo_form', array(
			'page_title' => $group ? 'Editar grupo' : 'Nuevo grupo',
			'current_module' => 'configuracion',
			'hide_main_menu' => TRUE,
			'group' => $group,
			'employees' => $employees,
			'selected_members' => $selected_members,
		));
	}

	private function evaluate_single_goal_definition($definition, $employee_id, $reference_date = null)
	{
		list($date_start, $date_end) = $this->get_goal_period_range(isset($definition['period_type']) ? $definition['period_type'] : 'daily', $definition, $reference_date);
		$selected_group_ids = $this->get_goal_selected_group_ids($definition);
		$employee_group_id = !empty($selected_group_ids) ? $this->resolve_employee_matching_group_id($employee_id, $selected_group_ids) : 0;
		$is_group_total = !empty($definition['employee_mode']) && $definition['employee_mode'] === 'group' && !empty($employee_group_id) && isset($definition['target_scope']) && $definition['target_scope'] === 'group_total';
		if ($is_group_total)
		{
			$current_value = $this->get_group_goal_metric_value_cached($employee_group_id, $definition, $date_start, $date_end);
			$target_value = $this->resolve_goal_target_value($definition, $employee_id, $date_start, $date_end, $reference_date);
			$ranking_rows = ($this->needs_goal_ranking($definition) || (isset($definition['display_mode']) && $definition['display_mode'] === 'bar'))
				? $this->get_group_goal_metric_ranking_cached($definition, $date_start, $date_end, $selected_group_ids)
				: array();
			$rank_position = $this->resolve_group_rank_position($ranking_rows, $employee_group_id);
		}
		else
		{
			$current_value = $this->get_goal_metric_for_employee_cached(
				$employee_id,
				isset($definition['scope_type']) ? $definition['scope_type'] : 'general',
				isset($definition['scope_id']) ? $definition['scope_id'] : null,
				isset($definition['metric_type']) ? $definition['metric_type'] : 'amount',
				$date_start,
				$date_end
			);

			$target_value = $this->resolve_goal_target_value($definition, $employee_id, $date_start, $date_end, $reference_date);
			$ranking_rows = $this->needs_goal_ranking($definition) ? $this->get_goal_metric_ranking_cached(
				isset($definition['scope_type']) ? $definition['scope_type'] : 'general',
				isset($definition['scope_id']) ? $definition['scope_id'] : null,
				isset($definition['metric_type']) ? $definition['metric_type'] : 'amount',
				$date_start,
				$date_end
			) : array();
			$rank_position = $this->resolve_employee_rank_position($ranking_rows, $employee_id);
		}
		$progress_ratio = $target_value > 0 ? min(1, max(0, $current_value / $target_value)) : 0;

		return array(
			'id' => (int)$definition['id'],
			'name' => isset($definition['name']) ? $definition['name'] : 'Meta',
			'display_mode' => isset($definition['display_mode']) ? $definition['display_mode'] : 'bar',
			'current_value' => (float)$current_value,
			'target_value' => (float)$target_value,
			'progress_ratio' => $progress_ratio,
			'is_completed' => $target_value > 0 && $current_value >= $target_value,
			'rank_position' => $rank_position,
			'metric_label' => isset($definition['metric_type']) && $definition['metric_type'] === 'quantity' ? 'Cantidad' : 'Monto',
			'target_label' => $this->build_goal_target_label($definition),
			'group_color' => isset($definition['group_color']) ? $definition['group_color'] : null,
			'group_total_label' => $is_group_total ? $this->build_group_total_label($definition, $selected_group_ids) : '',
			'group_segments' => $is_group_total && isset($definition['display_mode']) && $definition['display_mode'] === 'bar'
				? $this->build_group_bar_segments($definition, $selected_group_ids, $ranking_rows, $target_value)
				: array(),
			'current_group_id' => $employee_group_id,
		);
	}

	private function needs_goal_ranking($definition)
	{
		$method = isset($definition['goal_method']) ? $definition['goal_method'] : 'fixed';
		$display = isset($definition['display_mode']) ? $definition['display_mode'] : 'bar';
		return in_array($method, array('competitive_leader', 'competitive_target'), TRUE) || $display === 'rank';
	}

	private function resolve_employee_rank_position($ranking_rows, $employee_id)
	{
		$position = 0;
		$display_position = 0;
		$previous_value = null;
		foreach ($ranking_rows as $index => $row)
		{
			$value = isset($row['metric_value']) ? (float)$row['metric_value'] : 0.0;
			if ($previous_value === null || abs($value - $previous_value) > 0.0001)
			{
				$display_position = $index + 1;
				$previous_value = $value;
			}
			if ((int)$row['seller_id'] === (int)$employee_id)
			{
				$position = $display_position;
				break;
			}
		}

		return $position > 0 ? $position : 1;
	}

	private function resolve_group_rank_position($ranking_rows, $group_id)
	{
		$position = 0;
		$display_position = 0;
		$previous_value = null;
		foreach ($ranking_rows as $index => $row)
		{
			$value = isset($row['metric_value']) ? (float)$row['metric_value'] : 0.0;
			if ($previous_value === null || abs($value - $previous_value) > 0.0001)
			{
				$display_position = $index + 1;
				$previous_value = $value;
			}
			if ((int)$row['group_id'] === (int)$group_id)
			{
				$position = $display_position;
				break;
			}
		}

		return $position > 0 ? $position : 1;
	}

	private function resolve_goal_target_value($definition, $employee_id, $date_start, $date_end, $reference_date = null)
	{
		$method = isset($definition['goal_method']) ? $definition['goal_method'] : 'fixed';
		$selected_group_ids = $this->get_goal_selected_group_ids($definition);
		$employee_group_id = !empty($selected_group_ids) ? $this->resolve_employee_matching_group_id($employee_id, $selected_group_ids) : 0;
		$is_group_total = !empty($definition['employee_mode']) && $definition['employee_mode'] === 'group' && !empty($employee_group_id) && isset($definition['target_scope']) && $definition['target_scope'] === 'group_total';
		switch ($method)
		{
			case 'last_year_same_period':
				list($history_start, $history_end) = $this->get_goal_historical_period_range(isset($definition['period_type']) ? $definition['period_type'] : 'daily', $definition, $reference_date);
				if ($is_group_total)
				{
					return $this->get_group_goal_metric_value_cached($employee_group_id, $definition, $history_start, $history_end);
				}
				return $this->get_goal_metric_for_employee_cached($employee_id, isset($definition['scope_type']) ? $definition['scope_type'] : 'general', isset($definition['scope_id']) ? $definition['scope_id'] : null, isset($definition['metric_type']) ? $definition['metric_type'] : 'amount', $history_start, $history_end);
			case 'last_year_same_period_plus_percent':
				list($history_start, $history_end) = $this->get_goal_historical_period_range(isset($definition['period_type']) ? $definition['period_type'] : 'daily', $definition, $reference_date);
				$history_value = $is_group_total
					? $this->get_group_goal_metric_value_cached($employee_group_id, $definition, $history_start, $history_end)
					: $this->get_goal_metric_for_employee_cached($employee_id, isset($definition['scope_type']) ? $definition['scope_type'] : 'general', isset($definition['scope_id']) ? $definition['scope_id'] : null, isset($definition['metric_type']) ? $definition['metric_type'] : 'amount', $history_start, $history_end);
				$percent = isset($definition['goal_percent']) ? (float)$definition['goal_percent'] : 0.0;
				return $history_value + ($history_value * ($percent / 100));
			case 'competitive_leader':
				$ranking_rows = $is_group_total
					? $this->get_group_goal_metric_ranking_cached($definition, $date_start, $date_end, $selected_group_ids)
					: $this->get_goal_metric_ranking_cached(isset($definition['scope_type']) ? $definition['scope_type'] : 'general', isset($definition['scope_id']) ? $definition['scope_id'] : null, isset($definition['metric_type']) ? $definition['metric_type'] : 'amount', $date_start, $date_end);
				return !empty($ranking_rows) && isset($ranking_rows[0]['metric_value']) ? (float)$ranking_rows[0]['metric_value'] : 0.0;
			case 'competitive_target':
			case 'fixed':
			default:
				return isset($definition['goal_amount']) ? (float)$definition['goal_amount'] : 0.0;
		}
	}

	private function get_group_goal_metric_value($group_id, $definition, $date_start, $date_end)
	{
		$group_id = (int)$group_id;
		if ($group_id <= 0)
		{
			return 0.0;
		}

		$members = $this->App_write_model->get_employee_group_members($group_id);
		$total = 0.0;
		foreach ($members as $member_id)
		{
			$total += $this->Gala_read_model->get_goal_metric_for_employee(
				(int)$member_id,
				isset($definition['scope_type']) ? $definition['scope_type'] : 'general',
				isset($definition['scope_id']) ? $definition['scope_id'] : null,
				isset($definition['metric_type']) ? $definition['metric_type'] : 'amount',
				$date_start,
				$date_end
			);
		}

		return (float)$total;
	}

	private function get_active_goal_definitions_cached()
	{
		if ($this->activeGoalDefinitionsCache === null)
		{
			$this->activeGoalDefinitionsCache = $this->App_write_model->get_goal_definitions(TRUE);
		}

		return $this->activeGoalDefinitionsCache;
	}

	private function get_employee_group_members_index_cached()
	{
		if ($this->employeeGroupMembersIndexCache === null)
		{
			$this->employeeGroupMembersIndexCache = $this->App_write_model->get_employee_group_members_index();
		}

		return $this->employeeGroupMembersIndexCache;
	}

	private function get_goal_metric_for_employee_cached($employee_id, $scope_type, $scope_id, $metric_type, $date_start, $date_end)
	{
		$key = implode('|', array((int)$employee_id, (string)$scope_type, (string)$scope_id, (string)$metric_type, (string)$date_start, (string)$date_end));
		if (!array_key_exists($key, $this->goalMetricEmployeeCache))
		{
			$daily_value = $this->get_daily_cached_metric_value($employee_id, $scope_type, $scope_id, $metric_type, $date_start, $date_end);
			if ($daily_value !== null)
			{
				$this->goalMetricEmployeeCache[$key] = $daily_value;
				return $this->goalMetricEmployeeCache[$key];
			}

			$all_totals = $this->get_goal_metric_all_employees_cached($scope_type, $scope_id, $metric_type, $date_start, $date_end);
			$employee_id = (int)$employee_id;
			$this->goalMetricEmployeeCache[$key] = isset($all_totals[$employee_id]) ? (float)$all_totals[$employee_id] : 0.0;
		}

		return $this->goalMetricEmployeeCache[$key];
	}

	private function get_goal_metric_all_employees_cached($scope_type, $scope_id, $metric_type, $date_start, $date_end)
	{
		$key = implode('|', array((string)$scope_type, (string)$scope_id, (string)$metric_type, (string)$date_start, (string)$date_end));
		if (!array_key_exists($key, $this->goalMetricAllEmployeesCache))
		{
			$this->goalMetricAllEmployeesCache[$key] = $this->Gala_read_model->get_goal_metric_totals_by_employee($scope_type, $scope_id, $metric_type, $date_start, $date_end);
		}

		return $this->goalMetricAllEmployeesCache[$key];
	}

	private function prime_daily_goal_metric_cache($employee_id, DateTime $month_start, DateTime $month_end)
	{
		$definitions = $this->get_active_goal_definitions_cached();
		if (empty($definitions))
		{
			return;
		}

		$month_key_start = $month_start->format('Y-m-d 00:00:00');
		$month_key_end = $month_end->format('Y-m-d 23:59:59');

		foreach ($definitions as $definition)
		{
			if (isset($definition['period_type']) && $definition['period_type'] !== 'daily')
			{
				continue;
			}
			if (!$this->goal_applies_to_employee($definition, $employee_id))
			{
				continue;
			}

			$scope_type = isset($definition['scope_type']) ? $definition['scope_type'] : 'general';
			$scope_id = isset($definition['scope_id']) ? $definition['scope_id'] : null;
			$metric_type = isset($definition['metric_type']) ? $definition['metric_type'] : 'amount';
			$cache_key = $this->build_daily_metric_cache_key($employee_id, $scope_type, $scope_id, $metric_type, $month_key_start, $month_key_end);
			if (isset($this->goalMetricDailyEmployeeCache[$cache_key]))
			{
				continue;
			}

			$this->goalMetricDailyEmployeeCache[$cache_key] = $this->Gala_read_model->get_goal_metric_daily_totals_for_employee(
				(int)$employee_id,
				$scope_type,
				$scope_id,
				$metric_type,
				$month_key_start,
				$month_key_end
			);
		}
	}

	private function get_daily_cached_metric_value($employee_id, $scope_type, $scope_id, $metric_type, $date_start, $date_end)
	{
		$start_date = substr((string)$date_start, 0, 10);
		$end_date = substr((string)$date_end, 0, 10);
		if ($start_date === '' || $start_date !== $end_date)
		{
			return null;
		}

		$month_start = $start_date.' 00:00:00';
		$month_end = date('Y-m-t 23:59:59', strtotime($start_date));
		$cache_key = $this->build_daily_metric_cache_key($employee_id, $scope_type, $scope_id, $metric_type, $month_start, $month_end);
		if (!isset($this->goalMetricDailyEmployeeCache[$cache_key]))
		{
			return null;
		}

		$day_index = $this->goalMetricDailyEmployeeCache[$cache_key];
		return isset($day_index[$start_date]) ? (float)$day_index[$start_date] : 0.0;
	}

	private function build_daily_metric_cache_key($employee_id, $scope_type, $scope_id, $metric_type, $month_start, $month_end)
	{
		return implode('|', array((int)$employee_id, (string)$scope_type, (string)$scope_id, (string)$metric_type, (string)$month_start, (string)$month_end));
	}

	private function get_goal_metric_ranking_cached($scope_type, $scope_id, $metric_type, $date_start, $date_end)
	{
		$key = implode('|', array((string)$scope_type, (string)$scope_id, (string)$metric_type, (string)$date_start, (string)$date_end));
		if (!array_key_exists($key, $this->goalMetricRankingCache))
		{
			$this->goalMetricRankingCache[$key] = $this->Gala_read_model->get_goal_metric_ranking($scope_type, $scope_id, $metric_type, $date_start, $date_end);
		}

		return $this->goalMetricRankingCache[$key];
	}

	private function get_group_goal_metric_value_cached($group_id, $definition, $date_start, $date_end)
	{
		$key = implode('|', array((int)$group_id, (int)$definition['id'], (string)$date_start, (string)$date_end));
		if (!array_key_exists($key, $this->groupGoalMetricCache))
		{
			$this->groupGoalMetricCache[$key] = $this->get_group_goal_metric_value($group_id, $definition, $date_start, $date_end);
		}

		return $this->groupGoalMetricCache[$key];
	}

	private function get_group_goal_metric_ranking_cached($definition, $date_start, $date_end, $selected_group_ids = array())
	{
		$clean_group_ids = array_values(array_unique(array_map('intval', (array)$selected_group_ids)));
		sort($clean_group_ids);
		$key = implode('|', array((int)$definition['id'], (string)$date_start, (string)$date_end, implode(',', $clean_group_ids)));
		if (!array_key_exists($key, $this->groupGoalMetricRankingCache))
		{
			$this->groupGoalMetricRankingCache[$key] = $this->get_group_goal_metric_ranking($definition, $date_start, $date_end, $clean_group_ids);
		}

		return $this->groupGoalMetricRankingCache[$key];
	}

	private function get_group_goal_metric_ranking($definition, $date_start, $date_end, $selected_group_ids = array())
	{
		$rows = array();
		$selected_group_ids = !empty($selected_group_ids) ? array_values(array_unique(array_map('intval', $selected_group_ids))) : array();
		$groups = $this->App_write_model->get_employee_groups(TRUE);
		foreach ($groups as $group)
		{
			$group_id = isset($group['id']) ? (int)$group['id'] : 0;
			if ($group_id <= 0 || (!empty($selected_group_ids) && !in_array($group_id, $selected_group_ids, TRUE)))
			{
				continue;
			}

			$rows[] = array(
				'group_id' => $group_id,
				'metric_value' => $this->get_group_goal_metric_value($group_id, $definition, $date_start, $date_end),
			);
		}

		usort($rows, function ($a, $b) {
			$a_value = isset($a['metric_value']) ? (float)$a['metric_value'] : 0.0;
			$b_value = isset($b['metric_value']) ? (float)$b['metric_value'] : 0.0;
			if ($a_value === $b_value)
			{
				return 0;
			}
			return $a_value > $b_value ? -1 : 1;
		});

		return $rows;
	}

	private function get_goal_selected_group_ids($definition)
	{
		$group_ids = array();
		if (!empty($definition['group_ids']) && is_array($definition['group_ids']))
		{
			$group_ids = $definition['group_ids'];
		}
		elseif (!empty($definition['group_id']))
		{
			$group_ids = array((int)$definition['group_id']);
		}

		$clean_ids = array();
		foreach ($group_ids as $group_id)
		{
			$group_id = (int)$group_id;
			if ($group_id > 0)
			{
				$clean_ids[$group_id] = $group_id;
			}
		}
		return array_values($clean_ids);
	}

	private function resolve_employee_matching_group_id($employee_id, $selected_group_ids)
	{
		foreach ((array)$selected_group_ids as $group_id)
		{
			$members = $this->App_write_model->get_employee_group_members((int)$group_id);
			if (in_array((int)$employee_id, $members, TRUE))
			{
				return (int)$group_id;
			}
		}

		return 0;
	}

	private function build_group_total_label($definition, $selected_group_ids)
	{
		if (empty($selected_group_ids))
		{
			return '';
		}

		$groups = $this->index_option_rows($this->App_write_model->get_employee_groups(TRUE), 'id');
		$group_names = array();
		foreach ($selected_group_ids as $group_id)
		{
			if (isset($groups[(int)$group_id]['name']))
			{
				$group_names[] = $groups[(int)$group_id]['name'];
			}
		}

		return !empty($group_names) ? 'Compite con: '.implode(', ', $group_names) : '';
	}

	private function build_group_bar_segments($definition, $selected_group_ids, $ranking_rows, $target_value)
	{
		if (empty($selected_group_ids))
		{
			return array();
		}

		$groups = $this->index_option_rows($this->App_write_model->get_employee_groups(TRUE), 'id');
		$max_value = 0.0;
		foreach ((array)$ranking_rows as $row)
		{
			$metric_value = isset($row['metric_value']) ? (float)$row['metric_value'] : 0.0;
			if ($metric_value > $max_value)
			{
				$max_value = $metric_value;
			}
		}
		$reference_value = (float)$target_value > 0 ? (float)$target_value : $max_value;
		if ($reference_value <= 0)
		{
			$reference_value = 1;
		}

		$segments = array();
		foreach ((array)$ranking_rows as $row)
		{
			$group_id = isset($row['group_id']) ? (int)$row['group_id'] : 0;
			if ($group_id <= 0)
			{
				continue;
			}
			$group = isset($groups[$group_id]) ? $groups[$group_id] : array();
			$metric_value = isset($row['metric_value']) ? (float)$row['metric_value'] : 0.0;
			$segments[] = array(
				'group_id' => $group_id,
				'name' => !empty($group['name']) ? $group['name'] : ('Grupo '.$group_id),
				'color' => !empty($group['color']) ? $group['color'] : '#4d74c3',
				'metric_value' => $metric_value,
				'width_percent' => min(100, max(0, ($metric_value / $reference_value) * 100)),
			);
		}

		return $segments;
	}

	private function build_goal_target_label($definition)
	{
		$method = isset($definition['goal_method']) ? $definition['goal_method'] : 'fixed';
		switch ($method)
		{
			case 'last_year_same_period':
				return 'Comparado con ano pasado';
			case 'last_year_same_period_plus_percent':
				return 'Ano pasado + '.number_format(isset($definition['goal_percent']) ? (float)$definition['goal_percent'] : 0, 2, ',', '.').'%';
			case 'competitive_leader':
				return 'Comparado con el lider';
			case 'competitive_target':
				return 'Competencia por objetivo';
			case 'fixed':
			default:
				return 'Objetivo fijo';
		}
	}

	private function build_goal_report_for_employee($employee_id)
	{
		$timezone = new DateTimeZone('America/Caracas');
		$today = new DateTime('now', $timezone);
		$month_start = new DateTime($today->format('Y-m-01'), $timezone);
		$month_end = new DateTime($today->format('Y-m-t'), $timezone);
		$month_names = array(
			1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril',
			5 => 'Mayo', 6 => 'Junio', 7 => 'Julio', 8 => 'Agosto',
			9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre',
		);
		$calendar_days = array();
		$completed_today = 0;
		$completed_month = 0;
		$total_month = 0;
		$best_streak = 0;
		$current_streak = 0;
		$running_streak = 0;
		$default_detail_date = '';
		$month_sales_index = $this->Gala_read_model->get_sales_counts_by_day(
			$month_start->format('Y-m-d 00:00:00'),
			$month_end->format('Y-m-d 23:59:59')
		);
		$this->prime_daily_goal_metric_cache($employee_id, $month_start, $month_end);
		$this->ensure_employee_goal_snapshots($employee_id, $month_start, $month_end, $today, TRUE);
		$range_start = clone $month_start;
		$range_start->modify('-7 days');
		$snapshot_index = $this->App_write_model->get_goal_snapshots_index_for_employee(
			$employee_id,
			$range_start->format('Y-m-d 00:00:00'),
			$month_end->format('Y-m-d 23:59:59')
		);

		$cursor = clone $month_start;
		while ($cursor <= $month_end)
		{
			$date_key = $cursor->format('Y-m-d');
			$is_future = $cursor > $today;
			$day_status = array(
				'date' => $date_key,
				'day' => (int)$cursor->format('d'),
				'is_today' => $date_key === $today->format('Y-m-d'),
				'is_future' => $is_future,
				'status' => '',
				'completed_count' => 0,
				'total_count' => 0,
				'partial_count' => 0,
				'movement_count' => 0,
				'details' => array(),
			);

			if (!$is_future)
			{
				list($date_start, $date_end) = $this->get_calendar_day_range($date_key);
				$global_movements = isset($month_sales_index[$date_key]) ? (int)$month_sales_index[$date_key] : 0;
				$day_status['movement_count'] = (int)$global_movements;
					if ($global_movements > 0)
					{
						$cards = $this->evaluate_goal_cards_for_employee($employee_id, $date_key, $snapshot_index);
					$total_count = count($cards);
					$completed_count = 0;
					$partial_count = 0;
					foreach ($cards as $card)
					{
						$detail_status = 'failed';
						$detail_label = 'No cumplida';
						if (!empty($card['is_completed']))
						{
							$completed_count++;
							$detail_status = 'success';
							$detail_label = 'Cumplida';
						}
						elseif (!empty($card['progress_ratio']) && (float)$card['progress_ratio'] > 0)
						{
							$partial_count++;
							$detail_status = 'partial';
							$detail_label = 'Parcial';
						}

						$day_status['details'][] = array(
							'name' => isset($card['name']) ? (string)$card['name'] : 'Meta',
							'status' => $detail_status,
							'status_label' => $detail_label,
							'summary' => $this->build_goal_report_detail_summary($card),
						);
					}

					$day_status['completed_count'] = $completed_count;
					$day_status['total_count'] = $total_count;
					$day_status['partial_count'] = $partial_count;

					if ($total_count > 0)
					{
						$total_month += $total_count;
						$completed_month += $completed_count;
						if ($day_status['is_today'])
						{
							$completed_today = $completed_count;
						}
					}

					if ($completed_count > 0)
					{
						$day_status['status'] = 'success';
					}
					elseif ($partial_count > 0)
					{
						$day_status['status'] = 'partial';
					}
					else
					{
						$day_status['status'] = 'failed';
					}

					if ($default_detail_date === '' && $day_status['is_today'])
					{
						$default_detail_date = $date_key;
					}
					elseif ($default_detail_date === '')
					{
						$default_detail_date = $date_key;
					}
				}
			}

			if (!$is_future)
			{
				if ($day_status['status'] === 'success')
				{
					$running_streak++;
					$best_streak = max($best_streak, $running_streak);
					$current_streak = $running_streak;
				}
				else
				{
					$running_streak = 0;
					$current_streak = 0;
				}
			}

			$calendar_days[] = $day_status;
			$cursor->modify('+1 day');
		}

		return array(
			'completed_today' => $completed_today,
			'completed_month' => $completed_month,
			'total_month' => $total_month,
			'completion_percent_month' => $total_month > 0 ? round(($completed_month / $total_month) * 100, 1) : 0,
			'current_streak' => $current_streak,
			'best_streak' => $best_streak,
			'calendar_days' => $calendar_days,
			'default_detail_date' => $default_detail_date,
			'month_label' => (isset($month_names[(int)$today->format('n')]) ? $month_names[(int)$today->format('n')] : $today->format('F')).' '.$today->format('Y'),
			'month_start_weekday' => (int)$month_start->format('N'),
		);
	}

	private function ensure_employee_goal_snapshots($employee_id, DateTime $month_start, DateTime $month_end, DateTime $today, $skip_if_no_movements = FALSE)
	{
		$definitions = $this->get_active_goal_definitions_cached();
		if (empty($definitions))
		{
			return;
		}

		$timezone = $today->getTimezone();
		$today_start = new DateTime($today->format('Y-m-d 00:00:00'), $timezone);
		$yesterday_end = clone $today_start;
		$yesterday_end->modify('-1 second');
		if ($yesterday_end < $month_start)
		{
			return;
		}

		$periods = array();
		$cursor = clone $month_start;
		$cursor_end = clone $month_end;
		if ($yesterday_end < $cursor_end)
		{
			$cursor_end = $yesterday_end;
		}

		$sales_index = array();
		if ($skip_if_no_movements)
		{
			$range_start = clone $month_start;
			$range_start->modify('-7 days');
			$sales_index = $this->Gala_read_model->get_sales_counts_by_day(
				$range_start->format('Y-m-d 00:00:00'),
				$month_end->format('Y-m-d 23:59:59'),
				$employee_id
			);
		}

		while ($cursor <= $cursor_end)
		{
			$reference_date = $cursor->format('Y-m-d');
			foreach ($definitions as $definition)
			{
				if (!$this->goal_applies_to_employee($definition, $employee_id) || !$this->goal_is_currently_available($definition, $reference_date))
				{
					continue;
				}

				list($date_start, $date_end) = $this->get_goal_period_range(
					isset($definition['period_type']) ? $definition['period_type'] : 'daily',
					$definition,
					$reference_date
				);
				if ($date_end >= $today_start->format('Y-m-d H:i:s'))
				{
					continue;
				}

				if ($skip_if_no_movements)
				{
					$movement_count = $this->get_sales_count_for_period($sales_index, $date_start, $date_end);
					if ($movement_count <= 0)
					{
						continue;
					}
				}

				$key = $this->build_goal_snapshot_key((int)$definition['id'], $date_start, $date_end);
				if (!isset($periods[$key]))
				{
					$periods[$key] = array(
						'definition' => $definition,
						'reference_date' => $reference_date,
						'date_start' => $date_start,
						'date_end' => $date_end,
					);
				}
			}

			$cursor->modify('+1 day');
		}

		if (empty($periods))
		{
			return;
		}

		$min_start = null;
		$max_end = null;
		foreach ($periods as $period)
		{
			if ($min_start === null || $period['date_start'] < $min_start)
			{
				$min_start = $period['date_start'];
			}
			if ($max_end === null || $period['date_end'] > $max_end)
			{
				$max_end = $period['date_end'];
			}
		}

		$existing = $this->App_write_model->get_goal_snapshots_index_for_employee($employee_id, $min_start, $max_end);
		foreach ($periods as $key => $period)
		{
			if (isset($existing[$key]))
			{
				continue;
			}

			$definition = $period['definition'];
			$card = $this->evaluate_single_goal_definition($definition, $employee_id, $period['reference_date']);
			if (!$card)
			{
				continue;
			}

			$this->App_write_model->save_goal_snapshot(array(
				'goal_id' => (int)$definition['id'],
				'employee_id' => (int)$employee_id,
				'period_type' => isset($definition['period_type']) ? (string)$definition['period_type'] : 'daily',
				'period_start' => $period['date_start'],
				'period_end' => $period['date_end'],
				'current_value' => isset($card['current_value']) ? (float)$card['current_value'] : 0.0,
				'target_value' => isset($card['target_value']) ? (float)$card['target_value'] : 0.0,
				'progress_ratio' => isset($card['progress_ratio']) ? (float)$card['progress_ratio'] : 0.0,
				'is_completed' => !empty($card['is_completed']),
				'rank_position' => isset($card['rank_position']) ? (int)$card['rank_position'] : 0,
			));
		}
	}

	private function get_sales_count_for_period($sales_index, $date_start, $date_end)
	{
		if (empty($sales_index))
		{
			return 0;
		}

		$count = 0;
		$timezone = new DateTimeZone('America/Caracas');
		$cursor = new DateTime(substr((string)$date_start, 0, 10).' 00:00:00', $timezone);
		$end = new DateTime(substr((string)$date_end, 0, 10).' 00:00:00', $timezone);
		while ($cursor <= $end)
		{
			$key = $cursor->format('Y-m-d');
			$count += isset($sales_index[$key]) ? (int)$sales_index[$key] : 0;
			$cursor->modify('+1 day');
		}

		return $count;
	}

	private function build_goal_report_detail_summary($card)
	{
		$display_mode = isset($card['display_mode']) ? (string)$card['display_mode'] : 'bar';
		$metric_label = isset($card['metric_label']) ? (string)$card['metric_label'] : 'Monto';
		$current_value = isset($card['current_value']) ? (float)$card['current_value'] : 0;
		$target_value = isset($card['target_value']) ? (float)$card['target_value'] : 0;

		if ($display_mode === 'rank')
		{
			return 'Puesto #'.(int)(isset($card['rank_position']) ? $card['rank_position'] : 0);
		}

		if ($display_mode === 'number')
		{
			$decimals = $metric_label === 'Cantidad' ? 0 : 2;
			return number_format($current_value, $decimals, ',', '.').'/'.number_format($target_value, $decimals, ',', '.');
		}

		$progress_ratio = isset($card['progress_ratio']) ? (float)$card['progress_ratio'] : 0;
		return number_format(max(0, min(100, $progress_ratio * 100)), 0, ',', '.').'%';
	}

	private function build_task_report_for_employee($employee_id)
	{
		$timezone = new DateTimeZone('America/Caracas');
		$today = new DateTime('now', $timezone);
		$month_start = new DateTime($today->format('Y-m-01'), $timezone);
		$month_end = new DateTime($today->format('Y-m-t'), $timezone);
		$assignments = $this->Tasks_model->get_assignments_for_employee_range(
			(int)$employee_id,
			$month_start->format('Y-m-d'),
			$month_end->format('Y-m-d')
		);

		$days_in_month = (int)$month_end->format('j');
		$calendar_index = array();
		for ($day = 1; $day <= $days_in_month; $day++)
		{
			$date = sprintf('%s-%02d', $month_start->format('Y-m'), $day);
			$calendar_index[$date] = array(
				'date' => $date,
				'day' => $day,
				'assigned_count' => 0,
				'completed_count' => 0,
				'status' => '',
				'details' => array(),
				'is_today' => $date === $today->format('Y-m-d'),
			);
		}

		$completed_today = 0;
		$completed_month = 0;
		$total_month = count($assignments);

		foreach ($assignments as $assignment)
		{
			$assigned_date = isset($assignment['assigned_date']) ? (string)$assignment['assigned_date'] : '';
			if ($assigned_date === '' || !isset($calendar_index[$assigned_date]))
			{
				continue;
			}

			$status = isset($assignment['status']) ? (string)$assignment['status'] : 'pending';
			$calendar_index[$assigned_date]['assigned_count']++;
			if ($status === 'completed')
			{
				$calendar_index[$assigned_date]['completed_count']++;
				$completed_month++;
				if (!empty($assignment['completed_at']) && strpos((string)$assignment['completed_at'], $today->format('Y-m-d')) === 0)
				{
					$completed_today++;
				}
			}

			$calendar_index[$assigned_date]['details'][] = array(
				'name' => isset($assignment['title']) ? $assignment['title'] : 'Tarea',
				'status' => $status,
				'status_label' => $this->build_task_status_label($status),
				'summary' => $this->build_task_report_detail_summary($assignment),
			);
		}

		$calendar_days = array();
		$current_streak = 0;
		$best_streak = 0;
		$rolling_streak = 0;
		foreach ($calendar_index as $date => $day)
		{
			if ($day['assigned_count'] > 0)
			{
				if ($day['completed_count'] === $day['assigned_count'])
				{
					$day['status'] = 'success';
				}
				elseif ($day['completed_count'] > 0)
				{
					$day['status'] = 'partial';
				}
				else
				{
					$has_failed = FALSE;
					foreach ($day['details'] as $detail)
					{
						if ($detail['status'] === 'overdue')
						{
							$has_failed = TRUE;
							break;
						}
					}
					$day['status'] = $has_failed ? 'failed' : 'partial';
				}
			}

			if ($day['status'] === 'success')
			{
				$rolling_streak++;
				if ($rolling_streak > $best_streak)
				{
					$best_streak = $rolling_streak;
				}
			}
			elseif ($day['assigned_count'] > 0)
			{
				$rolling_streak = 0;
			}

			$calendar_days[] = $day;
		}

		for ($index = count($calendar_days) - 1; $index >= 0; $index--)
		{
			if ($calendar_days[$index]['status'] === 'success')
			{
				$current_streak++;
			}
			elseif ($calendar_days[$index]['assigned_count'] > 0)
			{
				break;
			}
		}

		$default_detail_date = '';
		if (isset($calendar_index[$today->format('Y-m-d')]) && $calendar_index[$today->format('Y-m-d')]['assigned_count'] > 0)
		{
			$default_detail_date = $today->format('Y-m-d');
		}
		else
		{
			foreach ($calendar_days as $day)
			{
				if ($day['assigned_count'] > 0)
				{
					$default_detail_date = $day['date'];
					break;
				}
			}
		}

		return array(
			'completed_today' => $completed_today,
			'completed_month' => $completed_month,
			'total_month' => $total_month,
			'completion_percent_month' => $total_month > 0 ? (int)round(($completed_month / $total_month) * 100) : 0,
			'current_streak' => $current_streak,
			'best_streak' => $best_streak,
			'calendar_days' => $calendar_days,
			'month_label' => $this->build_month_label($today),
			'month_start_weekday' => (int)$month_start->format('N'),
			'default_detail_date' => $default_detail_date,
		);
	}

	private function build_task_report_detail_summary($assignment)
	{
		$parts = array();
		$items_completed = isset($assignment['items_completed']) ? (int)$assignment['items_completed'] : 0;
		$items_total = isset($assignment['items_total']) ? (int)$assignment['items_total'] : 0;
		if ($items_total > 0)
		{
			$parts[] = $items_completed.' de '.$items_total.' subtareas';
		}

		if (!empty($assignment['completed_at']))
		{
			$parts[] = 'Cerrada '.date('d/m/Y h:i A', strtotime($assignment['completed_at']));
		}
		elseif (isset($assignment['status']) && $assignment['status'] === 'overdue')
		{
			$parts[] = 'Quedo vencida';
		}
		else
		{
			$parts[] = 'Asignada '.date('d/m/Y', strtotime($assignment['assigned_date']));
		}

		return implode(' · ', $parts);
	}

	private function build_task_status_label($status)
	{
		switch ((string)$status)
		{
			case 'completed':
				return 'Completada';
			case 'overdue':
				return 'Vencida';
			case 'in_progress':
				return 'En progreso';
			case 'pending':
			default:
				return 'Pendiente';
		}
	}

	private function build_month_label(DateTime $date)
	{
		$months = array(
			1 => 'Enero',
			2 => 'Febrero',
			3 => 'Marzo',
			4 => 'Abril',
			5 => 'Mayo',
			6 => 'Junio',
			7 => 'Julio',
			8 => 'Agosto',
			9 => 'Septiembre',
			10 => 'Octubre',
			11 => 'Noviembre',
			12 => 'Diciembre',
		);

		$month = (int)$date->format('n');
		return (isset($months[$month]) ? $months[$month] : $date->format('F')).' '.$date->format('Y');
	}

	private function get_calendar_day_range($date_key)
	{
		return array($date_key.' 00:00:00', $date_key.' 23:59:59');
	}

	private function get_goal_period_range($period_type, $definition = array(), $reference_date = null)
	{
		$timezone = new DateTimeZone('America/Caracas');
		$today = $reference_date ? new DateTime($reference_date.' 12:00:00', $timezone) : new DateTime('now', $timezone);
		switch ((string)$period_type)
		{
			case 'custom':
				$start_value = !empty($definition['custom_period_start_date']) ? $definition['custom_period_start_date'] : $today->format('Y-m-d');
				$end_value = !empty($definition['custom_period_end_date']) ? $definition['custom_period_end_date'] : $start_value;
				$start = new DateTime($start_value.' 00:00:00', $timezone);
				$end = new DateTime($end_value.' 23:59:59', $timezone);
				break;
			case 'weekly':
				$start = clone $today;
				$start->modify('monday this week')->setTime(0, 0, 0);
				$end = clone $start;
				$end->modify('+6 days')->setTime(23, 59, 59);
				break;
			case 'biweekly':
				$day = (int)$today->format('d');
				if ($day <= 15)
				{
					$start = new DateTime($today->format('Y-m-01 00:00:00'), $timezone);
					$end = new DateTime($today->format('Y-m-15 23:59:59'), $timezone);
				}
				else
				{
					$start = new DateTime($today->format('Y-m-16 00:00:00'), $timezone);
					$end = new DateTime($today->format('Y-m-t 23:59:59'), $timezone);
				}
				break;
			case 'monthly':
				$start = new DateTime($today->format('Y-m-01 00:00:00'), $timezone);
				$end = new DateTime($today->format('Y-m-t 23:59:59'), $timezone);
				break;
			case 'daily':
			default:
				$start = new DateTime($today->format('Y-m-d 00:00:00'), $timezone);
				$end = new DateTime($today->format('Y-m-d 23:59:59'), $timezone);
				break;
		}

		if (!empty($definition['created_at']))
		{
			$created_at = trim((string)$definition['created_at']);
			try
			{
				$created = new DateTime($created_at, $timezone);
				$created->modify('first day of this month')->setTime(0, 0, 0);
				if ($created > $start)
				{
					$start = $created;
				}
				if ($start > $end)
				{
					$start = clone $end;
				}
			}
			catch (Exception $e)
			{
				// Ignore invalid created_at values and use default range.
			}
		}

		return array($start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'));
	}

	private function get_goal_historical_period_range($period_type, $definition = array(), $reference_date = null)
	{
		$timezone = new DateTimeZone('America/Caracas');
		$today = $reference_date ? new DateTime($reference_date.' 12:00:00', $timezone) : new DateTime('now', $timezone);
		switch ((string)$period_type)
		{
			case 'custom':
				$start_value = !empty($definition['custom_period_start_date']) ? $definition['custom_period_start_date'] : $today->format('Y-m-d');
				$end_value = !empty($definition['custom_period_end_date']) ? $definition['custom_period_end_date'] : $start_value;
				$start = new DateTime($start_value.' 00:00:00', $timezone);
				$end = new DateTime($end_value.' 23:59:59', $timezone);
				$start->modify('-1 year');
				$end->modify('-1 year');
				break;
			case 'weekly':
				$isoWeek = (int)$today->format('W');
				$isoYear = (int)$today->format('o') - 1;
				$start = new DateTime('now', $timezone);
				$start->setISODate($isoYear, $isoWeek, 1)->setTime(0, 0, 0);
				$end = clone $start;
				$end->modify('+6 days')->setTime(23, 59, 59);
				break;
			case 'biweekly':
				$year = (int)$today->format('Y') - 1;
				$month = (int)$today->format('m');
				$day = (int)$today->format('d');
				if ($day <= 15)
				{
					$start = new DateTime(sprintf('%04d-%02d-01 00:00:00', $year, $month), $timezone);
					$end = new DateTime(sprintf('%04d-%02d-15 23:59:59', $year, $month), $timezone);
				}
				else
				{
					$start = new DateTime(sprintf('%04d-%02d-16 00:00:00', $year, $month), $timezone);
					$end = new DateTime(sprintf('%04d-%02d-01 00:00:00', $year, $month), $timezone);
					$end->modify('last day of this month')->setTime(23, 59, 59);
				}
				break;
			case 'monthly':
				$year = (int)$today->format('Y') - 1;
				$month = (int)$today->format('m');
				$start = new DateTime(sprintf('%04d-%02d-01 00:00:00', $year, $month), $timezone);
				$end = clone $start;
				$end->modify('last day of this month')->setTime(23, 59, 59);
				break;
			case 'daily':
			default:
				$isoWeek = (int)$today->format('W');
				$isoDay = (int)$today->format('N');
				$isoYear = (int)$today->format('o') - 1;
				$start = new DateTime('now', $timezone);
				$start->setISODate($isoYear, $isoWeek, $isoDay)->setTime(0, 0, 0);
				$end = clone $start;
				$end->setTime(23, 59, 59);
				break;
		}

		return array($start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'));
	}

	private function redirect_to_first_allowed_route()
	{
		$routes = array(
			'notificar.view' => 'notificar',
			'tareas.view' => 'tareas',
			'ventas.view' => 'ventas',
			'metas.view' => 'metas',
			'personalizar.view' => 'personalizar',
			'configuracion.view' => 'configuracion',
		);

		foreach ($routes as $permission => $route)
		{
			if ($this->has_permission($permission))
			{
				redirect($route);
			}
		}

		show_error('No tienes modulos habilitados en Visor.', 403, 'Acceso denegado');
	}

	private function render($view, $data = array())
	{
		$auth_user = $this->get_auth_user();
		$employee_id = $auth_user ? (int)$auth_user['employee_id'] : 0;
		$data['view'] = $view;
		$data['auth_user'] = $auth_user;
		$data['allowed_modules'] = $auth_user ? $this->get_allowed_modules() : array();
		$data['app_name'] = $this->App_write_model->get_setting('app_name', 'Visor');
		$data['company_name'] = $this->get_company_name();
		$data['user_preferences'] = $this->App_write_model->get_user_preferences($employee_id);
		$data['completion_counters'] = $employee_id > 0 ? $this->App_write_model->get_completion_counters($employee_id) : array('tasks' => 0, 'goals' => 0);
		$this->load->view('layouts/main', $data);
	}

	private function get_company_name()
	{
		return $this->Gala_read_model->get_company_name_for_visor(1);
	}

	private function get_allowed_modules()
	{
		$items = array(
			'dashboard' => array('label' => 'Dashboard', 'route' => 'dashboard', 'permission' => 'dashboard.view', 'icon' => 'ion-speedometer'),
			'notificar' => array('label' => 'Notificar', 'route' => 'notificar', 'permission' => 'notificar.view', 'icon' => 'ion-android-notifications'),
			'tareas' => array('label' => 'Tareas', 'route' => 'tareas', 'permission' => 'tareas.view', 'icon' => 'ion-checkmark-circled'),
			'ventas' => array('label' => 'Ventas', 'route' => 'ventas', 'permission' => 'ventas.view', 'icon' => 'ion-cash'),
			'metas' => array('label' => 'Metas', 'route' => 'metas', 'permission' => 'metas.view', 'icon' => 'ion-stats-bars'),
			'personalizar' => array('label' => 'Personalizar', 'route' => 'personalizar', 'permission' => 'personalizar.view', 'icon' => 'ion-gear-a'),
			'configuracion' => array('label' => 'Configuracion', 'route' => 'configuracion', 'permission' => 'configuracion.view', 'icon' => 'ion-settings'),
		);

		$allowed = array();
		foreach ($items as $key => $item)
		{
			if ($this->has_permission($item['permission']) && $this->is_module_available($key))
			{
				$allowed[$key] = $item;
			}
		}

		return $allowed;
	}

	private function is_module_available($module_key)
	{
		if ($module_key === 'personalizar')
		{
			return $this->has_permission('personalizar.visor.view')
				|| $this->has_permission('personalizar.aplicacion.view');
		}

		if ($module_key === 'configuracion')
		{
			return $this->has_permission('configuracion.descuentos.view')
				|| $this->has_permission('configuracion.grupos.view')
				|| $this->has_permission('configuracion.metas.view')
				|| $this->has_permission('configuracion.tareas.view')
				|| $this->has_permission('configuracion.notificaciones.view')
				|| $this->has_permission('configuracion.ventas.view')
				|| $this->has_permission('configuracion.empleados.view');
		}

		return TRUE;
	}

	private function build_employee_groups_index($groups, $group_members_index)
	{
		$index = array();
		foreach ($groups as $group)
		{
			$group_id = isset($group['id']) ? (int)$group['id'] : 0;
			if ($group_id <= 0 || empty($group_members_index[$group_id]))
			{
				continue;
			}

			foreach ($group_members_index[$group_id] as $employee_id)
			{
				if (!isset($index[$employee_id]))
				{
					$index[$employee_id] = array();
				}

				$index[$employee_id][] = array(
					'id' => $group_id,
					'name' => isset($group['name']) ? $group['name'] : 'Grupo',
					'color' => !empty($group['color']) ? $group['color'] : '#98d245',
				);
			}
		}

		return $index;
	}

	private function build_employee_goal_summary($goal_cards)
	{
		$total = count($goal_cards);
		$completed = 0;
		$has_progress = FALSE;
		$ratio_sum = 0.0;

		foreach ($goal_cards as $card)
		{
			if (!empty($card['is_completed']))
			{
				$completed++;
			}
			if (isset($card['progress_ratio']) && (float)$card['progress_ratio'] > 0)
			{
				$has_progress = TRUE;
			}
			$ratio_sum += isset($card['progress_ratio']) ? (float)$card['progress_ratio'] : 0.0;
		}

		if ($total === 0)
		{
			$status_key = 'no_goals';
			$status_label = 'Sin metas';
			$progress_percent = 0;
		}
		elseif ($completed >= $total)
		{
			$status_key = 'completed';
			$status_label = 'Cumplidas';
			$progress_percent = 100;
		}
		elseif ($has_progress)
		{
			$status_key = 'partial';
			$status_label = 'En progreso';
			$progress_percent = (int)round(($ratio_sum / max(1, $total)) * 100);
		}
		else
		{
			$status_key = 'partial';
			$status_label = 'Pendientes';
			$progress_percent = 0;
		}

		return array(
			'total' => $total,
			'completed' => $completed,
			'pending' => max(0, $total - $completed),
			'status_key' => $status_key,
			'status_label' => $status_label,
			'progress_percent' => max(0, min(100, $progress_percent)),
		);
	}

	private function find_gala_employee($employee_id)
	{
		$employees = $this->Gala_read_model->get_employees_for_visor();
		foreach ($employees as $employee)
		{
			if ((int)$employee['person_id'] === (int)$employee_id)
			{
				return $employee;
			}
		}

		return null;
	}

	private function build_permission_state($permissions)
	{
		$labels = $this->get_permission_labels();
		$state = array();
		foreach ($labels as $key => $label)
		{
			$state[$key] = !empty($permissions[$key]);
		}

		return $state;
	}

	private function encode_permission_key($key)
	{
		return str_replace('.', '__dot__', (string)$key);
	}

	private function decode_permission_post($permissions)
	{
		$decoded = array();
		foreach ($permissions as $key => $value)
		{
			$decoded[str_replace('__dot__', '.', (string)$key)] = $value;
		}

		return $decoded;
	}

	private function render_preference_pair_page($title, $back_route, $form_action, $fields)
	{
		$this->require_permission('personalizar.view');
		$auth_user = $this->get_auth_user();
		$preferences = $this->App_write_model->get_user_preferences((int)$auth_user['employee_id']);

		if ($this->input->method(TRUE) === 'POST' && $this->input->post('reset_preferences'))
		{
			$keys = array();
			foreach ($fields as $field)
			{
				$keys[] = $field['name'];
			}
			$this->App_write_model->reset_user_preferences((int)$auth_user['employee_id'], $keys);

			$return_to = trim((string)$this->input->post('return_to'));
			$open_section = trim((string)$this->input->post('open_section'));
			if ($return_to !== '')
			{
				if ($open_section !== '')
				{
					$return_to .= '?open='.rawurlencode($open_section);
				}
				redirect($return_to);
			}

			redirect($back_route);
		}

		foreach ($fields as $field)
		{
			$rule = $field['type'] === 'number' ? 'trim|required|numeric' : 'trim|required';
			$this->form_validation->set_rules($field['name'], $field['label'], $rule);
		}

		if ($this->input->method(TRUE) === 'POST' && $this->form_validation->run())
		{
			$payload = array();
			foreach ($fields as $field)
			{
				$payload[$field['name']] = $field['type'] === 'number'
					? (float)$this->input->post($field['name'])
					: (string)$this->input->post($field['name']);
			}
			$this->App_write_model->save_user_preferences((int)$auth_user['employee_id'], $payload);

			$return_to = trim((string)$this->input->post('return_to'));
			$open_section = trim((string)$this->input->post('open_section'));
			if ($return_to !== '')
			{
				if ($open_section !== '')
				{
					$return_to .= '?open='.rawurlencode($open_section);
				}
				redirect($return_to);
			}

			redirect($form_action);
		}

		foreach ($fields as &$field)
		{
			$field['value'] = isset($preferences[$field['name']]) ? $preferences[$field['name']] : '';
		}
		unset($field);

		$this->render('app/personalizar_pair', array(
			'page_title' => $title,
			'current_module' => 'personalizar',
			'title' => $title,
			'back_route' => $back_route,
			'form_action' => $form_action,
			'fields' => $fields,
			'hide_main_menu' => TRUE,
		));
	}

	private function is_authenticated()
	{
		$user = $this->session->userdata('visor_auth');
		return is_array($user) && !empty($user['employee_id']);
	}

	private function get_auth_user()
	{
		$user = $this->session->userdata('visor_auth');
		return is_array($user) ? $user : null;
	}

	private function ensure_active_authenticated_user()
	{
		$auth_user = $this->get_auth_user();
		if (!$auth_user)
		{
			return;
		}

		if (!$this->App_write_model->can_app_user_login((int)$auth_user['employee_id']))
		{
			$this->session->unset_userdata('visor_auth');
			$this->session->sess_destroy();
			redirect('login');
		}
	}

	private function get_visor_history()
	{
		$history = $this->session->userdata('visor_history');
		return is_array($history) ? $history : array();
	}

	private function push_visor_history($lookup_result)
	{
		$history = $this->get_visor_history();
		array_unshift($history, array(
			'name' => isset($lookup_result['name']) ? $lookup_result['name'] : '',
			'subtitle' => isset($lookup_result['subtitle']) ? $lookup_result['subtitle'] : '',
			'reference' => isset($lookup_result['reference']) ? $lookup_result['reference'] : '',
			'price_divisa' => isset($lookup_result['price_divisa']) ? (float)$lookup_result['price_divisa'] : 0.0,
			'created_at' => date('Y-m-d H:i:s'),
		));

		$history = array_slice($history, 0, 20);
		$this->session->set_userdata('visor_history', $history);
		return $history;
	}

	private function read_message_setting($key)
	{
		$raw = $this->App_write_model->get_setting($key, '[]');
		$list = json_decode((string)$raw, TRUE);
		if (!is_array($list))
		{
			return array();
		}

		return $this->normalize_message_list($list);
	}

	private function read_sales_messages()
	{
		$messages = $this->read_message_setting('sales_messages');
		if (!empty($messages))
		{
			return $messages;
		}

		$legacy = array_merge(
			$this->read_message_setting('sales_messages_no_sales'),
			$this->read_message_setting('sales_messages_with_sales')
		);

		return $this->normalize_message_list($legacy);
	}

	private function normalize_message_list($list)
	{
		if (!is_array($list))
		{
			return array();
		}

		$normalized = array();
		foreach ($list as $message)
		{
			$entry = null;
			if (is_array($message))
			{
				$entry = $message;
			}
			elseif (is_string($message))
			{
				$decoded = json_decode($message, TRUE);
				$entry = is_array($decoded) ? $decoded : array('text' => $message, 'min_sales' => 0);
			}

			if (!is_array($entry))
			{
				continue;
			}

			$text = isset($entry['text']) ? trim((string)$entry['text']) : '';
			$min_sales = isset($entry['min_sales']) ? max(0, (int)$entry['min_sales']) : 0;
			if ($text !== '')
			{
				$normalized[] = array(
					'text' => $text,
					'min_sales' => $min_sales,
				);
			}
		}

		$unique = array();
		foreach ($normalized as $entry)
		{
			$key = $entry['text'].'|'.$entry['min_sales'];
			$unique[$key] = $entry;
		}

		$normalized = array_values($unique);
		usort($normalized, function ($a, $b) {
			$minA = isset($a['min_sales']) ? (int)$a['min_sales'] : 0;
			$minB = isset($b['min_sales']) ? (int)$b['min_sales'] : 0;
			if ($minA === $minB)
			{
				return strcasecmp(
					isset($a['text']) ? (string)$a['text'] : '',
					isset($b['text']) ? (string)$b['text'] : ''
				);
			}

			return $minA <=> $minB;
		});

		return $normalized;
	}

	private function pick_sales_message($sales_count = 0)
	{
		$messages = $this->read_sales_messages();

		if (empty($messages))
		{
			return '';
		}

		$selected_threshold = null;
		foreach ($messages as $message)
		{
			$min_sales = isset($message['min_sales']) ? max(0, (int)$message['min_sales']) : 0;
			if ($sales_count >= $min_sales && ($selected_threshold === null || $min_sales > $selected_threshold))
			{
				$selected_threshold = $min_sales;
			}
		}

		if ($selected_threshold === null)
		{
			return '';
		}

		$eligible = array();
		foreach ($messages as $message)
		{
			$min_sales = isset($message['min_sales']) ? max(0, (int)$message['min_sales']) : 0;
			if ($min_sales === $selected_threshold)
			{
				$eligible[] = $message;
			}
		}

		if (empty($eligible))
		{
			return '';
		}

		$selected = $eligible[array_rand($eligible)];
		return isset($selected['text']) ? $selected['text'] : '';
	}
}
