Files
studiobeve.no/application/controllers/Admin.php
T
UbuntuandClaude Opus 5 13e1b1a672 Add Amarildo to the barber page and gate workers by service
New barber Amarildo Champimpi is introduced on the barber page in all three
languages, and workers can now be restricted to a subset of the services in
their category.

Amarildo cannot perform beard colouring, any waxing, or the all-in package, and
he is a barber only - but category 1 "Kozmetika" holds 19 barber AND 27 beauty
services, so the category-derived vertical wrongly made him beauty-capable.
Because the worker picker is only ever fetched AFTER services are ticked, one
per-service capability filter solves both problems: excluding him from every
beauty service removes him from that vertical entirely.

worker_services(worker_id, service_id) is an allow-list where an EMPTY set means
UNRESTRICTED. That default is deliberate: a missing migration degrades to the
previous behaviour instead of hiding every worker from the booking flow, and
existing workers keep working untouched. Ticking every box in the admin grid
stores nothing at all, so an unrestricted worker also picks up services added
later; unticking even one makes the worker restricted, and new services must
then be granted explicitly.

Enforcement is in three places. The picker offers only workers who can perform
EVERY selected service, and both booking paths re-check server-side, since the
picker is only a UI affordance - a crafted POST now gets service_not_offered/403
rather than a booking the worker cannot honour.

Fixed alongside, all found while building the above:

- getWorkersByCategorySlug() never filtered is_active, so marking a worker
  inactive had NO effect on the public booking flow. Both of its callers are
  guest-facing. The sibling fallback getActiveWorkers() had always filtered it.
- Worker profile picture uploads failed SILENTLY above PHP's upload_max_filesize.
  Both upload blocks gated on tmp_name alone, which cannot distinguish a rejected
  upload from "no file chosen" - PHP empties tmp_name in both cases - so the
  worker was saved with an empty worker_profile_img and no error shown. The
  upload error code is now read and reported, and a separate guard catches
  post_max_size overflow, where $_POST and $_FILES both arrive empty and the form
  silently did nothing at all.
- createWorker() omitted is_active from its INSERT, so the column default (1)
  always won and a worker created as inactive silently came back active.

Migration - the table MUST be created before this code is deployed, because the
picker query subselects it whenever service ids are passed:

    CREATE TABLE worker_services (
      worker_id  INT NOT NULL,
      service_id INT NOT NULL,
      PRIMARY KEY (worker_id, service_id),
      KEY idx_worker_services_worker (worker_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Already applied on test, dev and prod. Server-side, upload_max_filesize/
post_max_size were raised to 8M/12M on all three environments (php.ini on test,
.user.ini on the shared-host dev and prod docroots) - not carried by this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYGSbK1erKv7VG1pvdPEjG
2026-08-24 12:07:07 +00:00

1552 lines
66 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class Admin extends CI_Controller {
/**
* Index Page for this controller.
*
* Maps to the following URL
* http://example.com/index.php/welcome
* - or -
* http://example.com/index.php/welcome/index
* - or -
* Since this controller is set as the default controller in
* config/routes.php, it's displayed at http://example.com/
*
* So any other public methods not prefixed with an underscore will
* map to /index.php/welcome/<method_name>
* @see https://codeigniter.com/userguide3/general/urls.html
*/
public function dashboard(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Vezérlőpult';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$this->load->view('admin/dashboard', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function services(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Szolgáltatások';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['results'] = $this->Service_model->getAllServices();
$this->load->view('admin/services', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function add_service(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Szolgáltatás hozzáadás';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['serviceCategories'] = $this->Service_model->getAllServiceCategories();
$this->load->view('admin/add-service', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function worker_schedule() {
$this->load->helper('url');
$this->load->model('Service_model');
$this->load->model('User_model');
$this->load->model('Module_model');
if (!isset($_SESSION['username'])) {
header("Location:".SITEURL."login");
exit;
}
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
if ($this->User_model->is_user_admin($_SESSION['username'])) {
$data['activeModules'] = $this->Module_model->getAllModule();
} else {
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['workers'] = $this->Service_model->getAllWorkers();
$worker_id = $this->input->get('worker_id');
if ($worker_id) {
$data['selectedWorkerId'] = $worker_id;
$data['schedule'] = $this->Service_model->getWorkerSchedule($worker_id);
} else {
$data['selectedWorkerId'] = null;
$data['schedule'] = [];
}
$this->load->view('admin/worker_schedule', $data);
}
public function worker_schedule_overrides() {
$this->load->model('Service_model');
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['workerSchedules'] = $this->Service_model->getWorkerScheduleOverrides();
$data['workers'] = $this->Service_model->getAllWorkers();
$data['pageTitle'] = 'Dolgozói egyedi munkaidők';
$this->load->view('admin/worker_schedule_overrides', $data);
}
public function admin_worker_schedule() {
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$data['pageTitle'] = 'Munkarend beállítása';
$data['workers'] = $this->Service_model->getAllWorkers();
if ($this->input->post('storeSchedule')) {
$this->load->database();
$this->db->where('worker_id', $_POST['worker_id']);
$this->db->delete('worker_schedule');
foreach ($_POST['weekday'] as $i => $day) {
$insert = [
'worker_id' => $_POST['worker_id'],
'weekday' => $day,
'start_time' => $_POST['start_time'][$i],
'end_time' => $_POST['end_time'][$i],
'is_alternate_week' => isset($_POST['is_alternate_week'][$i]) ? 1 : 0
];
$this->db->insert('worker_schedule', $insert);
}
}
if (!empty($_GET['worker_id'])) {
$this->load->database();
$worker_id = $_GET['worker_id'];
$data['selectedWorkerId'] = $worker_id;
$data['schedule'] = $this->db->query("SELECT * FROM worker_schedule WHERE worker_id = ?", array($worker_id))->result();
}
$this->load->view('admin/worker_schedule', $data);
}
public function store_worker_schedule() {
$this->load->model('Service_model');
$this->load->helper('url');
$worker_id = $this->input->post('worker_id');
if (!$worker_id) {
show_error('Dolgozó nincs kiválasztva.');
return;
}
// Clear existing schedule
$this->Service_model->clearWorkerSchedule($worker_id);
// Loop through submitted times
for ($i = 0; $i <= 6; $i++) {
$start = $this->input->post("start_time[$i]");
$end = $this->input->post("end_time[$i]");
$alt = $this->input->post("alternate_week[$i]") ? 1 : 0;
if (!empty($start) && !empty($end)) {
$this->Service_model->addWorkerSchedule([
'worker_id' => $worker_id,
'weekday' => $i,
'start_time' => $start,
'end_time' => $end,
'is_alternate_week' => $alt
]);
}
}
redirect('admin/worker_schedule');
}
public function manage_worker_schedule($worker_id = null) {
$this->load->model('Service_model');
$data['workers'] = $this->Service_model->getAllWorkers();
$data['selectedWorker'] = null;
$data['schedule'] = [];
if ($worker_id) {
$data['selectedWorker'] = $this->Service_model->getWorkerById($worker_id);
$data['schedule'] = $this->Service_model->getWorkerSchedule($worker_id);
}
$this->load->view('admin/manage_worker_schedule', $data);
}
public function save_worker_schedule() {
$this->load->model('Service_model');
$worker_id = $this->input->post('worker_id');
$this->Service_model->clearWorkerSchedule($worker_id);
foreach ($_POST['weekday'] as $index => $day) {
if (!empty($_POST['start_time'][$index]) && !empty($_POST['end_time'][$index])) {
$this->Service_model->addWorkerSchedule([
'worker_id' => $worker_id,
'weekday' => $day,
'start_time' => $_POST['start_time'][$index],
'end_time' => $_POST['end_time'][$index],
'is_alternate_week' => isset($_POST['is_alternate_week'][$index]) ? 1 : 0
]);
}
}
redirect('admin/manage_worker_schedule/' . $worker_id);
}
public function update_service($service_id){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Szolgáltatás módosítása (#'.$service_id.')';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['selectedItem'] = $this->Service_model->getServiceById($service_id);
$data['serviceCategories'] = $this->Service_model->getAllServiceCategories();
$this->load->view('admin/update-service', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function service_process(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Log_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = '';
$data['message'] = '';
$data['message_class'] = '';
if(isset($_GET['delete-service'])){
$selected_service = $this->Service_model->getServiceById($_GET['delete-service']);
$serviceArray = array(
'is_deleted' => '1'
);
$this->Service_model->updateService($_GET['delete-service'], $serviceArray);
$this->Log_model->addLog('services', $data['currentUser']->username.' törölte a(z) '.$_GET['delete-service'].' számú szolgáltatást ('.$selected_service->service_name_hu.')', $data['currentUser']->username);
}
if(isset($_POST['addNewService'])){
$allowedServiceTypes = array('barber', 'beauty', 'massage');
$serviceArray = array(
'service_type' => (isset($_POST['service_type']) && in_array($_POST['service_type'], $allowedServiceTypes, true)) ? $_POST['service_type'] : 'beauty',
'service_category_no' => $_POST['service_category_no'],
'service_category_en' => $_POST['service_category_en'],
'service_category_hu' => $_POST['service_category_hu'],
'service_name_no' => $_POST['service_name_no'],
'service_name_en' => $_POST['service_name_en'],
'service_name_hu' => $_POST['service_name_hu'],
'service_description_no' => $_POST['service_description_no'],
'service_description_en' => $_POST['service_description_en'],
'service_description_hu' => $_POST['service_description_hu'],
'service_price' => $_POST['service_price'],
'service_time' => $_POST['service_time'],
'service_category_id' => $_POST['service_category_id'],
'is_enabled' => $_POST['is_enabled']
);
$this->Log_model->addLog('services', $data['currentUser']->username.' hozzáadta a(z) '.$_POST['service_name_hu'].' szolgáltatást ('.implode(',',$serviceArray).')', $data['currentUser']->username);
$this->Service_model->createService($serviceArray);
}
if(isset($_POST['updateService'])){
$allowedServiceTypes = array('barber', 'beauty', 'massage');
$existingService = $this->Service_model->getServiceById($_POST['service_id']);
$serviceArray = array(
'service_type' => (isset($_POST['service_type']) && in_array($_POST['service_type'], $allowedServiceTypes, true)) ? $_POST['service_type'] : ($existingService ? $existingService->service_type : 'beauty'),
'service_category_no' => $_POST['service_category_no'],
'service_category_en' => $_POST['service_category_en'],
'service_category_hu' => $_POST['service_category_hu'],
'service_name_no' => $_POST['service_name_no'],
'service_name_en' => $_POST['service_name_en'],
'service_name_hu' => $_POST['service_name_hu'],
'service_description_no' => $_POST['service_description_no'],
'service_description_en' => $_POST['service_description_en'],
'service_description_hu' => $_POST['service_description_hu'],
'service_price' => $_POST['service_price'],
'service_time' => $_POST['service_time'],
'service_category_id' => $_POST['service_category_id'],
'is_enabled' => $_POST['is_enabled']
);
$this->Log_model->addLog('services', $data['currentUser']->username.' módosította a(z) '.$_POST['service_name_hu'].' szolgáltatást ('.implode(',',$serviceArray).')', $data['currentUser']->username);
$this->Service_model->updateService($_POST['service_id'], $serviceArray);
}
header('Location:'.SITEURL.'services');
$this->load->view('admin/service-process', $data);
}
}
public function service_categories(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Szolgáltatás kategóriák';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['results'] = $this->Service_model->getAllServiceCategories();
$this->load->view('admin/service-categories', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function add_service_category(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Szolgáltatás kategória hozzáadása';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$this->load->view('admin/add-service-category', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function update_service_category($service_category_id){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Szolgáltatás módosítása (#'.$service_category_id.')';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['selectedItem'] = $this->Service_model->getServiceCategoryById($service_category_id);
$this->load->view('admin/update-service-category', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function service_category_process(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Log_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = '';
$data['message'] = '';
$data['message_class'] = '';
if(isset($_GET['delete-service-category'])){
$selected_service = $this->Service_model->getServiceCategoryById($_GET['delete-service-category']);
$serviceArray = array(
'is_deleted' => '1'
);
$this->Service_model->updateServiceCategory($_GET['delete-service-category'], $serviceArray);
}
if(isset($_POST['addNewServiceCategory'])){
$serviceCategoryArray = array(
'serv_cat_slug' => $_POST['serv_cat_slug'],
'serv_cat_name' => $_POST['serv_cat_name']
);
$this->Service_model->createServiceCategory($serviceCategoryArray);
}
if(isset($_POST['updateServiceCategory'])){
$serviceCategoryArray = array(
'serv_cat_slug' => $_POST['serv_cat_slug'],
'serv_cat_name' => $_POST['serv_cat_name']
);
$this->Service_model->updateServiceCategory($_POST['service_category_id'], $serviceCategoryArray);
}
header('Location:'.SITEURL.'service-categories');
$this->load->view('admin/service-category-process', $data);
}
}
public function workers(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Dolgozók';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['results'] = $this->Service_model->getAllWorkers();
$this->load->view('admin/workers', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function add_worker(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Dolgozó hozzáadása';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['serviceCategories'] = $this->Service_model->getAllServiceCategories();
$this->load->view('admin/add-worker', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function update_worker($worker_id){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Dolgozó módosítása (#'.$worker_id.')';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['serviceCategories'] = $this->Service_model->getAllServiceCategories();
$data['selectedItem'] = $this->Service_model->getWorkerById($worker_id);
// Verticals are derived from the worker's service category, not stored.
$data['workerVerticals'] = $this->Service_model->getVerticalsForWorker($worker_id);
// Capability grid: which of the category's services this worker performs.
// An empty stored set means unrestricted, which the view renders as
// everything ticked - see the note in the form.
$data['categoryServices'] = $this->Service_model->getServicesByCategoryId($data['selectedItem']->service_category_id);
$data['workerServiceIds'] = $this->Service_model->getWorkerServiceIds($worker_id);
$this->load->view('admin/update-worker', $data);
}
else{
header("Location:".SITEURL."login");
}
}
/**
* Translate the PHP upload error code for the worker profile picture into
* an admin-facing message.
*
* The upload blocks below used to gate on tmp_name alone, which made a
* REJECTED upload indistinguishable from "no file chosen": PHP empties
* tmp_name in both cases, so an oversized photo was skipped in silence and
* the worker was saved with an empty worker_profile_img. Reading the error
* code is the only way to tell the two apart.
*
* @return string '' when there is nothing to report (no file chosen, or a
* file that arrived intact), otherwise the message to show.
*/
private function _workerImgUploadError(){
// No file part at all - nothing was submitted, nothing to report.
if(!isset($_FILES['worker_img']) || !isset($_FILES['worker_img']['error'])){
return '';
}
switch($_FILES['worker_img']['error']){
case UPLOAD_ERR_OK:
case UPLOAD_ERR_NO_FILE:
// Intact, or the admin simply left the field empty.
return '';
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return 'A kép túl nagy! A megengedett legnagyobb méret: '.ini_get('upload_max_filesize').'.';
case UPLOAD_ERR_PARTIAL:
return 'A kép csak részben töltődött fel. Kérlek próbáld újra!';
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
return 'A kép feltöltése szerverhiba miatt nem sikerült. Szólj az üzemeltetőnek!';
default:
return 'Ismeretlen hiba a kép feltöltése közben.';
}
}
public function worker_process(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Log_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = '';
$data['message'] = '';
$data['message_class'] = '';
// A POST body larger than post_max_size is discarded WHOLESALE by PHP:
// $_POST and $_FILES both arrive empty, so none of the branches below
// fire and the form appears to do nothing at all. CONTENT_LENGTH still
// reports the real size, which is the only surviving evidence.
if($_SERVER['REQUEST_METHOD'] === 'POST' && empty($_POST) && !empty($_SERVER['CONTENT_LENGTH'])){
$data['message'] = 'A beküldött adat túl nagy volt, ezért a mentés nem történt meg. A kép legfeljebb '.ini_get('upload_max_filesize').' lehet.';
$data['message_class'] = 'errorMessage';
}
if(isset($_GET['delete-worker'])){
$selected_worker = $this->Service_model->getWorkerById($_GET['delete-worker']);
$workerArray = array(
'is_deleted' => '1'
);
$this->Service_model->updateWorker($_GET['delete-worker'], $workerArray);
$this->Log_model->addLog('workers', $data['currentUser']->username.' törölte a(z) '.$_GET['delete-worker'].' számú dolgozót ('.$selected_worker->worker_name.')', $data['currentUser']->username);
}
if(isset($_POST['addNewWorker'])){
$data['worker_img'] = '';
$workerImgError = $this->_workerImgUploadError();
if($workerImgError !== ''){
$data['message'] = $workerImgError;
$data['message_class'] = 'errorMessage';
} elseif($_FILES["worker_img"]["tmp_name"] != ''){
$target_dir = getcwd()."/assets/img/";
$target_file = $target_dir . 'worker_'.str_replace(' ','_', $_POST['worker_name']);
$imageFileType = strtolower(pathinfo(basename($_FILES["worker_img"]["name"]),PATHINFO_EXTENSION));
$allowedTypes = array('jpg', 'jpeg', 'png', 'gif', 'webp');
if (!in_array($imageFileType, $allowedTypes)) {
$data['message'] = 'Csak képfájlok engedélyezettek (jpg, png, gif, webp)!';
$data['message_class'] = 'errorMessage';
} elseif (move_uploaded_file($_FILES["worker_img"]["tmp_name"], $target_file.'.'.$imageFileType)) {
$data['message'] = 'A profilkép sikeresen feltöltve!';
$data['message_class'] = 'successMessage';
$data['worker_img'] = 'worker_'.str_replace(' ','_', $_POST['worker_name']).'.'.$imageFileType;
} else {
$data['message'] = 'Hiba a feltöltés közben!';
$data['message_class'] = 'errorMessage';
}
}
// is_beauty / is_barber are no longer submitted by the form - the
// vertical is derived from service_category_id. Service_model
// keeps the legacy columns consistent on write.
$workerArray = array(
'worker_name' => $_POST['worker_name'],
'worker_info' => $_POST['worker_info'],
'worker_profile_img' => $data['worker_img'],
'service_category_id' => $_POST['service_category_id'],
'is_active' => $_POST['is_active']
);
$this->Log_model->addLog('workers', $data['currentUser']->username.' létrehozta a(z) '.$_POST['worker_name'].' nevű dolgozót ('.implode(',',$workerArray).')', $data['currentUser']->username);
$this->Service_model->createWorker($workerArray);
}
if(isset($_POST['updateWorker'])){
$workerArray = array(
'worker_name' => $_POST['worker_name'],
'worker_info' => $_POST['worker_info'],
'service_category_id' => $_POST['service_category_id'],
'is_active' => $_POST['is_active'],
'google_calendar_id' => isset($_POST['google_calendar_id']) ? $_POST['google_calendar_id'] : '',
'ntfy_topic' => isset($_POST['ntfy_topic']) ? $_POST['ntfy_topic'] : '',
'lunch_window_start' => !empty($_POST['lunch_window_start']) ? $_POST['lunch_window_start'] : null,
'lunch_window_end' => !empty($_POST['lunch_window_end']) ? $_POST['lunch_window_end'] : null,
'lunch_preferred_time' => !empty($_POST['lunch_preferred_time']) ? $_POST['lunch_preferred_time'] : null,
);
$this->Service_model->updateWorker($_POST['worker_id'], $workerArray);
$this->Log_model->addLog('workers', $data['currentUser']->username.' módosította a(z) '.$_POST['worker_name'].' nevű dolgozót ('.implode(',',$workerArray).')', $data['currentUser']->username);
// Capability set. The grid posts the ticked service ids; anything not
// belonging to the worker's (possibly just-changed) category is dropped
// so a category switch cannot leave orphaned rows behind.
$postedServiceIds = isset($_POST['worker_services']) && is_array($_POST['worker_services'])
? $_POST['worker_services']
: array();
$categoryServices = $this->Service_model->getServicesByCategoryId($_POST['service_category_id']);
$categoryServiceIds = array();
if(is_array($categoryServices)){
foreach($categoryServices as $svc){
$categoryServiceIds[] = (int)$svc->service_id;
}
}
$allowedServiceIds = array();
foreach($postedServiceIds as $postedId){
if(in_array((int)$postedId, $categoryServiceIds, TRUE)){
$allowedServiceIds[] = (int)$postedId;
}
}
// Every service ticked means "no restriction": store nothing, so the
// worker keeps performing services added in the future too. Storing the
// full set instead would silently freeze them out of every new service.
if(count($allowedServiceIds) === count($categoryServiceIds)){
$allowedServiceIds = array();
}
$this->Service_model->setWorkerServiceIds($_POST['worker_id'], $allowedServiceIds);
$this->Log_model->addLog('workers', $data['currentUser']->username.' beállította a(z) '.$_POST['worker_name'].' nevű dolgozó végezhető szolgáltatásait ('.(empty($allowedServiceIds) ? 'nincs korlátozás' : implode(',', $allowedServiceIds)).')', $data['currentUser']->username);
$workerImgError = $this->_workerImgUploadError();
if($workerImgError !== ''){
$data['message'] = $workerImgError;
$data['message_class'] = 'errorMessage';
} elseif($_FILES["worker_img"]["tmp_name"] != ''){
$target_dir = getcwd()."/assets/img/";
$target_file = $target_dir . 'worker_'.str_replace(' ','_', $_POST['worker_name']);
$imageFileType = strtolower(pathinfo(basename($_FILES["worker_img"]["name"]),PATHINFO_EXTENSION));
$allowedTypes = array('jpg', 'jpeg', 'png', 'gif', 'webp');
if (!in_array($imageFileType, $allowedTypes)) {
$data['message'] = 'Csak képfájlok engedélyezettek (jpg, png, gif, webp)!';
$data['message_class'] = 'errorMessage';
} elseif (move_uploaded_file($_FILES["worker_img"]["tmp_name"], $target_file.'.'.$imageFileType)) {
$data['message'] = 'A profilkép sikeresen feltöltve!';
$data['message_class'] = 'successMessage';
$data['worker_img'] = 'worker_'.str_replace(' ','_', $_POST['worker_name']).'.'.$imageFileType;
$workerArray = array(
'worker_profile_img' => $data['worker_img']
);
$this->Log_model->addLog('workers', $data['currentUser']->username.' profilképet állított be a(z) '.$_POST['worker_name'].' nevű dolgozóhoz', $data['currentUser']->username);
$this->Service_model->updateWorker($_POST['worker_id'], $workerArray);
} else {
$data['message'] = 'Hiba a feltöltés közben!';
$data['message_class'] = 'errorMessage';
}
}
}
header("Location:".SITEURL."workers");
$this->load->view('admin/worker-process', $data);
}
}
public function bookings(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Foglalások';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['workers'] = $this->Service_model->getActiveWorkers();
if(isset($_GET['worker'])){
$data['results'] = $this->Service_model->getActualBookingsByWorkerID($_GET['worker']);
}
else{
$data['results'] = $this->Service_model->getActualBookings();
}
$this->load->view('admin/bookings', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function booking_calendar(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Foglalási naptár';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
} else {
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
// Compute week bounds
$weekParam = isset($_GET['week']) ? $_GET['week'] : date('Y-m-d');
$monday = new DateTime($weekParam);
$dow = (int)$monday->format('N'); // 1=Mon..7=Sun
if ($dow > 1) $monday->modify('-'.($dow - 1).' days');
$sunday = clone $monday;
$sunday->modify('+6 days');
$data['weekStart'] = $monday->format('Y-m-d');
$data['weekEnd'] = $sunday->format('Y-m-d');
$data['workers'] = $this->Service_model->getActiveWorkers();
$workerId = isset($_GET['worker_id']) ? $_GET['worker_id'] : null;
$data['bookings'] = $this->Service_model->getBookingsByDateRange($data['weekStart'], $data['weekEnd'], $workerId);
$this->load->view('admin/booking-calendar', $data);
} else {
header("Location:".SITEURL."login");
}
}
public function booking_calendar_data(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
if(!isset($_SESSION['username'])){
header('Content-Type: application/json');
echo json_encode(array('error' => 'unauthorized'));
return;
}
$weekParam = isset($_GET['week']) ? $_GET['week'] : date('Y-m-d');
$monday = new DateTime($weekParam);
$dow = (int)$monday->format('N');
if ($dow > 1) $monday->modify('-'.($dow - 1).' days');
$sunday = clone $monday;
$sunday->modify('+6 days');
$workerId = isset($_GET['worker_id']) && $_GET['worker_id'] !== '' ? $_GET['worker_id'] : null;
$bookings = $this->Service_model->getBookingsByDateRange($monday->format('Y-m-d'), $sunday->format('Y-m-d'), $workerId);
$result = array();
foreach ($bookings as $b) {
$services = array();
if (is_array($b->services)) {
foreach ($b->services as $s) {
$services[] = array(
'service_name' => $s->service_name,
'service_price' => $s->service_price,
'service_time' => $s->service_time
);
}
}
$result[] = array(
'booking_id' => $b->booking_id,
'guest_name' => $b->guest_name,
'guest_email' => $b->guest_email,
'guest_phone' => $b->guest_phone,
'worker_id' => $b->worker_id,
'worker_name' => $b->worker_name,
'booking_date' => $b->booking_date,
'booking_start_time' => $b->booking_start_time,
'booking_finish_time' => $b->booking_finish_time,
'services' => $services,
'service_names_joined' => $b->service_names_joined
);
}
header('Content-Type: application/json');
echo json_encode($result);
}
public function add_booking(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Foglalás hozzáadása';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$available_services = $this->Service_model->getAvailableServices();
$serviceFile = fopen("load_services.json", "w") or die("Unable to open file!");
fwrite($serviceFile, json_encode($available_services));
fclose($serviceFile);
$data['workers'] = $this->Service_model->getAllWorkers();
$this->load->view('admin/add-booking', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function update_booking($booking_id){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Module_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = 'Foglalás módosítása (#'.$booking_id.')';
$data['message'] = '';
$data['message_class'] = '';
if($this->User_model->is_user_admin($_SESSION['username'])){
$data['activeModules'] = $this->Module_model->getAllModule();
}
else{
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$available_services = $this->Service_model->getAvailableServices();
$serviceFile = fopen("load_services.json", "w") or die("Unable to open file!");
fwrite($serviceFile, json_encode($available_services));
fclose($serviceFile);
$data['workers'] = $this->Service_model->getAllWorkers();
$data['selectedItem'] = $this->Service_model->getBookingById($booking_id);
$this->load->view('admin/update-booking', $data);
}
else{
header("Location:".SITEURL."login");
}
}
public function booking_process(){
$this->load->helper('url');
$this->load->model('User_model');
$this->load->model('Service_model');
$this->load->model('Log_model');
if(isset($_SESSION['username'])){
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
$data['pageTitle'] = '';
$data['message'] = '';
$data['message_class'] = '';
if(isset($_GET['delete-booking'])){
$selectedBooking = $this->Service_model->getBookingById($_GET['delete-booking']);
// Google Calendar: delete events before removing booking
$this->load->library('GoogleCalendar');
$this->config->load('google_calendar');
$delWorker = $this->Service_model->getWorkerById($selectedBooking->worker_id);
if(!empty($selectedBooking->gcal_event_id_worker) && $delWorker && !empty($delWorker->google_calendar_id)){
$this->googlecalendar->deleteEvent($delWorker->google_calendar_id, $selectedBooking->gcal_event_id_worker);
}
$evelinCalIdDel = $this->config->item('gcal_evelin_calendar_id');
if(!empty($selectedBooking->gcal_event_id_owner) && !empty($evelinCalIdDel)){
$this->googlecalendar->deleteEvent($evelinCalIdDel, $selectedBooking->gcal_event_id_owner);
}
// ntfy push notification
$this->_ntfy(
'Admin deleted booking: ' . $selectedBooking->guest_name,
'Worker: ' . ($delWorker ? $delWorker->worker_name : '?') . "\n" .
'Date: ' . $selectedBooking->booking_date . ' ' . $selectedBooking->booking_start_time,
($delWorker && isset($delWorker->ntfy_topic)) ? $delWorker->ntfy_topic : null
);
$this->Service_model->deleteBooking($_GET['delete-booking']);
$this->Log_model->addLog('bookings', $data['currentUser']->username.' törölte a(z) '.$_GET['delete-booking'].' számú foglalást ('.$selectedBooking->guest_name.', '.$selectedBooking->guest_email.', '.$selectedBooking->guest_phone.', '.$selectedBooking->booking_date.', '.$selectedBooking->booking_start_time.', '.$selectedBooking->booking_finish_time.', '.$selectedBooking->service_ids.')', $data['currentUser']->username);
header('Location:'.SITEURL.'bookings');
}
if(isset($_POST['addNewBookingManual'])){
$booking_date = new DateTime(date('Y-m-d', strtotime($_POST['booking_date'])));
$end_booking_date = new DateTime(date('Y-m-d', strtotime($_POST['end_booking_date'])));
$diff = $booking_date->diff($end_booking_date);
$selectedServiceArray = array();
if($_POST['service_ids'] != ''){
$selectedServiceArray = explode(',', $_POST['service_ids']);
}
if($_POST['booking_date'] != $_POST['end_booking_date'] && $booking_date <= $end_booking_date){
for($index = 0; $index <= $diff->days; $index++){
$selectedDate = date('Y-m-d', strtotime($_POST['booking_date']. ' + '.$index.' days'));
$manageToken = bin2hex(random_bytes(16));
$bookingArray = array(
'guest_name' => $_POST['guest_name'],
'guest_email' => $_POST['guest_email'],
'guest_phone' => $_POST['guest_phone'],
'worker_id' => $_POST['worker_id'],
'booking_date' => $selectedDate,
'booking_start_time' => $_POST['booking_start_time'],
'booking_finish_time' => $_POST['booking_finish_time'],
'service_ids' => serialize($selectedServiceArray),
'guest_confirmed' => '0',
'guest_confirm_code' => '1234',
'manage_token' => $manageToken,
);
$this->Service_model->createBooking($bookingArray);
}
}
else{
$manageToken = bin2hex(random_bytes(16));
$bookingArray = array(
'guest_name' => $_POST['guest_name'],
'guest_email' => $_POST['guest_email'],
'guest_phone' => $_POST['guest_phone'],
'worker_id' => $_POST['worker_id'],
'booking_date' => $_POST['booking_date'],
'booking_start_time' => $_POST['booking_start_time'],
'booking_finish_time' => $_POST['booking_finish_time'],
'service_ids' => serialize($selectedServiceArray),
'guest_confirmed' => '0',
'guest_confirm_code' => '1234',
'manage_token' => $manageToken,
);
$this->Log_model->addLog('bookings', $data['currentUser']->username.' létrehoza a(z) '.$_POST['guest_name'].' nevű vendég foglalást ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
$this->Service_model->createBooking($bookingArray);
}
if($_POST['send_notification']){
$worker = $this->Service_model->getWorkerById($bookingArray['worker_id']);
$services = unserialize($bookingArray['service_ids']);
$serviceArray = array();
$emailArray = array();
if(is_array($services)){
foreach($services as $serviceItem){
$selectedService = $this->Service_model->getServiceById($serviceItem);
if(is_object($selectedService)){
$serviceArray[] = $selectedService;
}
}
}
$content = '';
switch($_POST['lang']){
case 'no':
$content = 'Kjære gjest,<br />';
$content .= 'Takk for din bestilling!<br />';
$content .= '<br />';
$content .= '<table>';
$content .= '<tr><td>Navn: </td><td>'.$bookingArray['guest_name'].'</td></tr>';
$content .= '<tr><td>E-post: </td><td>'.$bookingArray['guest_email'].'</td></tr>';
$content .= '<tr><td>Telefon: </td><td>'.$bookingArray['guest_phone'].'</td></tr>';
$content .= '<tr><td>Arbeider: </td><td>'.$worker->worker_name.'</td></tr>';
$content .= '<tr><td>Dato: </td><td>'.$bookingArray['booking_date'].'</td></tr>';
$content .= '<tr><td>Tid: </td><td>'.$bookingArray['booking_start_time'].' - '.$bookingArray['booking_finish_time'].'</td></tr>';
$content .= '<tr><td>Tjenester: </td><td><table>';
$totalServicePrice = 0;
foreach($serviceArray as $serviceArrayItem){
$totalServicePrice += $serviceArrayItem->service_price;
$content .= '<tr><td>'.$serviceArrayItem->service_name_no.'</td><td>'.$serviceArrayItem->service_price.' kr</td></tr>';
}
$content .= '</table></td>';
$content .= '<tr><td>Totalpris: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr>';
$content .= '</table>';
$content .= '<br /><br /><a href="'.SITEURL.$_POST['lang'].'/manage-booking/'.$manageToken.'">Administrer bestilling</a><br />';
$content .= '<br /> Dersom noe skulle dukke opp og du ikke kan komme til avtalen din, vennligst gi oss beskjed minst 24 timer i forveien, slik at vi kan gi timen videre til en annen kunde og våre kollegers tid ikke blir stående ubrukt; avbestillinger innen 24 timer, eller manglende oppmøte, vil bli belastet med full pris for behandlingen; avbestilling kan gjøres via e-post til <a href="mailto:info@studiobeve.no">info@studiobeve.no</a> eller ved å sende oss en melding på Instagram eller Facebook. Takk for forståelsen og samarbeidet!';
$content .= '<br />';
$content .= '<br /> STUDIOBEVE';
$this->Log_model->addLog('bookings', $data['currentUser']->username.' új foglalásról norvég nyelvű értesítést küldött a(z) '.$bookingArray['guest_email'].' email címre ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
break;
case 'en':
$content = 'Dear Guest,<br />';
$content .= 'Thank you for your booking!<br />';
$content .= '<br />';
$content .= '<table>';
$content .= '<tr><td>Name: </td><td>'.$bookingArray['guest_name'].'</td></tr>';
$content .= '<tr><td>Email: </td><td>'.$bookingArray['guest_email'].'</td></tr>';
$content .= '<tr><td>Phone: </td><td>'.$bookingArray['guest_phone'].'</td></tr>';
$content .= '<tr><td>Worker: </td><td>'.$worker->worker_name.'</td></tr>';
$content .= '<tr><td>Date: </td><td>'.$bookingArray['booking_date'].'</td></tr>';
$content .= '<tr><td>Time: </td><td>'.$bookingArray['booking_start_time'].' - '.$bookingArray['booking_finish_time'].'</td></tr>';
$content .= '<tr><td>Services: </td><td><table>';
$totalServicePrice = 0;
foreach($serviceArray as $serviceArrayItem){
$totalServicePrice += $serviceArrayItem->service_price;
$content .= '<tr><td>'.$serviceArrayItem->service_name_en.'</td><td>'.$serviceArrayItem->service_price.' kr</td></tr>';
}
$content .= '</table></td>';
$content .= '<tr><td>Total price: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr>';
$content .= '</table>';
$content .= '<br /><br /><a href="'.SITEURL.$_POST['lang'].'/manage-booking/'.$manageToken.'">Manage booking</a><br />';
$content .= '<br /> If something comes up and you can’t make it to your booked appointment, please let us know at least 24 hours in advance so we can offer the time slot to another guest and our colleagues’ time won’t remain unused; cancellations within 24 hours or no-shows will be charged the full service fee; you can cancel by emailing <a href="mailto:info@studiobeve.no">info@studiobeve.no</a>, or by sending us a message on Instagram or Facebook. Thank you for your understanding and cooperation!';
$content .= '<br />';
$content .= '<br /> STUDIOBEVE';
$this->Log_model->addLog('bookings', $data['currentUser']->username.' új foglalásról angol nyelvű értesítést küldött a(z) '.$bookingArray['guest_email'].' email címre ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
break;
case 'hu':
$content = 'Kedves Vendégünk,<br />';
$content .= 'Köszönettel fogadtuk a foglalását!<br />';
$content .= '<br />';
$content .= '<table>';
$content .= '<tr><td>Név: </td><td>'.$bookingArray['guest_name'].'</td></tr>';
$content .= '<tr><td>Email: </td><td>'.$bookingArray['guest_email'].'</td></tr>';
$content .= '<tr><td>Telefon: </td><td>'.$bookingArray['guest_phone'].'</td></tr>';
$content .= '<tr><td>Dolgozó: </td><td>'.$worker->worker_name.'</td></tr>';
$content .= '<tr><td>Dátum: </td><td>'.$bookingArray['booking_date'].'</td></tr>';
$content .= '<tr><td>Időpont: </td><td>'.$bookingArray['booking_start_time'].' - '.$bookingArray['booking_finish_time'].'</td></tr>';
$content .= '<tr><td>Szolgáltatás(ok): </td><td><table>';
$totalServicePrice = 0;
foreach($serviceArray as $serviceArrayItem){
$totalServicePrice += $serviceArrayItem->service_price;
$content .= '<tr><td>'.$serviceArrayItem->service_name_hu.'</td><td>'.$serviceArrayItem->service_price.' kr</td></tr>';
}
$content .= '</table></td>';
$content .= '<tr><td>Összesen fizetendő: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr>';
$content .= '</table>';
$content .= '<br /><br /><a href="'.SITEURL.$_POST['lang'].'/manage-booking/'.$manageToken.'">Foglalás kezelése</a><br />';
$content .= '<br /> Kérünk, ha valami közbejön, és mégsem tudsz eljönni a lefoglalt időpontodra, jelezd nekünk legalább 24 órával előre, így más vendégnek is tudunk szabad időpontot biztosítani, és kollégáink ideje sem marad kihasználatlanul; 24 órán belüli lemondás, illetve meg nem jelenés esetén a szolgáltatás teljes díja felszámításra kerül; a lemondást az <a href="mailto:info@studiobeve.no">info@studiobeve.no</a> e-mail címen, vagy Instagram- és Facebook-üzenetben tudod megtenni. Köszönjük a megértést és az együttműködést!';
$content .= '<br />';
$content .= '<br /> STUDIOBEVE';
$this->Log_model->addLog('bookings', $data['currentUser']->username.' új foglalásról magyar nyelvű értesítést küldött a(z) '.$bookingArray['guest_email'].' email címre ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
break;
}
$data['selectedLang'] = $_POST['lang'];
$emailItem = array(
'addressee_email' => $bookingArray['guest_email'],
'addressee_name' => $bookingArray['guest_name'],
'subject' => '[studiobeve] Your booking has arrived',
'content' => $content,
'attachments' => array()
);
$emailArray[] = $emailItem;
$this->User_model->sendEmail($emailArray, $_POST['lang'], '');
}
header('Location:'.SITEURL.'bookings');
}
if(isset($_POST['updateBookingManual'])){
$selectedServiceArray = array();
if($_POST['service_ids'] != ''){
$selectedServiceArray = explode(',', $_POST['service_ids']);
}
$bookingArray = array(
'guest_name' => $_POST['guest_name'],
'guest_email' => $_POST['guest_email'],
'guest_phone' => $_POST['guest_phone'],
'worker_id' => $_POST['worker_id'],
'booking_date' => $_POST['booking_date'],
'booking_start_time' => $_POST['booking_start_time'],
'booking_finish_time' => $_POST['booking_finish_time'],
'service_ids' => serialize($selectedServiceArray),
'guest_confirmed' => '0',
'guest_confirm_code' => '1234'
);
$oldBooking = $this->Service_model->getBookingById($_POST['booking_id']);
$this->Service_model->updateBooking($_POST['booking_id'], $bookingArray);
$this->Log_model->addLog('bookings', $data['currentUser']->username.' módosította a foglalását a(z) '.$bookingArray['guest_name'].' nevű vendégnek ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
// Google Calendar: delete old events, create new ones
$this->load->library('GoogleCalendar');
$this->config->load('google_calendar');
$evelinCalId = $this->config->item('gcal_evelin_calendar_id');
if($oldBooking){
$oldWorker = $this->Service_model->getWorkerById($oldBooking->worker_id);
if(!empty($oldBooking->gcal_event_id_worker) && $oldWorker && !empty($oldWorker->google_calendar_id)){
$this->googlecalendar->deleteEvent($oldWorker->google_calendar_id, $oldBooking->gcal_event_id_worker);
}
if(!empty($oldBooking->gcal_event_id_owner) && !empty($evelinCalId)){
$this->googlecalendar->deleteEvent($evelinCalId, $oldBooking->gcal_event_id_owner);
}
}
$gcalWorker = $this->Service_model->getWorkerById($bookingArray['worker_id']);
$gcalServices = unserialize($bookingArray['service_ids']);
$gcalServiceArray = array();
if(is_array($gcalServices)){
foreach($gcalServices as $sid){
$svc = $this->Service_model->getServiceById($sid);
if(is_object($svc)) $gcalServiceArray[] = $svc;
}
}
$gcalServiceNames = implode(', ', array_map(function($s){ return $s->service_name_no; }, $gcalServiceArray));
$gcalTitle = $gcalWorker->worker_name . ' — ' . $gcalServiceNames . ' — ' . $bookingArray['guest_name'];
$gcalDescription = 'Guest: ' . $bookingArray['guest_name'] . "\nPhone: " . $bookingArray['guest_phone'] . "\nEmail: " . $bookingArray['guest_email'];
$gcalStart = $bookingArray['booking_date'] . 'T' . $bookingArray['booking_start_time'];
$gcalEnd = $bookingArray['booking_date'] . 'T' . $bookingArray['booking_finish_time'];
$newWorkerEventId = null;
$newOwnerEventId = null;
if(!empty($gcalWorker->google_calendar_id)){
$newWorkerEventId = $this->googlecalendar->createEvent($gcalWorker->google_calendar_id, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
}
if(!empty($evelinCalId)){
$newOwnerEventId = $this->googlecalendar->createEvent($evelinCalId, $gcalTitle, $gcalDescription, $gcalStart, $gcalEnd);
}
$this->Service_model->updateBookingCalEvents($_POST['booking_id'], $newWorkerEventId, $newOwnerEventId);
// ntfy push notification
$this->_ntfy(
'Admin modified booking: ' . $bookingArray['guest_name'],
'Worker: ' . $gcalWorker->worker_name . "\n" .
'Date: ' . $bookingArray['booking_date'] . ' ' . $bookingArray['booking_start_time'] . ' - ' . $bookingArray['booking_finish_time'] . "\n" .
'Services: ' . $gcalServiceNames,
isset($gcalWorker->ntfy_topic) ? $gcalWorker->ntfy_topic : null
);
if($_POST['send_notification']){
$worker = $this->Service_model->getWorkerById($bookingArray['worker_id']);
$services = unserialize($bookingArray['service_ids']);
$serviceArray = array();
$emailArray = array();
if(is_array($services)){
foreach($services as $serviceItem){
$selectedService = $this->Service_model->getServiceById($serviceItem);
if(is_object($selectedService)){
$serviceArray[] = $selectedService;
}
}
}
$manageToken = (isset($oldBooking) && !empty($oldBooking->manage_token)) ? $oldBooking->manage_token : '';
if(empty($manageToken)){
$manageToken = bin2hex(random_bytes(16));
$this->Service_model->updateBooking($_POST['booking_id'], array('manage_token' => $manageToken));
}
$content = '';
$email_subject ='';
switch($_POST['lang']){
case 'no':
$email_subject ='[studiobeve] Din reservasjon er endret';
$content = 'Kjære gjest,<br />';
$content .= 'Vi vil gjerne informere deg om at din reservasjon er endret.<br />';
$content .= '<br />';
$content .= '<table>';
$content .= '<tr><td>Navn: </td><td>'.$bookingArray['guest_name'].'</td></tr>';
$content .= '<tr><td>E-post: </td><td>'.$bookingArray['guest_email'].'</td></tr>';
$content .= '<tr><td>Telefon: </td><td>'.$bookingArray['guest_phone'].'</td></tr>';
$content .= '<tr><td>Arbeider: </td><td>'.$worker->worker_name.'</td></tr>';
$content .= '<tr><td>Dato: </td><td>'.$bookingArray['booking_date'].'</td></tr>';
$content .= '<tr><td>Tid: </td><td>'.$bookingArray['booking_start_time'].' - '.$bookingArray['booking_finish_time'].'</td></tr>';
$content .= '<tr><td>Tjenester: </td><td><table>';
$totalServicePrice = 0;
foreach($serviceArray as $serviceArrayItem){
$totalServicePrice += $serviceArrayItem->service_price;
$content .= '<tr><td>'.$serviceArrayItem->service_name_no.'</td><td>'.$serviceArrayItem->service_price.' kr</td></tr>';
}
$content .= '</table></td>';
$content .= '<tr><td>Totalpris: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr>';
$content .= '</table>';
$content .= '<br /><br /><a href="'.SITEURL.$_POST['lang'].'/manage-booking/'.$manageToken.'">Administrer bestilling</a><br />';
$content .= '<br /> Dersom noe skulle dukke opp og du ikke kan komme til avtalen din, vennligst gi oss beskjed minst 24 timer i forveien, slik at vi kan gi timen videre til en annen kunde og våre kollegers tid ikke blir stående ubrukt; avbestillinger innen 24 timer, eller manglende oppmøte, vil bli belastet med full pris for behandlingen; avbestilling kan gjøres via e-post til <a href="mailto:info@studiobeve.no">info@studiobeve.no</a> eller ved å sende oss en melding på Instagram eller Facebook. Takk for forståelsen og samarbeidet!';
$content .= '<br />';
$content .= '<br /> STUDIOBEVE';
$this->Log_model->addLog('bookings', $data['currentUser']->username.' foglalás módosításról norvég nyelvű értesítést küldött a(z) '.$bookingArray['guest_email'].' email címre ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
break;
case 'en':
$email_subject ='[studiobeve] Your booking has changed';
$content = 'Dear Guest,<br />';
$content .= 'We inform you that your booking has changed.<br />';
$content .= '<br />';
$content .= '<table>';
$content .= '<tr><td>Name: </td><td>'.$bookingArray['guest_name'].'</td></tr>';
$content .= '<tr><td>Email: </td><td>'.$bookingArray['guest_email'].'</td></tr>';
$content .= '<tr><td>Phone: </td><td>'.$bookingArray['guest_phone'].'</td></tr>';
$content .= '<tr><td>Worker: </td><td>'.$worker->worker_name.'</td></tr>';
$content .= '<tr><td>Date: </td><td>'.$bookingArray['booking_date'].'</td></tr>';
$content .= '<tr><td>Time: </td><td>'.$bookingArray['booking_start_time'].' - '.$bookingArray['booking_finish_time'].'</td></tr>';
$content .= '<tr><td>Services: </td><td><table>';
$totalServicePrice = 0;
foreach($serviceArray as $serviceArrayItem){
$totalServicePrice += $serviceArrayItem->service_price;
$content .= '<tr><td>'.$serviceArrayItem->service_name_en.'</td><td>'.$serviceArrayItem->service_price.' kr</td></tr>';
}
$content .= '</table></td>';
$content .= '<tr><td>Total price: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr>';
$content .= '</table>';
$content .= '<br /><br /><a href="'.SITEURL.$_POST['lang'].'/manage-booking/'.$manageToken.'">Manage booking</a><br />';
$content .= '<br /> If something comes up and you can’t make it to your booked appointment, please let us know at least 24 hours in advance so we can offer the time slot to another guest and our colleagues’ time won’t remain unused; cancellations within 24 hours or no-shows will be charged the full service fee; you can cancel by emailing <a href="mailto:info@studiobeve.no">info@studiobeve.no</a>, or by sending us a message on Instagram or Facebook. Thank you for your understanding and cooperation!';
$content .= '<br />';
$content .= '<br /> STUDIOBEVE';
$this->Log_model->addLog('bookings', $data['currentUser']->username.' foglalás módosításról angol nyelvű értesítést küldött a(z) '.$bookingArray['guest_email'].' email címre ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
break;
case 'hu':
$email_subject ='[studiobeve] A foglalása módosult';
$content = 'Kedves Vendégünk,<br />';
$content .= 'Tájékoztatjuk, hogy a foglalása módosult.<br />';
$content .= '<br />';
$content .= '<table>';
$content .= '<tr><td>Név: </td><td>'.$bookingArray['guest_name'].'</td></tr>';
$content .= '<tr><td>Email: </td><td>'.$bookingArray['guest_email'].'</td></tr>';
$content .= '<tr><td>Telefon: </td><td>'.$bookingArray['guest_phone'].'</td></tr>';
$content .= '<tr><td>Dolgozó: </td><td>'.$worker->worker_name.'</td></tr>';
$content .= '<tr><td>Dátum: </td><td>'.$bookingArray['booking_date'].'</td></tr>';
$content .= '<tr><td>Időpont: </td><td>'.$bookingArray['booking_start_time'].' - '.$bookingArray['booking_finish_time'].'</td></tr>';
$content .= '<tr><td>Szolgáltatás(ok): </td><td><table>';
$totalServicePrice = 0;
foreach($serviceArray as $serviceArrayItem){
$totalServicePrice += $serviceArrayItem->service_price;
$content .= '<tr><td>'.$serviceArrayItem->service_name_hu.'</td><td>'.$serviceArrayItem->service_price.' kr</td></tr>';
}
$content .= '</table></td>';
$content .= '<tr><td>Összesen fizetendő: </td><td><strong>'.$totalServicePrice.' kr</strong></td></tr>';
$content .= '</table>';
$content .= '<br /><br /><a href="'.SITEURL.$_POST['lang'].'/manage-booking/'.$manageToken.'">Foglalás kezelése</a><br />';
$content .= '<br /> Kérünk, ha valami közbejön, és mégsem tudsz eljönni a lefoglalt időpontodra, jelezd nekünk legalább 24 órával előre, így más vendégnek is tudunk szabad időpontot biztosítani, és kollégáink ideje sem marad kihasználatlanul; 24 órán belüli lemondás, illetve meg nem jelenés esetén a szolgáltatás teljes díja felszámításra kerül; a lemondást az <a href="mailto:info@studiobeve.no">info@studiobeve.no</a> e-mail címen, vagy Instagram- és Facebook-üzenetben tudod megtenni. Köszönjük a megértést és az együttműködést!';
$content .= '<br />';
$content .= '<br /> STUDIOBEVE';
$this->Log_model->addLog('bookings', $data['currentUser']->username.' foglalás módosításról magyar nyelvű értesítést küldött a(z) '.$bookingArray['guest_email'].' email címre ('.implode(', ', $bookingArray).')', $data['currentUser']->username);
break;
}
$data['selectedLang'] = $_POST['lang'];
$emailItem = array(
'addressee_email' => $bookingArray['guest_email'],
'addressee_name' => $bookingArray['guest_name'],
'subject' => $email_subject,
'content' => $content,
'attachments' => array()
);
$emailArray[] = $emailItem;
$this->User_model->sendEmail($emailArray, $_POST['lang'], '');
}
header('Location:'.SITEURL.'bookings');
}
$this->load->view('admin/booking-process', $data);
}
}
private function _ntfy($title, $message, $workerTopic = null){
$this->config->load('google_calendar');
$evelinTopic = $this->config->item('ntfy_topic');
$topics = [];
if(!empty($evelinTopic)) $topics[] = $evelinTopic;
if(!empty($workerTopic) && $workerTopic !== $evelinTopic) $topics[] = $workerTopic;
if(empty($topics)) return;
foreach($topics as $topic){
$ch = curl_init('https://ntfy.sh/' . $topic);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $message,
CURLOPT_HTTPHEADER => ['Title: ' . $title, 'Priority: high', 'Tags: calendar'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
curl_exec($ch);
curl_close($ch);
}
}
public function worker_calendar($worker_id = null) {
$this->load->helper('url');
$this->load->model('Service_model');
$this->load->model('User_model');
$this->load->model('Module_model');
if (!isset($_SESSION['username'])) {
header("Location:".SITEURL."login");
exit;
}
$data['currentUser'] = $this->User_model->getUserByUsername($_SESSION['username']);
if ($this->User_model->is_user_admin($_SESSION['username'])) {
$data['activeModules'] = $this->Module_model->getAllModule();
} else {
$data['activeModules'] = $this->Module_model->getUserModules($data['currentUser']->user_id);
}
$data['workers'] = $this->Service_model->getAllWorkers();
$data['selectedWorker'] = null;
$data['overridesByDate'] = [];
$data['scheduleRows'] = [];
$monthParam = $this->input->get('month');
if ($monthParam && preg_match('/^\d{4}-\d{2}$/', $monthParam)) {
list($year, $month) = explode('-', $monthParam);
$year = (int)$year;
$month = (int)$month;
} else {
$year = (int)date('Y');
$month = (int)date('m');
}
$data['year'] = $year;
$data['month'] = $month;
if ($worker_id) {
$data['selectedWorker'] = $this->Service_model->getWorkerById($worker_id);
$data['overridesByDate'] = $this->Service_model->getWorkerOverridesByMonth($worker_id, $year, $month);
$data['scheduleRows'] = $this->Service_model->getWorkerSchedule($worker_id);
}
// Compute lunch break slot for every working day of the month
$data['lunchByDate'] = [];
$sw = $data['selectedWorker'];
if ($sw && !empty($sw->lunch_window_start) && !empty($sw->lunch_window_end)) {
$monthBookings = $this->Service_model->getBookingsForWorkerMonth($worker_id, $year, $month);
$scheduleMap = [];
foreach ($data['scheduleRows'] as $row) {
$scheduleMap[$row->weekday] = $row;
}
$daysInMonth = (int)date('t', mktime(0, 0, 0, $month, 1, $year));
for ($d = 1; $d <= $daysInMonth; $d++) {
$dateStr = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-' . str_pad($d, 2, '0', STR_PAD_LEFT);
$override = isset($data['overridesByDate'][$dateStr]) ? $data['overridesByDate'][$dateStr] : null;
if ($override && $override->is_day_off) continue;
if ($override && !$override->is_day_off && $override->end_time) {
$shiftStart = new DateTime($dateStr . ' ' . $override->start_time);
$shiftEnd = new DateTime($dateStr . ' ' . $override->end_time);
} else {
$phpWeekday = (int)date('w', strtotime($dateStr));
if (!isset($scheduleMap[$phpWeekday])) continue;
$shiftStart = new DateTime($dateStr . ' ' . $scheduleMap[$phpWeekday]->start_time);
$shiftEnd = new DateTime($dateStr . ' ' . $scheduleMap[$phpWeekday]->end_time);
}
$dayBookings = isset($monthBookings[$dateStr]) ? $monthBookings[$dateStr] : [];
$lunch = $this->Service_model->computeLunchBreak(
$sw->lunch_window_start, $sw->lunch_window_end,
$dateStr, $dayBookings, $shiftEnd, $shiftStart,
isset($sw->lunch_preferred_time) ? $sw->lunch_preferred_time : null
);
if ($lunch) $data['lunchByDate'][$dateStr] = $lunch;
}
}
$data['pageTitle'] = 'Munkaidő naptár';
$this->load->view('admin/worker_calendar', $data);
}
public function store_calendar_override() {
$this->load->model('Service_model');
$this->load->helper('url');
if (!isset($_SESSION['username'])) {
header("Location:".SITEURL."login");
exit;
}
$worker_id = $this->input->post('worker_id');
$start_date = $this->input->post('start_date');
$end_date = $this->input->post('end_date') ?: $start_date;
$type = $this->input->post('override_type');
$start_time = $this->input->post('start_time');
$end_time = $this->input->post('end_time');
$note = $this->input->post('note');
$current = strtotime($start_date);
$last = strtotime($end_date);
$guard = 0;
while ($current <= $last && $guard < 366) {
$dateStr = date('Y-m-d', $current);
if ($type === 'remove') {
$this->Service_model->deleteWorkerScheduleOverrideByDate($worker_id, $dateStr);
} else {
$data = [];
if ($type === 'day_off') {
$data['is_day_off'] = 1;
$data['absence_type'] = null;
$data['start_time'] = null;
$data['end_time'] = null;
$data['note'] = null;
} elseif ($type === 'custom') {
$data['is_day_off'] = 0;
$data['absence_type'] = 'custom';
$data['start_time'] = $start_time;
$data['end_time'] = $end_time;
$data['note'] = $note;
} else {
// vacation, sick, other
$data['is_day_off'] = 1;
$data['absence_type'] = $type;
$data['start_time'] = null;
$data['end_time'] = null;
$data['note'] = $note;
}
$this->Service_model->upsertWorkerScheduleOverride($worker_id, $dateStr, $data);
}
$current = strtotime('+1 day', $current);
$guard++;
}
$monthParam = date('Y-m', strtotime($start_date));
redirect('workers/worker-calendar/' . $worker_id . '?month=' . $monthParam);
}
public function delete_calendar_override() {
$this->load->model('Service_model');
$this->load->helper('url');
if (!isset($_SESSION['username'])) {
header("Location:".SITEURL."login");
exit;
}
$worker_id = $this->input->post('worker_id');
$date = $this->input->post('date');
$this->Service_model->deleteWorkerScheduleOverrideByDate($worker_id, $date);
$monthParam = date('Y-m', strtotime($date));
redirect('workers/worker-calendar/' . $worker_id . '?month=' . $monthParam);
}
}