Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce7400578a | ||
|
|
13e1b1a672 | ||
|
|
4c823f92ca | ||
|
|
1a364f21b3 | ||
|
|
06088aee8b | ||
|
|
baa597a7c5 | ||
|
|
35115404be |
@@ -547,6 +547,13 @@ class Admin extends CI_Controller {
|
||||
$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{
|
||||
@@ -554,6 +561,48 @@ class Admin extends CI_Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
@@ -565,7 +614,16 @@ class Admin extends CI_Controller {
|
||||
$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(
|
||||
@@ -581,7 +639,12 @@ class Admin extends CI_Controller {
|
||||
|
||||
$data['worker_img'] = '';
|
||||
|
||||
if($_FILES["worker_img"]["tmp_name"] != ''){
|
||||
$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));
|
||||
@@ -632,7 +695,44 @@ class Admin extends CI_Controller {
|
||||
$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);
|
||||
|
||||
if($_FILES["worker_img"]["tmp_name"] != ''){
|
||||
// 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));
|
||||
|
||||
@@ -137,7 +137,15 @@ class Pages extends CI_Controller {
|
||||
$data['pageTitle'] = '';
|
||||
|
||||
if(isset($_POST['action']) && $_POST['action'] == 'getAvailableWorkersByServiceCategory' && isset($_POST['serv_cat_slug'])){
|
||||
$workers = $this->Service_model->getWorkersByCategorySlug($_POST['serv_cat_slug']);
|
||||
// The picker must only offer workers who can perform EVERY service the
|
||||
// guest has ticked. service_ids is supplied by the booking form; when it
|
||||
// is absent (older cached JS) the call degrades to the category-only
|
||||
// behaviour rather than returning nobody.
|
||||
$requestedServiceIds = isset($_POST['service_ids']) && is_array($_POST['service_ids'])
|
||||
? $_POST['service_ids']
|
||||
: array();
|
||||
|
||||
$workers = $this->Service_model->getWorkersByCategorySlug($_POST['serv_cat_slug'], $requestedServiceIds);
|
||||
$returnedWorkers = '';
|
||||
$workerIndex = 0;
|
||||
$workerListShow = '';
|
||||
@@ -307,7 +315,7 @@ class Pages extends CI_Controller {
|
||||
* form is a normal full-page POST, so whatever this outputs is what the
|
||||
* customer actually sees in their browser.
|
||||
*/
|
||||
private function _booking_error($errorCode, $statusCode = 409){
|
||||
private function _booking_error($errorCode, $statusCode = 409, $backUrl = ''){
|
||||
$this->load->helper('url');
|
||||
|
||||
$lang = vertical_lang(isset($_POST['lang']) ? $_POST['lang'] : '');
|
||||
@@ -324,7 +332,10 @@ class Pages extends CI_Controller {
|
||||
'selectedLang' => $lang,
|
||||
'subpage' => $vertical['slug'],
|
||||
'vertical' => $vertical,
|
||||
'bookingErrorCode' => $errorCode
|
||||
'bookingErrorCode' => $errorCode,
|
||||
// Manage-booking errors must return the guest to their own manage page,
|
||||
// not to a fresh booking form. Empty = the new-booking default.
|
||||
'bookingBackUrl' => $backUrl
|
||||
);
|
||||
|
||||
$this->output->set_status_header($statusCode);
|
||||
@@ -403,7 +414,24 @@ class Pages extends CI_Controller {
|
||||
'guest_confirm_code' => '1234',
|
||||
'manage_token' => $manageToken,
|
||||
);
|
||||
|
||||
|
||||
// Reject an unusable e-mail BEFORE anything is written. The booking is
|
||||
// saved well before sendEmail() runs, so without this the row is created,
|
||||
// PHPMailer's addAddress() throws, and the guest is redirected to the
|
||||
// success page having received nothing - no confirmation and no manage
|
||||
// link. Evelin is a CC on that same message, so the salon is not told
|
||||
// either. filter_var is equal-or-stricter than PHPMailer's own check, so
|
||||
// anything accepted here will not throw later.
|
||||
//
|
||||
// Public flow only: admin-created block bookings legitimately carry an
|
||||
// empty guest_email and go through Admin::booking_process().
|
||||
$guestEmailInput = isset($_POST['guest_email']) ? trim($_POST['guest_email']) : '';
|
||||
if ($guestEmailInput === '' || !filter_var($guestEmailInput, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->_booking_error('invalid_email', 400);
|
||||
return;
|
||||
}
|
||||
$bookingArray['guest_email'] = $guestEmailInput;
|
||||
|
||||
$bookingDate = $bookingArray['booking_date'];
|
||||
$bookingTime = $bookingArray['booking_start_time'];
|
||||
|
||||
@@ -428,6 +456,15 @@ class Pages extends CI_Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-worker capability. The category check above only proves the
|
||||
// services belong to the worker's category - not that this particular
|
||||
// worker performs them. The picker already hides incapable workers, but
|
||||
// that is a UI affordance, so re-check before the booking is written.
|
||||
if (!$this->Service_model->workerCanPerformServices($bookingArray['worker_id'], $selectedServiceArray)) {
|
||||
$this->_booking_error('service_not_offered', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
// Single-service verticals: the client disables the other checkboxes,
|
||||
// but that is a UI affordance only. The vertical is derived from the
|
||||
// services themselves rather than $_POST['subpage'], which is
|
||||
@@ -820,10 +857,14 @@ class Pages extends CI_Controller {
|
||||
// wrong vertical's branding.
|
||||
$serviceIds = unserialize($booking->service_ids);
|
||||
$subpage = 'barber';
|
||||
$firstService = NULL;
|
||||
if(is_array($serviceIds)){
|
||||
foreach($serviceIds as $serviceIdItem){
|
||||
$resolvedService = $this->Service_model->getServiceById($serviceIdItem);
|
||||
if($resolvedService && $resolvedService->service_type !== ''){
|
||||
// Kept for the worker filter below: the category (and therefore the
|
||||
// capability check) is derived from this same resolved service.
|
||||
$firstService = $resolvedService;
|
||||
$subpage = $resolvedService->service_type;
|
||||
break;
|
||||
}
|
||||
@@ -859,14 +900,14 @@ class Pages extends CI_Controller {
|
||||
// Filter workers to those qualified for the booking's service category,
|
||||
// so the guest can't switch to a worker who doesn't perform these services.
|
||||
$categorySlug = '';
|
||||
if(isset($firstService) && $firstService){
|
||||
if($firstService){
|
||||
$category = $this->Service_model->getServiceCategoryById($firstService->service_category_id);
|
||||
if($category){
|
||||
$categorySlug = $category->serv_cat_slug;
|
||||
}
|
||||
}
|
||||
if($categorySlug !== ''){
|
||||
$data['workers'] = $this->Service_model->getWorkersByCategorySlug($categorySlug);
|
||||
$data['workers'] = $this->Service_model->getWorkersByCategorySlug($categorySlug, is_array($serviceIds) ? $serviceIds : array());
|
||||
} else {
|
||||
$data['workers'] = $this->Service_model->getActiveWorkers($subpage);
|
||||
}
|
||||
@@ -894,11 +935,17 @@ class Pages extends CI_Controller {
|
||||
return;
|
||||
}
|
||||
|
||||
// This form is a normal POST, not AJAX, so every rejection below is rendered
|
||||
// as the shared booking error page. Returning raw JSON showed the guest an
|
||||
// unstyled {"error":...} blob after they had filled the whole form in.
|
||||
// Use the stored token, not the posted one: it is echoed straight into the
|
||||
// error page's back link.
|
||||
$manageBackUrl = SITEURL.vertical_lang(isset($_POST['lang']) ? $_POST['lang'] : '').'/manage-booking/'.$booking->manage_token;
|
||||
|
||||
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
||||
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
||||
if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Modification cutoff has passed.']));
|
||||
$this->_booking_error('cutoff_passed', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -929,27 +976,31 @@ class Pages extends CI_Controller {
|
||||
if(!empty($selectedServiceArray) && $submittedWorker){
|
||||
$firstSelectedService = $this->Service_model->getServiceById($selectedServiceArray[0]);
|
||||
if($firstSelectedService && $firstSelectedService->service_category_id != $submittedWorker->service_category_id){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Selected worker does not perform the chosen services.']));
|
||||
$this->_booking_error('category_mismatch', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-worker capability, same reasoning as booking_process(): the
|
||||
// category check proves only that the services belong to the worker's
|
||||
// category, not that this worker performs them.
|
||||
if(!$this->Service_model->workerCanPerformServices($newWorkerId, $selectedServiceArray)){
|
||||
$this->_booking_error('service_not_offered', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule check
|
||||
$schedule = $this->Service_model->getWorkerScheduleForDate($newWorkerId, $newBookingDate);
|
||||
if(!$schedule){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Worker is not available on this day.']));
|
||||
$this->_booking_error('worker_unavailable', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
if($newStartTime < $schedule->start_time || $newStartTime >= $schedule->end_time){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Booking time is outside worker schedule.']));
|
||||
$this->_booking_error('outside_schedule', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
if(!$this->Service_model->isWorkerAvailableThisWeek($newWorkerId, $newBookingDate)){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Worker only works every second week.']));
|
||||
$this->_booking_error('alternate_week', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -957,8 +1008,7 @@ class Pages extends CI_Controller {
|
||||
$available = $this->Service_model->getAvailableTimes($newWorkerId, $newBookingDate, $servicelength, $booking->booking_id);
|
||||
|
||||
if(!is_array($available) || !in_array($newStartTime, $available)){
|
||||
$this->output->set_status_header(409)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Conflict. Timeslot is taken or does not fit the service.']));
|
||||
$this->_booking_error('slot_taken', 409, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -967,8 +1017,7 @@ class Pages extends CI_Controller {
|
||||
$maxDate = (clone $today)->modify('+3 months');
|
||||
$selectedDate = new DateTime($newBookingDate);
|
||||
if($selectedDate > $maxDate){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Bookings can only be made up to 3 months in advance.']));
|
||||
$this->_booking_error('too_far', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -980,8 +1029,7 @@ class Pages extends CI_Controller {
|
||||
$bookingEnd->add(new DateInterval('PT'.intval($bookingLength->format('i')).'M'));
|
||||
$closingTime = new DateTime('18:00');
|
||||
if($bookingEnd > $closingTime){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Selected time exceeds business hours.']));
|
||||
$this->_booking_error('after_hours', 403, $manageBackUrl);
|
||||
return;
|
||||
}
|
||||
$finishTime = $bookingEnd->format('H:i:s');
|
||||
@@ -1143,8 +1191,7 @@ class Pages extends CI_Controller {
|
||||
$now = new DateTime('now', new DateTimeZone('Europe/Oslo'));
|
||||
$appointmentDT = new DateTime($booking->booking_date.' '.$booking->booking_start_time, new DateTimeZone('Europe/Oslo'));
|
||||
if(($appointmentDT->getTimestamp() - $now->getTimestamp()) < 86400){
|
||||
$this->output->set_status_header(403)->set_content_type('application/json')
|
||||
->set_output(json_encode(['error' => 'Cancellation cutoff has passed.']));
|
||||
$this->_booking_error('cutoff_passed', 403, SITEURL.vertical_lang($lang).'/manage-booking/'.$booking->manage_token);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -164,10 +164,25 @@ public function getModulesByPermissionSlug($permissionSlug){
|
||||
$this->load->model('Module_model');
|
||||
$selectedPermission = $this->Module_model->getPermissionBySlug($permissionSlug);
|
||||
|
||||
// An unknown slug yields no permission at all; without this the line below
|
||||
// dereferences null. No permission means no modules.
|
||||
if(!$selectedPermission){
|
||||
return array();
|
||||
}
|
||||
|
||||
$query = $this->db->query('SELECT * FROM modules;');
|
||||
$results = $query->result();
|
||||
$resultArray = array();
|
||||
foreach($results as $resultItem){
|
||||
// modules.permission_ids is NULL for at least one row (module 8,
|
||||
// 'Szerviz kategoriak') on every environment. Passing null to explode()
|
||||
// is deprecated in PHP 8.1 and emits a notice. A module with no
|
||||
// permissions is visible to nobody, so skipping it keeps the previous
|
||||
// behaviour - explode(',', null) returned array('') which matched nothing.
|
||||
if($resultItem->permission_ids === null || $resultItem->permission_ids === ''){
|
||||
continue;
|
||||
}
|
||||
|
||||
$permissionIdsArray = explode(',', $resultItem->permission_ids);
|
||||
if(in_array($selectedPermission->permission_id, $permissionIdsArray)){
|
||||
$resultArray[] = $resultItem;
|
||||
|
||||
@@ -127,20 +127,28 @@ class Service_model extends CI_Model {
|
||||
|
||||
$this->_applyLegacyVerticalFlags($workerArray, $workerArray['service_category_id']);
|
||||
|
||||
// is_active was previously omitted from this INSERT, so the column
|
||||
// default (1) always won and a worker created as inactive silently
|
||||
// came back active. The admin's choice is honoured here; callers that
|
||||
// do not supply it still get the old default.
|
||||
$is_active = isset($workerArray['is_active']) ? (int)$workerArray['is_active'] : 1;
|
||||
|
||||
$query = $this->db->query('INSERT INTO workers (
|
||||
worker_name,
|
||||
worker_profile_img,
|
||||
worker_info,
|
||||
is_beauty,
|
||||
is_barber,
|
||||
service_category_id
|
||||
) VALUES(?, ?, ?, ?, ?, ?)', array(
|
||||
service_category_id,
|
||||
is_active
|
||||
) VALUES(?, ?, ?, ?, ?, ?, ?)', array(
|
||||
$workerArray['worker_name'],
|
||||
$workerArray['worker_profile_img'],
|
||||
$workerArray['worker_info'],
|
||||
$workerArray['is_beauty'],
|
||||
$workerArray['is_barber'],
|
||||
$workerArray['service_category_id']
|
||||
$workerArray['service_category_id'],
|
||||
$is_active
|
||||
));
|
||||
}
|
||||
|
||||
@@ -243,6 +251,25 @@ class Service_model extends CI_Model {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Live services belonging to one category, ordered for display as the
|
||||
* admin capability grid (sub-heading, then name).
|
||||
*
|
||||
* @param int $service_category_id
|
||||
* @return array
|
||||
*/
|
||||
public function getServicesByCategoryId($service_category_id){
|
||||
$this->load->database();
|
||||
$query = $this->db->query(
|
||||
'SELECT * FROM services
|
||||
WHERE service_category_id = ? AND is_deleted != "1"
|
||||
ORDER BY service_type ASC, service_category_no ASC, service_id ASC',
|
||||
array($service_category_id)
|
||||
);
|
||||
|
||||
return $query->result();
|
||||
}
|
||||
|
||||
public function getAllServiceCategories(){
|
||||
$this->load->database();
|
||||
$query = $this->db->query('SELECT * FROM service_categories WHERE is_deleted != "1" ORDER BY service_category_id;');
|
||||
@@ -300,9 +327,161 @@ class Service_model extends CI_Model {
|
||||
return $query->result();
|
||||
}
|
||||
|
||||
public function getWorkersByCategorySlug($serv_cat_slug){
|
||||
/**
|
||||
* The service ids one worker is allowed to perform.
|
||||
*
|
||||
* An EMPTY result means "unrestricted" - the worker performs every live
|
||||
* service in their category, which is how every worker behaved before
|
||||
* worker_services existed. Restrictions are therefore opt-in per worker,
|
||||
* and a missing migration degrades to the old behaviour instead of
|
||||
* hiding everybody from the booking flow.
|
||||
*
|
||||
* @param int $worker_id
|
||||
* @return array list of service_id ints (empty = unrestricted)
|
||||
*/
|
||||
public function getWorkerServiceIds($worker_id){
|
||||
$this->load->database();
|
||||
$query = $this->db->query('SELECT * FROM workers JOIN service_categories ON workers.service_category_id = service_categories.service_category_id WHERE workers.is_deleted != "1" AND serv_cat_slug = ? ORDER BY worker_name ASC', array($serv_cat_slug));
|
||||
$query = $this->db->query(
|
||||
'SELECT service_id FROM worker_services WHERE worker_id = ? ORDER BY service_id ASC',
|
||||
array($worker_id)
|
||||
);
|
||||
|
||||
$ids = array();
|
||||
foreach($query->result() as $row){
|
||||
$ids[] = (int)$row->service_id;
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a worker's allowed-service set.
|
||||
*
|
||||
* Passing an empty array clears every row, which restores the worker to
|
||||
* the unrestricted default rather than making them unable to do anything.
|
||||
*
|
||||
* @param int $worker_id
|
||||
* @param array $service_ids
|
||||
* @return void
|
||||
*/
|
||||
public function setWorkerServiceIds($worker_id, $service_ids){
|
||||
$this->load->database();
|
||||
|
||||
$this->db->query('DELETE FROM worker_services WHERE worker_id = ?', array($worker_id));
|
||||
|
||||
if(!is_array($service_ids) || empty($service_ids)){
|
||||
return;
|
||||
}
|
||||
|
||||
// De-duplicate: the PK would reject repeats and abort the whole save.
|
||||
$clean = array();
|
||||
foreach($service_ids as $service_id){
|
||||
$service_id = (int)$service_id;
|
||||
if($service_id > 0){
|
||||
$clean[$service_id] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
foreach(array_keys($clean) as $service_id){
|
||||
$this->db->query(
|
||||
'INSERT INTO worker_services (worker_id, service_id) VALUES (?, ?)',
|
||||
array($worker_id, $service_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this worker perform EVERY one of these services?
|
||||
*
|
||||
* Used as the server-side guard behind the worker picker: the picker only
|
||||
* offers capable workers, but that is a UI affordance and a crafted POST
|
||||
* must not get past it.
|
||||
*
|
||||
* @param int $worker_id
|
||||
* @param array $service_ids
|
||||
* @return bool
|
||||
*/
|
||||
public function workerCanPerformServices($worker_id, $service_ids){
|
||||
if(!is_array($service_ids) || empty($service_ids)){
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
$allowed = $this->getWorkerServiceIds($worker_id);
|
||||
|
||||
// No rows at all = unrestricted worker.
|
||||
if(empty($allowed)){
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
foreach($service_ids as $service_id){
|
||||
if(!in_array((int)$service_id, $allowed, TRUE)){
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookable workers for one service category, optionally narrowed to those
|
||||
* able to perform a specific set of services.
|
||||
*
|
||||
* Both callers are guest-facing (the booking worker picker and the
|
||||
* manage-booking worker list), so an INACTIVE worker must not appear:
|
||||
* this used to filter on is_deleted alone, which meant switching a
|
||||
* worker to inactive in the admin had no effect on the public flow.
|
||||
* getActiveWorkers() - the fallback used by the very same caller when
|
||||
* no category is known - has always filtered on is_active; the two
|
||||
* paths now agree.
|
||||
*
|
||||
* @param string $serv_cat_slug
|
||||
* @param array $service_ids selected services; empty = no capability filter
|
||||
* @return array
|
||||
*/
|
||||
public function getWorkersByCategorySlug($serv_cat_slug, $service_ids = array()){
|
||||
$this->load->database();
|
||||
|
||||
$sql = 'SELECT w.* FROM workers w
|
||||
JOIN service_categories sc ON w.service_category_id = sc.service_category_id
|
||||
WHERE w.is_deleted != "1" AND w.is_active = "1" AND sc.serv_cat_slug = ?';
|
||||
$params = array($serv_cat_slug);
|
||||
|
||||
// Capability filter. A worker qualifies when they are unrestricted (no
|
||||
// worker_services rows at all) OR their allowed set covers EVERY
|
||||
// selected service - counting matched rows against the number asked
|
||||
// for, so a worker who can do 2 of the 3 chosen services is excluded.
|
||||
$clean = array();
|
||||
if(is_array($service_ids)){
|
||||
foreach($service_ids as $service_id){
|
||||
$service_id = (int)$service_id;
|
||||
if($service_id > 0){
|
||||
$clean[$service_id] = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
$clean = array_keys($clean);
|
||||
|
||||
if(!empty($clean)){
|
||||
$placeholders = implode(',', array_fill(0, count($clean), '?'));
|
||||
|
||||
$sql .= ' AND (
|
||||
NOT EXISTS (SELECT 1 FROM worker_services wsx WHERE wsx.worker_id = w.worker_id)
|
||||
OR (
|
||||
SELECT COUNT(*) FROM worker_services ws
|
||||
WHERE ws.worker_id = w.worker_id
|
||||
AND ws.service_id IN ('.$placeholders.')
|
||||
) = ?
|
||||
)';
|
||||
|
||||
foreach($clean as $service_id){
|
||||
$params[] = $service_id;
|
||||
}
|
||||
$params[] = count($clean);
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY w.worker_name ASC';
|
||||
|
||||
$query = $this->db->query($sql, $params);
|
||||
return $query->result();
|
||||
}
|
||||
|
||||
@@ -622,6 +801,10 @@ class Service_model extends CI_Model {
|
||||
$finishTime = new DateTime($selectedDate . ' ' . $schedule->end_time, new DateTimeZone('Europe/Oslo'));
|
||||
}
|
||||
|
||||
// A műszak valódi kezdete - $dayStartTime-ot a mai napra alább felülírjuk a legkorábban
|
||||
// foglalható időponttal, az ebédszünet számítása viszont a tényleges műszakkal dolgozik.
|
||||
$shiftStartTime = clone $dayStartTime;
|
||||
|
||||
// --- ÚJ: ma foglalva csak (következő 15 perces blokk + 1 óra) UTÁN legyen időpont ---
|
||||
if ($selectedDate == date('Y-m-d')) {
|
||||
// használd a helyi (Oslo) időt, ne az esetleges UTC defaultot
|
||||
@@ -648,7 +831,7 @@ class Service_model extends CI_Model {
|
||||
// --- Lunch break setup ---
|
||||
// Pre-fetch all bookings for the day so we can do hypothetical checks per slot
|
||||
$workerRow = $this->db->query("SELECT lunch_window_start, lunch_window_end, lunch_preferred_time FROM workers WHERE worker_id = ? LIMIT 1", array($worker_id))->row();
|
||||
$shiftSeconds = $finishTime->getTimestamp() - $dayStartTime->getTimestamp();
|
||||
$shiftSeconds = $finishTime->getTimestamp() - $shiftStartTime->getTimestamp();
|
||||
$lunchRequired = $workerRow && $workerRow->lunch_window_start && $workerRow->lunch_window_end && $shiftSeconds >= 6 * 3600;
|
||||
|
||||
$allBookings = [];
|
||||
@@ -705,7 +888,7 @@ class Service_model extends CI_Model {
|
||||
]]);
|
||||
$canStillBreak = $this->computeLunchBreak(
|
||||
$workerRow->lunch_window_start, $workerRow->lunch_window_end,
|
||||
$selectedDate, $hypo, $finishTime, $dayStartTime,
|
||||
$selectedDate, $hypo, $finishTime, $shiftStartTime,
|
||||
$workerRow->lunch_preferred_time
|
||||
) !== null;
|
||||
if (!$canStillBreak) continue; // booking this slot would eliminate the only break opportunity
|
||||
|
||||
@@ -132,7 +132,6 @@ class User_model extends CI_Model {
|
||||
public function sendEmail($addressList, $lang, $subpage){
|
||||
// Import PHPMailer classes into the global namespace
|
||||
// These must be at the top of your script, not inside a function
|
||||
echo $lang;
|
||||
if(!empty($addressList)){
|
||||
foreach($addressList as $addressListItem){
|
||||
// Instantiation and passing `true` enables exceptions
|
||||
@@ -179,7 +178,6 @@ class User_model extends CI_Model {
|
||||
//$mail->AltBody = strip_tags($answer_message);
|
||||
|
||||
$mail->send();
|
||||
echo 'Üzenet elküldve';
|
||||
if($subpage != ''){
|
||||
header('location:'.base_url().$lang.'/booking-finished/'.$subpage);
|
||||
}
|
||||
@@ -187,7 +185,10 @@ class User_model extends CI_Model {
|
||||
} catch (Exception $e) {
|
||||
//header('location:'.base_url().'manage-applicants/?email-sent=false&error='.$mail->ErrorInfo);
|
||||
log_message('error', 'sendEmail FAILED: ' . $mail->ErrorInfo);
|
||||
echo "Üzenetküldési hiba. Mailer Error: {$mail->ErrorInfo}";
|
||||
// Do NOT echo the mailer error. This runs after the booking is
|
||||
// saved and after the redirect header is queued, so it only leaks
|
||||
// Hungarian internals into a response the guest never reads.
|
||||
// The log_message() above is the record.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,144 @@
|
||||
<style>
|
||||
/* Capability grid. Scoped with a ws- prefix so it cannot leak into the other
|
||||
admin forms. Colours follow the existing admin palette: #9e8462 borders and
|
||||
headings, #cacaca body text on the dark background (see .tableClass). */
|
||||
.wsCapabilityRow {
|
||||
max-width: 760px;
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
|
||||
.wsSectionTitle {
|
||||
font-size: 15px;
|
||||
text-transform: uppercase;
|
||||
color: #9e8462;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.wsHint {
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #cacaca;
|
||||
text-align: center;
|
||||
margin: 0 auto 12px auto;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.wsToolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wsCount {
|
||||
font-size: 13px;
|
||||
color: #cacaca;
|
||||
}
|
||||
|
||||
/* main.css styles every <button> with height:3.5em, letter-spacing:.35em,
|
||||
font-weight:900 and an inset #585858 box-shadow, and forces the colour with
|
||||
!important - so each of those has to be undone explicitly here. */
|
||||
.wsMiniBtn {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: inset 0 0 0 1px #9e8462;
|
||||
color: #9e8462 !important;
|
||||
border-radius: 20px;
|
||||
height: auto;
|
||||
line-height: 1.4;
|
||||
padding: 5px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wsMiniBtn:hover {
|
||||
background: #9e8462;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.wsBox {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #9e8462;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
padding: 10px 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* The shared .tableClass centres itself with a 30px margin, which stacks badly
|
||||
when several of them sit inside one scroll box. */
|
||||
.wsTable {
|
||||
width: 100%;
|
||||
margin: 0 0 14px 0;
|
||||
}
|
||||
|
||||
.wsTable th {
|
||||
color: #9e8462;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #9e8462;
|
||||
text-align: left;
|
||||
padding: 6px 4px;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wsTable td {
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(158, 132, 98, 0.25);
|
||||
padding: 5px 4px;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
line-height: 1.35;
|
||||
color: #cacaca;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.wsTable .wsCheckCell {
|
||||
width: 34px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* modules.css already re-enables the native checkbox for .tableClass tables
|
||||
(main.css hides every checkbox with opacity:0 and draws a fake one via
|
||||
"+ label:before", which cannot work when the label sits in another cell).
|
||||
That rule outranks a plain .wsTable selector, so match its shape rather than
|
||||
fight it - only accent-color and height are added on top. */
|
||||
.wsTable > tbody > tr > td > input[type="checkbox"] {
|
||||
height: 18px;
|
||||
accent-color: #9e8462;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* The global label rule carries a 12.6px bottom margin, which was inflating
|
||||
every row to ~54px. */
|
||||
.wsNameCell label {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
line-height: 1.35;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.wsId {
|
||||
color: #7d7d7d;
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.wsBox { max-height: 300px; padding: 8px; }
|
||||
.wsTable td { font-size: 13px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="mainContentContainer" style="top:100px">
|
||||
<div class="pageTitle"><?php echo $pageTitle;?></div>
|
||||
<form method="post" action="<?php echo base_url();?>workers/worker-process" enctype="multipart/form-data">
|
||||
@@ -72,6 +213,76 @@
|
||||
<label class="formTitle">ntfy Topic</label>
|
||||
<input type="text" class="formInputBox" name="ntfy_topic" value="<?php echo htmlspecialchars(isset($selectedItem->ntfy_topic) ? $selectedItem->ntfy_topic : '', ENT_QUOTES, 'UTF-8');?>">
|
||||
</div>
|
||||
<div class="formRow wsCapabilityRow">
|
||||
<div class="wsSectionTitle">Végezhető szolgáltatások</div>
|
||||
<div class="wsHint">
|
||||
Csak a bepipált szolgáltatásokat ajánlja fel a foglalási rendszer ennél a dolgozónál.
|
||||
Ha <strong>mindegyik</strong> be van pipálva, a dolgozó korlátozás nélkül dolgozik, és a
|
||||
később felvett új szolgáltatásokat is automatikusan végezheti. Ha akár egyet is kiveszel,
|
||||
a dolgozó korlátozott lesz, és az ezután létrehozott új szolgáltatásokat külön be kell
|
||||
majd pipálni neki.
|
||||
</div>
|
||||
<?php
|
||||
// No stored rows = unrestricted, so show every box ticked. worker_services
|
||||
// is written back the same way: all ticked saves nothing at all.
|
||||
$wsIds = isset($workerServiceIds) && is_array($workerServiceIds) ? $workerServiceIds : array();
|
||||
$wsUnrestricted = empty($wsIds);
|
||||
$wsHasServices = isset($categoryServices) && is_array($categoryServices) && count($categoryServices) > 0;
|
||||
|
||||
if($wsHasServices){
|
||||
// Group first so each heading can be rendered as its own table.
|
||||
$wsGroups = array();
|
||||
foreach($categoryServices as $svc){
|
||||
$wsGroups[$svc->service_type.' - '.$svc->service_category_no][] = $svc;
|
||||
}
|
||||
?>
|
||||
<div class="wsToolbar">
|
||||
<span class="wsCount" id="wsCount"></span>
|
||||
<button type="button" class="wsMiniBtn" onclick="wsSetAll(true)">Mindet bepipál</button>
|
||||
<button type="button" class="wsMiniBtn" onclick="wsSetAll(false)">Mindet kivesz</button>
|
||||
</div>
|
||||
|
||||
<div class="wsBox">
|
||||
<?php
|
||||
foreach($wsGroups as $wsHeading => $wsRows){
|
||||
?>
|
||||
<table class="tableClass wsTable">
|
||||
<tr>
|
||||
<th colspan="2"><?php echo htmlspecialchars($wsHeading, ENT_QUOTES, 'UTF-8');?></th>
|
||||
</tr>
|
||||
<?php
|
||||
foreach($wsRows as $svc){
|
||||
$checked = $wsUnrestricted || in_array((int)$svc->service_id, $wsIds, TRUE);
|
||||
$svcName = $svc->service_name_hu !== '' ? $svc->service_name_hu : $svc->service_name_no;
|
||||
?>
|
||||
<tr>
|
||||
<td class="wsCheckCell">
|
||||
<input type="checkbox" class="wsService" id="wsService<?php echo (int)$svc->service_id;?>" name="worker_services[]" value="<?php echo (int)$svc->service_id;?>"<?php echo $checked ? ' checked' : '';?>>
|
||||
</td>
|
||||
<td class="wsNameCell">
|
||||
<label for="wsService<?php echo (int)$svc->service_id;?>">
|
||||
<?php echo htmlspecialchars($svcName, ENT_QUOTES, 'UTF-8');?>
|
||||
<span class="wsId">#<?php echo (int)$svc->service_id;?></span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
else{
|
||||
?>
|
||||
<div class="wsHint">Ehhez a kategóriához nincs szolgáltatás.</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<input type="submit" class="submitButton" name="updateWorker" value="Módosítás">
|
||||
<a href="<?php echo base_url();?>workers" class="cancelButton">Mégsem</a>
|
||||
@@ -79,3 +290,38 @@
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Keeps the counter honest and powers the two bulk buttons. All ticked is
|
||||
// meaningful state, not just a convenience: the controller stores it as
|
||||
// "no restriction" so the worker keeps getting future services too.
|
||||
function wsRefreshCount(){
|
||||
var boxes = document.querySelectorAll('.wsService');
|
||||
var ticked = document.querySelectorAll('.wsService:checked').length;
|
||||
var el = document.getElementById('wsCount');
|
||||
if(!el){ return; }
|
||||
|
||||
if(boxes.length > 0 && ticked === boxes.length){
|
||||
el.innerHTML = ticked + ' / ' + boxes.length + ' – korlátozás nélkül';
|
||||
}
|
||||
else{
|
||||
el.innerHTML = ticked + ' / ' + boxes.length + ' kiválasztva';
|
||||
}
|
||||
}
|
||||
|
||||
function wsSetAll(state){
|
||||
var boxes = document.querySelectorAll('.wsService');
|
||||
for(var i = 0; i < boxes.length; i++){
|
||||
boxes[i].checked = state;
|
||||
}
|
||||
wsRefreshCount();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var boxes = document.querySelectorAll('.wsService');
|
||||
for(var i = 0; i < boxes.length; i++){
|
||||
boxes[i].addEventListener('change', wsRefreshCount);
|
||||
}
|
||||
wsRefreshCount();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -100,6 +100,21 @@
|
||||
<img src="<?php echo SITEURL;?>assets/img/barber/evelin/4.jpg">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dolgozo-nev">
|
||||
<hr>
|
||||
<img src="<?php echo SITEURL;?>assets/img/barber/amarildo/amarildo.jpg" class="dolgozo-kep">
|
||||
<h4>AMARILDO Champimpi <span>- Barber</span></h4>
|
||||
</div>
|
||||
<div class="bemutatkozas-dolgozo no-gallery">
|
||||
<div class="bem-dolgozo-bal">
|
||||
<p>Amarildo here, originally from Albania. I have more than six years of experience as a barber, and throughout my career I have worked in both Greece and England. I have been living in Norway for the past three years, and I am now happy to join the Studio Beve team.</p>
|
||||
<p>I specialise in both modern and classic haircuts, as well as skin fades and taper fades. Precision and attention to detail are very important to me, and I always strive to achieve the best possible result for every client.</p>
|
||||
<p>Since I started working as a barber, my profession has become a true passion of mine. I continuously develop my skills, learn new techniques, and keep myself up to date with the latest trends and styles.</p>
|
||||
<p>For me, barbering is not simply about giving someone a haircut. It is important to create a style that truly suits each individual and to make sure every client leaves my chair feeling confident and satisfied.</p>
|
||||
<p>I am excited to bring the experience I have gained over the years, my dedication to the profession, and my creativity to Studio Beve, and I look forward to welcoming you to my chair soon.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -76,6 +76,21 @@
|
||||
<img src="<?php echo SITEURL;?>assets/img/barber/evelin/4.jpg">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dolgozo-nev">
|
||||
<hr>
|
||||
<img src="<?php echo SITEURL;?>assets/img/barber/amarildo/amarildo.jpg" class="dolgozo-kep">
|
||||
<h4>AMARILDO Champimpi <span>- Barber</span></h4>
|
||||
</div>
|
||||
<div class="bemutatkozas-dolgozo no-gallery">
|
||||
<div class="bem-dolgozo-bal">
|
||||
<p>Amarildo vagyok, és Albániából érkeztem. Több mint hatéves tapasztalattal rendelkezem barberként, pályafutásom során Görögországban és Angliában is dolgoztam. Három éve élek Norvégiában, és most örömmel csatlakozom a Studio Beve csapatához.</p>
|
||||
<p>Modern és klasszikus hajvágásokra, valamint skin fade és taper fade technikákra specializálódtam. A munkámban fontos számomra a precizitás és a részletekre való odafigyelés, és mindig arra törekszem, hogy minden vendégem számára a lehető legjobb eredményt érjem el.</p>
|
||||
<p>Amióta barberként dolgozom, a szakmám valódi szenvedélyemmé vált. Folyamatosan fejlesztem a tudásomat, új technikákat tanulok, és igyekszem naprakész maradni a legújabb trendekkel és stílusokkal.</p>
|
||||
<p>Számomra a barbering nem egyszerűen egy hajvágás elkészítéséről szól. Fontos, hogy olyan stílust alakítsak ki, amely valóban illik az adott vendéghez, és hogy mindenki magabiztosan és elégedetten távozzon a székemből.</p>
|
||||
<p>Örömmel hozom a Studio Bevébe az elmúlt években megszerzett tapasztalataimat, a szakmám iránti elkötelezettségemet és a kreativitásomat, és várom, hogy hamarosan téged is üdvözölhesselek a székemben.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -77,6 +77,21 @@
|
||||
<img src="<?php echo SITEURL;?>assets/img/barber/evelin/4.jpg">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dolgozo-nev">
|
||||
<hr>
|
||||
<img src="<?php echo SITEURL;?>assets/img/barber/amarildo/amarildo.jpg" class="dolgozo-kep">
|
||||
<h4>AMARILDO Champimpi <span>- Barber</span></h4>
|
||||
</div>
|
||||
<div class="bemutatkozas-dolgozo no-gallery">
|
||||
<div class="bem-dolgozo-bal">
|
||||
<p>Jeg heter Amarildo og kommer opprinnelig fra Albania. Jeg har mer enn seks års erfaring som barber, og gjennom karrieren min har jeg jobbet både i Hellas og England. Jeg har bodd i Norge de siste tre årene, og nå er jeg glad for å bli en del av teamet hos Studio Beve.</p>
|
||||
<p>Jeg spesialiserer meg på både moderne og klassiske hårklipper, samt skin fade og taper fade. Presisjon og sans for detaljer er svært viktig for meg, og jeg streber alltid etter å oppnå best mulig resultat for hver enkelt kunde.</p>
|
||||
<p>Siden jeg begynte å jobbe som barber, har yrket blitt en ekte lidenskap for meg. Jeg utvikler meg kontinuerlig, lærer nye teknikker og holder meg oppdatert på de nyeste trendene og stilene.</p>
|
||||
<p>For meg handler barberfaget ikke bare om å klippe håret. Det er viktig å skape en stil som virkelig passer den enkelte, og sørge for at hver kunde forlater stolen med en følelse av selvtillit og tilfredshet.</p>
|
||||
<p>Jeg gleder meg til å ta med meg erfaringen jeg har opparbeidet gjennom årene, engasjementet mitt for faget og kreativiteten min til Studio Beve, og jeg ser frem til å ønske deg velkommen i stolen min.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
'after_hours' => 'The treatment would not finish before closing time. Please choose an earlier time.',
|
||||
'too_far' => 'Bookings can only be made up to 3 months in advance.',
|
||||
'category_mismatch' => 'The selected staff member does not perform the chosen services. Please start again.',
|
||||
'service_not_offered' => 'The selected staff member does not perform all of the chosen treatments. Please pick another staff member or change the treatments.',
|
||||
'single_service_only' => 'Only one treatment can be booked at a time. Please select just one.',
|
||||
'invalid_email' => 'That e-mail address does not look valid. Please check it - your confirmation and the link to manage your booking are sent there.',
|
||||
'cutoff_passed' => 'Your booking is within 24 hours and can no longer be changed or cancelled. Please contact the salon.',
|
||||
'no_service' => 'No service was selected. Please choose at least one service.',
|
||||
'default' => 'Something went wrong with your booking. Please try again.',
|
||||
);
|
||||
@@ -17,12 +20,24 @@
|
||||
: $bookingErrorMessages['default'];
|
||||
|
||||
$backSubpage = isset($subpage) && $subpage != '' ? $subpage : 'barber';
|
||||
$backUrl = SITEURL.'en/booking/'.$backSubpage;
|
||||
|
||||
// _booking_error() passes a back URL when the error came from the
|
||||
// manage-booking flow; that guest must return to their own booking page
|
||||
// rather than being dropped into an empty new-booking form.
|
||||
$isManageError = isset($bookingBackUrl) && $bookingBackUrl !== '';
|
||||
$backUrl = $isManageError ? $bookingBackUrl : SITEURL.'en/booking/'.$backSubpage;
|
||||
$backLabel = $isManageError ? 'Back to your booking' : 'Back to booking';
|
||||
|
||||
if(isset($bookingErrorCode) && $bookingErrorCode === 'slot_taken'){
|
||||
$errorTitle = 'This time is no longer available';
|
||||
} else {
|
||||
$errorTitle = $isManageError ? 'Your booking could not be changed' : 'Your booking could not be completed';
|
||||
}
|
||||
?>
|
||||
<div class="bookingContainer" style="min-height:500px;">
|
||||
<div class="bookingThankYouTitle">This time is no longer available</div>
|
||||
<div class="bookingThankYouTitle"><?php echo $errorTitle;?></div>
|
||||
<div class="bookingThankYouMessageTitle"><?php echo $shownError;?></div>
|
||||
<div style="text-align:center; margin-top:30px;">
|
||||
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;">Back to booking</a>
|
||||
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;"><?php echo $backLabel;?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
'after_hours' => 'A kezelés nem érne véget zárásig. Kérjük, válasszon korábbi időpontot.',
|
||||
'too_far' => 'Foglalás legfeljebb 3 hónappal előre adható le.',
|
||||
'category_mismatch' => 'A kiválasztott kolléga nem végzi a kiválasztott szolgáltatásokat. Kérjük, válasszon újra.',
|
||||
'service_not_offered' => 'A kiválasztott kolléga nem végzi el az összes kiválasztott szolgáltatást. Kérjük, válasszon másik kollégát, vagy módosítsa a szolgáltatásokat.',
|
||||
'single_service_only' => 'Egyszerre csak egy kezelés foglalható. Kérjük, csak egyet válasszon.',
|
||||
'invalid_email' => 'Az e-mail cím nem tűnik érvényesnek. Kérjük, ellenőrizze - a visszaigazolást és a foglalás kezelésére szolgáló linket erre a címre küldjük.',
|
||||
'cutoff_passed' => 'A foglalás 24 órán belül van, ezért már nem módosítható és nem törölhető. Kérjük, vedd fel a kapcsolatot a szalonnal.',
|
||||
'no_service' => 'Nem választott ki szolgáltatást. Kérjük, válasszon legalább egyet.',
|
||||
'default' => 'Hiba történt a foglalás során. Kérjük, próbálja újra.',
|
||||
);
|
||||
@@ -17,12 +20,24 @@
|
||||
: $bookingErrorMessages['default'];
|
||||
|
||||
$backSubpage = isset($subpage) && $subpage != '' ? $subpage : 'barber';
|
||||
$backUrl = SITEURL.'hu/booking/'.$backSubpage;
|
||||
|
||||
// _booking_error() passes a back URL when the error came from the
|
||||
// manage-booking flow; that guest must return to their own booking page
|
||||
// rather than being dropped into an empty new-booking form.
|
||||
$isManageError = isset($bookingBackUrl) && $bookingBackUrl !== '';
|
||||
$backUrl = $isManageError ? $bookingBackUrl : SITEURL.'hu/booking/'.$backSubpage;
|
||||
$backLabel = $isManageError ? 'Vissza a foglalásodhoz' : 'Vissza a foglaláshoz';
|
||||
|
||||
if(isset($bookingErrorCode) && $bookingErrorCode === 'slot_taken'){
|
||||
$errorTitle = 'Ez az időpont már nem foglalható';
|
||||
} else {
|
||||
$errorTitle = $isManageError ? 'A foglalást nem sikerült módosítani' : 'A foglalást nem sikerült véglegesíteni';
|
||||
}
|
||||
?>
|
||||
<div class="bookingContainer" style="min-height:500px;">
|
||||
<div class="bookingThankYouTitle">Ez az időpont már nem foglalható</div>
|
||||
<div class="bookingThankYouTitle"><?php echo $errorTitle;?></div>
|
||||
<div class="bookingThankYouMessageTitle"><?php echo $shownError;?></div>
|
||||
<div style="text-align:center; margin-top:30px;">
|
||||
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;">Vissza a foglaláshoz</a>
|
||||
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;"><?php echo $backLabel;?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
'after_hours' => 'Behandlingen rekker ikke å bli ferdig innen stengetid. Vennligst velg et tidligere tidspunkt.',
|
||||
'too_far' => 'Du kan kun bestille time inntil 3 måneder frem i tid.',
|
||||
'category_mismatch' => 'Den valgte medarbeideren utfører ikke de valgte tjenestene. Vennligst velg på nytt.',
|
||||
'service_not_offered' => 'Den valgte medarbeideren utfører ikke alle de valgte behandlingene. Vennligst velg en annen medarbeider eller endre behandlingene.',
|
||||
'single_service_only' => 'Du kan kun bestille én behandling om gangen. Vennligst velg kun én.',
|
||||
'invalid_email' => 'E-postadressen ser ikke ut til å være gyldig. Vennligst kontroller den - bekreftelsen og lenken for å endre bestillingen sendes dit.',
|
||||
'cutoff_passed' => 'Bestillingen er innen 24 timer og kan ikke lenger endres eller avbestilles. Vennligst ta kontakt med salongen.',
|
||||
'no_service' => 'Du har ikke valgt noen tjeneste. Vennligst velg minst én tjeneste.',
|
||||
'default' => 'Noe gikk galt med bestillingen. Vennligst prøv igjen.',
|
||||
);
|
||||
@@ -17,12 +20,24 @@
|
||||
: $bookingErrorMessages['default'];
|
||||
|
||||
$backSubpage = isset($subpage) && $subpage != '' ? $subpage : 'barber';
|
||||
$backUrl = SITEURL.'no/booking/'.$backSubpage;
|
||||
|
||||
// _booking_error() passes a back URL when the error came from the
|
||||
// manage-booking flow; that guest must return to their own booking page
|
||||
// rather than being dropped into an empty new-booking form.
|
||||
$isManageError = isset($bookingBackUrl) && $bookingBackUrl !== '';
|
||||
$backUrl = $isManageError ? $bookingBackUrl : SITEURL.'no/booking/'.$backSubpage;
|
||||
$backLabel = $isManageError ? 'Tilbake til bestillingen din' : 'Tilbake til bestilling';
|
||||
|
||||
if(isset($bookingErrorCode) && $bookingErrorCode === 'slot_taken'){
|
||||
$errorTitle = 'Tidspunktet er ikke lenger tilgjengelig';
|
||||
} else {
|
||||
$errorTitle = $isManageError ? 'Bestillingen kunne ikke endres' : 'Bestillingen kunne ikke fullføres';
|
||||
}
|
||||
?>
|
||||
<div class="bookingContainer" style="min-height:500px;">
|
||||
<div class="bookingThankYouTitle">Tidspunktet er ikke lenger tilgjengelig</div>
|
||||
<div class="bookingThankYouTitle"><?php echo $errorTitle;?></div>
|
||||
<div class="bookingThankYouMessageTitle"><?php echo $shownError;?></div>
|
||||
<div style="text-align:center; margin-top:30px;">
|
||||
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;">Tilbake til bestilling</a>
|
||||
<a href="<?php echo $backUrl;?>" style="display:inline-block; padding:12px 28px; background:#000; color:#fff; text-decoration:none; border-radius:5px; font-size:16px;"><?php echo $backLabel;?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label class="bookingEmailTitle">E-mail:</label>
|
||||
<input type="text" class="bookingEmail" name="guest_email" value="" required>
|
||||
<input type="email" class="bookingEmail" name="guest_email" value="" required>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label class="bookingPhoneTitle">Phone:</label>
|
||||
@@ -364,12 +364,19 @@
|
||||
}
|
||||
|
||||
function getAvailableWorkersByServiceCategorySlug(categorySlug){
|
||||
// Only workers who can perform EVERY ticked service may be offered.
|
||||
// The checkbox id IS the service_id (see the service list markup).
|
||||
var selectedServiceIds = $('.service:checked').map(function(){
|
||||
return this.id;
|
||||
}).get();
|
||||
|
||||
$.ajax({
|
||||
url: '<?php echo base_url();?>ajax',
|
||||
type: 'POST',
|
||||
data: {
|
||||
action:'getAvailableWorkersByServiceCategory',
|
||||
serv_cat_slug:categorySlug
|
||||
serv_cat_slug:categorySlug,
|
||||
service_ids:selectedServiceIds
|
||||
},
|
||||
error: function() {
|
||||
},
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label class="bookingEmailTitle">E-mail:</label>
|
||||
<input type="text" class="bookingEmail" name="guest_email" value="" required>
|
||||
<input type="email" class="bookingEmail" name="guest_email" value="" required>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label class="bookingPhoneTitle">Telefon:</label>
|
||||
@@ -365,12 +365,19 @@
|
||||
}
|
||||
|
||||
function getAvailableWorkersByServiceCategorySlug(categorySlug){
|
||||
// Only workers who can perform EVERY ticked service may be offered.
|
||||
// The checkbox id IS the service_id (see the service list markup).
|
||||
var selectedServiceIds = $('.service:checked').map(function(){
|
||||
return this.id;
|
||||
}).get();
|
||||
|
||||
$.ajax({
|
||||
url: '<?php echo base_url();?>ajax',
|
||||
type: 'POST',
|
||||
data: {
|
||||
action:'getAvailableWorkersByServiceCategory',
|
||||
serv_cat_slug:categorySlug
|
||||
serv_cat_slug:categorySlug,
|
||||
service_ids:selectedServiceIds
|
||||
},
|
||||
error: function() {
|
||||
},
|
||||
|
||||
@@ -178,7 +178,7 @@ if (is_array($services) && count($services) > 0) {
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label class="bookingEmailTitle">E-mail:</label>
|
||||
<input type="text" class="bookingEmail" name="guest_email" value="" required>
|
||||
<input type="email" class="bookingEmail" name="guest_email" value="" required>
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label class="bookingPhoneTitle">Telefon:</label>
|
||||
@@ -363,12 +363,19 @@ if (is_array($services) && count($services) > 0) {
|
||||
}
|
||||
|
||||
function getAvailableWorkersByServiceCategorySlug(categorySlug){
|
||||
// Only workers who can perform EVERY ticked service may be offered.
|
||||
// The checkbox id IS the service_id (see the service list markup).
|
||||
var selectedServiceIds = $('.service:checked').map(function(){
|
||||
return this.id;
|
||||
}).get();
|
||||
|
||||
$.ajax({
|
||||
url: '<?php echo base_url();?>ajax',
|
||||
type: 'POST',
|
||||
data: {
|
||||
action:'getAvailableWorkersByServiceCategory',
|
||||
serv_cat_slug:categorySlug
|
||||
serv_cat_slug:categorySlug,
|
||||
service_ids:selectedServiceIds
|
||||
},
|
||||
error: function() {
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
'back' => 'Tilbake',
|
||||
'send' => 'Bekreft endring',
|
||||
'no_times' => 'Ingen ledige tider denne dagen!',
|
||||
'no_workers' => 'Ingen medarbeidere utfører alle de valgte tjenestene.',
|
||||
'expired_msg' => 'Denne bestillingen er allerede passert og kan ikke endres.',
|
||||
'cutoff_msg' => 'Bestillingen er innen 24 timer og kan ikke endres eller avbestilles.',
|
||||
),
|
||||
@@ -53,6 +54,7 @@
|
||||
'back' => 'Vissza',
|
||||
'send' => 'Módosítás megerősítése',
|
||||
'no_times' => 'Ezen a napon nincs szabad időpont!',
|
||||
'no_workers' => 'Egyik kolléga sem végzi el az összes kiválasztott szolgáltatást.',
|
||||
'expired_msg' => 'Ez a foglalás már elmúlt, nem módosítható.',
|
||||
'cutoff_msg' => 'A foglalás 24 órán belül van, nem módosítható vagy törölhető.',
|
||||
),
|
||||
@@ -76,6 +78,7 @@
|
||||
'back' => 'Back',
|
||||
'send' => 'Confirm changes',
|
||||
'no_times' => 'There is no available time on this day!',
|
||||
'no_workers' => 'No staff member performs all of the selected services.',
|
||||
'expired_msg' => 'This booking has already passed and cannot be modified.',
|
||||
'cutoff_msg' => 'Your booking is within 24 hours and can no longer be changed or cancelled.',
|
||||
),
|
||||
@@ -246,6 +249,7 @@
|
||||
onsubmit="return confirm('<?php echo addslashes($l['cancel_confirm']); ?>');">
|
||||
<input type="hidden" name="token" value="<?php echo htmlspecialchars($token); ?>">
|
||||
<input type="hidden" name="lang" value="<?php echo htmlspecialchars($lang); ?>">
|
||||
<input type="hidden" name="subpage" value="<?php echo htmlspecialchars($subpage); ?>">
|
||||
<button type="submit" class="manage-cancel-btn"><?php echo $l['cancel_btn']; ?></button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -445,6 +449,7 @@
|
||||
|
||||
<script>
|
||||
var noTimesMsg = '<?php echo addslashes($l['no_times']); ?>';
|
||||
var noWorkersMsg = '<?php echo addslashes($l['no_workers']); ?>';
|
||||
|
||||
$(document).ready(function(){
|
||||
recalcServices();
|
||||
@@ -543,16 +548,30 @@
|
||||
}
|
||||
|
||||
function getAvailableWorkersByServiceCategorySlug(categorySlug){
|
||||
// Same rule as the new-booking form: only workers who can perform EVERY
|
||||
// ticked service may be offered. The checkbox id IS the service_id.
|
||||
var selectedServiceIds = $('.service:checked').map(function(){
|
||||
return this.id;
|
||||
}).get();
|
||||
|
||||
$.ajax({
|
||||
url: '<?php echo base_url(); ?>ajax',
|
||||
type: 'POST',
|
||||
data: { action: 'getAvailableWorkersByServiceCategory', serv_cat_slug: categorySlug },
|
||||
data: {
|
||||
action: 'getAvailableWorkersByServiceCategory',
|
||||
serv_cat_slug: categorySlug,
|
||||
service_ids: selectedServiceIds
|
||||
},
|
||||
dataType: 'json',
|
||||
}).done(function(result){
|
||||
$('#workerTable').html(result.workerListShow);
|
||||
if(result.workers && result.workers.length > 0){
|
||||
$('#workerTable').html(result.workerListShow);
|
||||
setWorker(result.workers[0].worker_id);
|
||||
}
|
||||
else{
|
||||
$('#workerTable').html('<div class="noWorkerMessage">' + noWorkersMsg + '</div>');
|
||||
setWorker('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -645,6 +664,13 @@
|
||||
function setWorker(worker_id){
|
||||
$('#worker_id').val(worker_id);
|
||||
$('.serviceBookingWorkerContainer').hide().html('');
|
||||
|
||||
// The worker (or the service set that produced this list) has changed, so
|
||||
// a time picked for the previous one is stale. Clearing it also disables
|
||||
// the submit button via the #booking_time change handler.
|
||||
$('#booking_time').val('').trigger('change');
|
||||
$('#serviceBookingTime').html('').hide();
|
||||
$('.availableBookingTime').removeClass('selectedTime');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3364,6 +3364,11 @@ input, select, textarea {
|
||||
|
||||
}
|
||||
|
||||
/* Worker bio with no photo strip alongside - let the text use the full width. */
|
||||
.bemutatkozas-dolgozo.no-gallery .bem-dolgozo-bal{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Treatments - Our Services */
|
||||
|
||||
@@ -3297,6 +3297,12 @@ input, select, textarea {
|
||||
text-align: right;
|
||||
/* was `margin: auto` - on a flex item that centres it on BOTH axes. */
|
||||
margin: 0;
|
||||
/* Optical alignment with the bio text. Both columns are flex-start, so
|
||||
their boxes DO start level - but body text is 13pt/1.75em, which puts
|
||||
6.5px of half-leading above the first line's glyphs, while an inline
|
||||
image starts flush at the box top. Without this the strip reads as
|
||||
sitting high. (1.75em - 1em) / 2 = 0.375em = that same 6.5px. */
|
||||
padding-top: 0.375em;
|
||||
|
||||
}
|
||||
|
||||
@@ -4488,7 +4494,9 @@ input, select, textarea {
|
||||
|
||||
.bemutatkozas-dolgozo{display: block;}
|
||||
.bem-dolgozo-bal {width: 100%;}
|
||||
.bem-dolgozo-jobb {width: 100%; float: none; text-align: center;}
|
||||
/* Container is display:block here, so the strip no longer sits
|
||||
beside the text and the half-leading correction is unwanted. */
|
||||
.bem-dolgozo-jobb {width: 100%; float: none; text-align: center; padding-top: 0;}
|
||||
|
||||
#czcategorylistblock .czcategorylist-wrapper .category_lists .one-half .category-content {width: 100%; }
|
||||
.one-half{width:100%; float: none;}
|
||||
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |