feat: redesign tickets main view and basket view with improved styles and layout

- Updated SCSS for tickets main view and basket view, enhancing visual design and responsiveness.
- Added new calendar functionality in the admin panel for managing ticket availability.
- Implemented error handling for ticket availability in the basket view.
- Introduced a new calendar availability table in the database for tracking ticket dates.
- Added a new link to the calendar in the logged-in user navigation.
- Improved ticket selection logic to disable unavailable tickets in the main view.
- Created guidelines for code standards in AGENTS.md.
This commit is contained in:
2026-03-06 01:06:43 +01:00
parent 238c9ecaed
commit 38f5babde3
14 changed files with 2218 additions and 261 deletions

View File

@@ -115,3 +115,10 @@ initial_prompt: ""
# override of the corresponding setting in serena_config.yml, see the documentation there. # override of the corresponding setting in serena_config.yml, see the documentation there.
# If null or missing, the value from the global config is used. # If null or missing, the value from the global config is used.
symbol_info_budget: symbol_info_budget:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:

File diff suppressed because it is too large Load Diff

14
AGENTS.md Normal file
View File

@@ -0,0 +1,14 @@
# Zasady pisania kodu
- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii
- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności
- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 3050 linii (jeśli dłuższe dzielić)
- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod
- Nazewnictwo:
- klasy: PascalCase
- metody/zmienne: camelCase
- stałe: UPPER_SNAKE_CASE
- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 23 linijki
- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem)
- XSS: escape w widokach (np. helper e())
- CSRF dla formularzy, sensowna obsługa sesji
- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co”

View File

@@ -276,4 +276,67 @@ class Apanel
echo json_encode(['status' => 'ok']); echo json_encode(['status' => 'ok']);
exit; exit;
} }
}
static public function calendar()
{
$token = bin2hex(random_bytes(32));
\S::set_session('admin_calendar_csrf', $token);
return \Tpl::view('admin-panel/calendar', [
'calendar_groups' => \factory\Tickets::getCalendarDefinitions(),
'csrf_token' => $token
]);
}
static public function calendar_dates()
{
header('Content-Type: application/json; charset=utf-8');
$groupKey = trim((string) \S::get('ticket_group'));
$definitions = \factory\Tickets::getCalendarDefinitions();
if (!isset($definitions[$groupKey])) {
echo json_encode(['status' => 'error', 'message' => 'Niepoprawny rodzaj biletu.']);
exit;
}
$dates = \factory\Tickets::getEnabledDatesByGroup($groupKey);
echo json_encode([
'status' => 'ok',
'enabled_dates' => $dates
]);
exit;
}
static public function calendar_save()
{
header('Content-Type: application/json; charset=utf-8');
$sessionToken = (string) \S::get_session('admin_calendar_csrf');
$requestToken = trim((string) \S::get('csrf_token'));
if (empty($sessionToken) || empty($requestToken) || !hash_equals($sessionToken, $requestToken)) {
echo json_encode(['status' => 'error', 'message' => 'Niepoprawny token CSRF.']);
exit;
}
$groupKey = trim((string) \S::get('ticket_group'));
$definitions = \factory\Tickets::getCalendarDefinitions();
if (!isset($definitions[$groupKey])) {
echo json_encode(['status' => 'error', 'message' => 'Niepoprawny rodzaj biletu.']);
exit;
}
$dates = $_POST['dates'] ?? [];
if (!is_array($dates)) {
$dates = [];
}
$saved = \factory\Tickets::saveEnabledDatesForGroup($groupKey, $dates);
if (!$saved) {
echo json_encode(['status' => 'error', 'message' => 'Nie udało się zapisać kalendarza.']);
exit;
}
echo json_encode(['status' => 'ok']);
exit;
}
}

View File

@@ -9,13 +9,24 @@ class Tickets
global $settings; global $settings;
$enable_sell = \factory\Apanel::getSetting('enable_sell', '1'); $enable_sell = \factory\Apanel::getSetting('enable_sell', '1');
$selectedDate = \S::get('selected_date');
$normalizedSelectedDate = \factory\Tickets::normalizeDateFromPicker($selectedDate);
$ticketAvailability = [];
if ($normalizedSelectedDate) {
$ticketAvailability = \factory\Tickets::getTicketAvailabilityForDate(
array_keys($settings['tickets']),
$normalizedSelectedDate
);
}
if ( !$enable_sell || $enable_sell === '0' ) if ( !$enable_sell || $enable_sell === '0' )
return \Tpl::view( 'tickets/disabled-sell' ); return \Tpl::view( 'tickets/disabled-sell' );
return \Tpl::view('tickets/main-view', [ return \Tpl::view('tickets/main-view', [
'cart' => \S::get_session('basket'), 'cart' => \S::get_session('basket'),
'settings' => $settings 'settings' => $settings,
'ticket_availability' => $ticketAvailability
]); ]);
} }
@@ -23,19 +34,45 @@ class Tickets
{ {
global $settings; global $settings;
$selected_date = \S::get( 'date' ) ? \S::get( 'date' ) : null; $selected_date = \S::get( 'date' ) ? \S::get( 'date' ) : null;
$diffDays = \S::get('diffdays') ?? 0; $diffDays = \S::get('diffdays') ?? 0;
$ticket_id = \S::get('ticket_id');
$basket = \S::get_session('basket');
if (!$selected_date && isset($basket[$ticket_id][$diffDays]['date'])) {
$selected_date = $basket[$ticket_id][$diffDays]['date'];
}
if (!$selected_date && \factory\Tickets::getCalendarGroupForTicket($ticket_id)) {
echo json_encode([
'status' => 'error',
'message' => 'Wybierz datę wizyty dla tego biletu.'
]);
exit;
}
if ( $selected_date ) if ( $selected_date )
{ {
$selected = new \DateTime($selected_date); $selected = \DateTime::createFromFormat('d-m-Y', $selected_date);
if (!$selected) {
echo json_encode([
'status' => 'error',
'message' => 'Niepoprawna data wizyty.'
]);
exit;
}
$today = new \DateTime(date('Y-m-d') . ' 00:00:00'); $today = new \DateTime(date('Y-m-d') . ' 00:00:00');
$diffDays = $selected->diff($today)->days; $diffDays = $selected->diff($today)->days;
}
$basket = \S::get_session('basket'); if (!\factory\Tickets::canBuyTicketOnDate($ticket_id, $selected_date)) {
$ticket_id = \S::get('ticket_id'); echo json_encode([
'status' => 'error',
'message' => 'Ten rodzaj biletu jest niedostępny w wybranym dniu.'
]);
exit;
}
}
if ( !empty($basket[$ticket_id][$diffDays]) ) if ( !empty($basket[$ticket_id][$diffDays]) )
{ {
@@ -53,7 +90,8 @@ class Tickets
$addon = 0; $addon = 0;
} }
$is_weekend = $selected_date && in_array((new \DateTime($selected_date))->format('N'), ['6', '7']); $selectedDateObject = $selected_date ? \DateTime::createFromFormat('d-m-Y', $selected_date) : null;
$is_weekend = $selectedDateObject && in_array($selectedDateObject->format('N'), ['6', '7']);
$base_price = $is_weekend $base_price = $is_weekend
? ($settings['tickets'][$ticket_id]['price_weekend'] ?? $settings['tickets'][$ticket_id]['price']) ? ($settings['tickets'][$ticket_id]['price_weekend'] ?? $settings['tickets'][$ticket_id]['price'])
: $settings['tickets'][$ticket_id]['price']; : $settings['tickets'][$ticket_id]['price'];
@@ -228,6 +266,24 @@ class Tickets
global $settings, $mdb; global $settings, $mdb;
$basket = \S::get_session('basket'); $basket = \S::get_session('basket');
if (empty($basket) || !is_array($basket)) {
header('Location: /tickets/main_view/');
exit;
}
foreach ($basket as $ticketId => $variants) {
foreach ($variants as $variant) {
if (empty($variant['date'])) {
continue;
}
if (!\factory\Tickets::canBuyTicketOnDate($ticketId, $variant['date'])) {
header('Location: /tickets/basket_view/?calendar_error=1');
exit;
}
}
}
$date = date('Y-m-d H:i:s'); $date = date('Y-m-d H:i:s');
$finalPrice = 0; $finalPrice = 0;
@@ -637,4 +693,4 @@ class Tickets
{ {
return \Tpl::view('site/regulamin-for-gifts'); return \Tpl::view('site/regulamin-for-gifts');
} }
} }

View File

@@ -1,6 +1,203 @@
<?php <?php
namespace factory; namespace factory;
class Tickets { class Tickets {
private static $calendarTableReady = false;
private static $allowedDateCache = [];
static public function getCalendarDefinitions()
{
return [
'park-rozrywki' => [
'label' => 'Park rozrywki i dinozaurów + bilety prezentowe',
'ticket_ids' => [
'plac-zabaw-ulgowy',
'plac-zabaw-normalny',
'gift-plac-zabaw-ulgowy',
'gift-plac-zabaw-normalny',
],
],
'park-wodny' => [
'label' => 'Park wodny',
'ticket_ids' => [
'park-wodny-ulgowy',
'park-wodny-normalny',
],
],
'all-open' => [
'label' => 'All Open + bilety prezentowe',
'ticket_ids' => [
'bilet-all-open-ulgowy',
'bilet-all-open-normalny',
'gift-bilet-all-open-ulgowy',
'gift-bilet-all-open-normalny',
],
],
];
}
static public function ensureCalendarTable()
{
global $mdb;
if (self::$calendarTableReady) {
return;
}
$sql = "CREATE TABLE IF NOT EXISTS ticket_calendar_availability (
ticket_group VARCHAR(64) NOT NULL,
available_date DATE NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (ticket_group, available_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8";
$mdb->pdo->exec($sql);
self::$calendarTableReady = true;
}
static public function normalizeDateFromPicker($date)
{
$date = trim((string) $date);
if ($date === '') {
return null;
}
$formats = ['d-m-Y', 'Y-m-d'];
foreach ($formats as $format) {
$parsedDate = \DateTime::createFromFormat($format, $date);
if ($parsedDate && $parsedDate->format($format) === $date) {
return $parsedDate->format('Y-m-d');
}
}
return null;
}
static public function getCalendarGroupForTicket($ticketId)
{
foreach (self::getCalendarDefinitions() as $groupKey => $group) {
if (in_array($ticketId, $group['ticket_ids'], true)) {
return $groupKey;
}
}
return null;
}
static public function getEnabledDatesByGroup($groupKey)
{
global $mdb;
self::ensureCalendarTable();
$definitions = self::getCalendarDefinitions();
if (!isset($definitions[$groupKey])) {
return [];
}
$statement = $mdb->pdo->prepare(
"SELECT available_date
FROM ticket_calendar_availability
WHERE ticket_group = :ticket_group
ORDER BY available_date ASC"
);
$statement->execute([':ticket_group' => $groupKey]);
$rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
if (!is_array($rows)) {
return [];
}
return array_values(array_filter(array_map(static function ($row) {
return $row['available_date'] ?? null;
}, $rows)));
}
static public function saveEnabledDatesForGroup($groupKey, array $dates)
{
global $mdb;
self::ensureCalendarTable();
$definitions = self::getCalendarDefinitions();
if (!isset($definitions[$groupKey])) {
return false;
}
$normalizedDates = [];
foreach ($dates as $date) {
$normalizedDate = self::normalizeDateFromPicker($date);
if (!$normalizedDate) {
continue;
}
$normalizedDates[$normalizedDate] = $normalizedDate;
}
$normalizedDates = array_values($normalizedDates);
$mdb->pdo->beginTransaction();
try {
$deleteStatement = $mdb->pdo->prepare(
"DELETE FROM ticket_calendar_availability WHERE ticket_group = :ticket_group"
);
$deleteStatement->execute([':ticket_group' => $groupKey]);
if (!empty($normalizedDates)) {
$insertStatement = $mdb->pdo->prepare(
"INSERT INTO ticket_calendar_availability (ticket_group, available_date)
VALUES (:ticket_group, :available_date)"
);
foreach ($normalizedDates as $date) {
$insertStatement->execute([
':ticket_group' => $groupKey,
':available_date' => $date,
]);
}
}
$mdb->pdo->commit();
} catch (\Exception $exception) {
$mdb->pdo->rollBack();
return false;
}
self::$allowedDateCache = [];
return true;
}
static public function canBuyTicketOnDate($ticketId, $date)
{
$groupKey = self::getCalendarGroupForTicket($ticketId);
if (!$groupKey) {
return true;
}
$normalizedDate = self::normalizeDateFromPicker($date);
if (!$normalizedDate) {
return false;
}
$cacheKey = $groupKey . '|' . $normalizedDate;
if (array_key_exists($cacheKey, self::$allowedDateCache)) {
return self::$allowedDateCache[$cacheKey];
}
$enabledDates = self::getEnabledDatesByGroup($groupKey);
$isAllowed = in_array($normalizedDate, $enabledDates, true);
self::$allowedDateCache[$cacheKey] = $isAllowed;
return $isAllowed;
}
static public function getTicketAvailabilityForDate(array $ticketIds, $date)
{
$availability = [];
foreach ($ticketIds as $ticketId) {
$availability[$ticketId] = self::canBuyTicketOnDate($ticketId, $date);
}
return $availability;
}
static public function recalculate_ticket_protection( $basket ) { static public function recalculate_ticket_protection( $basket ) {
global $settings; global $settings;
@@ -110,4 +307,4 @@ class Tickets {
return false; return false;
} }
} }

BIN
layout/.DS_Store vendored

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -1453,4 +1453,729 @@ body {
75% { 75% {
transform: translateX(-2px); transform: translateX(-2px);
} }
} }
/* Tickets main view redesign */
#tickets-main-view {
.tickets_calendar {
margin-top: 48px;
margin-bottom: 30px;
}
#dateForm {
max-width: 1040px;
margin: 0 auto;
background: linear-gradient(160deg, #ffffff 0%, #f6faef 100%);
border: 1px solid rgba(114, 184, 27, 0.2);
border-radius: 20px;
padding: 26px 24px;
box-shadow: 0 12px 28px rgba(36, 58, 11, 0.08);
}
#dateForm ._title {
width: 100%;
text-align: left;
font-size: 30px;
line-height: 1.15;
font-weight: 900;
letter-spacing: 0.2px;
margin-bottom: 10px;
}
.flatpickr-calendar.inline {
width: 100%;
max-width: 420px;
margin: 0 auto;
border-radius: 16px;
box-shadow: 0 8px 22px rgba(51, 51, 51, 0.08);
border: 1px solid #e8eed6;
}
.tickets {
width: min(1120px, calc(100% - 30px));
margin: 0 auto 60px;
gap: 42px;
}
.tickets .ticket-container {
gap: 18px;
align-items: stretch;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(51, 51, 51, 0.08);
border-radius: 20px;
padding: 26px 22px 18px;
box-shadow: 0 8px 26px rgba(21, 39, 5, 0.05);
backdrop-filter: blur(2px);
}
.tickets .ticket-container .title-container {
padding-bottom: 14px;
margin-bottom: 6px;
}
.tickets .ticket-container .title-container::after {
width: 140px;
border-bottom: 2px dashed rgba(51, 51, 51, 0.35);
}
.tickets .ticket-container .title-container .title {
font-size: clamp(30px, 4vw, 44px);
}
.tickets .ticket-container .title-container .description {
font-size: clamp(14px, 2.5vw, 20px);
line-height: 1.3;
margin-bottom: 0;
color: #4f5d48;
}
.tickets .ticket {
width: 100%;
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 16px 20px;
align-items: center;
padding: 10px;
border-radius: 16px;
background: #ffffff;
border: 1px solid #edf1e5;
}
.tickets .ticket .ticket-description {
width: 100%;
min-height: 86px;
padding: 16px 14px;
border-radius: 12px;
}
.tickets .ticket .ticket-description .ticket__name {
font-size: clamp(22px, 3.2vw, 31px);
line-height: 1.05;
}
.tickets .ticket .ticket-description .description {
margin-top: 4px;
font-size: 12px;
line-height: 1.2;
}
.tickets .ticket .ticket-description .ticket-alert {
font-size: 13px;
margin-top: 10px;
text-decoration: none;
}
.tickets .ticket .price {
margin-left: 0;
min-width: 105px;
text-align: right;
font-size: clamp(31px, 3.5vw, 40px);
line-height: 1;
}
.tickets .ticket .ticket-quantity {
margin-left: 0;
}
.tickets .ticket .ticket-quantity .button {
border-radius: 999px;
padding: 11px 16px;
min-width: 170px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.35px;
box-shadow: 0 6px 14px rgba(114, 184, 27, 0.22);
}
.tickets .ticket .ticket-quantity .button:disabled {
background: #d6dccd;
color: #5d6658;
box-shadow: none;
}
.bottom-info__description {
max-width: 1120px;
margin: 0 auto;
font-size: 14px;
line-height: 1.8;
}
@include respond-below(md) {
#dateForm ._title {
text-align: center;
font-size: 24px;
}
.tickets .ticket {
grid-template-columns: 1fr auto;
grid-template-areas:
"desc desc"
"price button";
gap: 12px;
}
.tickets .ticket .ticket-description {
grid-area: desc;
}
.tickets .ticket .price {
grid-area: price;
text-align: left;
min-width: unset;
}
.tickets .ticket .ticket-quantity {
grid-area: button;
justify-self: end;
}
}
@include respond-below(sm) {
.tickets_calendar {
margin-top: 32px;
}
#dateForm {
border-radius: 16px;
padding: 18px 14px;
}
.tickets {
width: calc(100% - 20px);
gap: 22px;
}
.tickets .ticket-container {
border-radius: 14px;
padding: 16px 10px 10px;
}
.tickets .ticket {
padding: 8px;
border-radius: 12px;
}
.tickets .ticket .ticket-description {
min-height: 76px;
padding: 12px 10px;
}
.tickets .ticket .ticket-quantity .button {
min-width: 132px;
padding: 10px 12px;
font-size: 12px;
}
}
}
/* Basket view redesign */
#basket_page {
.basket_content {
width: min(1120px, calc(100% - 30px));
margin: 42px auto 70px;
}
.box_01 table {
border-radius: 16px;
overflow: hidden;
border: 1px solid #dfe8cd;
box-shadow: 0 10px 26px rgba(30, 43, 18, 0.08);
}
.box_01 table tr th {
background: #f4f9ea;
border-top: 0;
}
.box_01 table tr th h3 {
font-size: 1rem;
letter-spacing: 0.3px;
}
.box_01 table tr td {
font-size: 15px;
background: #fff;
}
.box_01 table tr td .ticket-date {
display: inline-block;
margin-top: 4px;
padding: 3px 8px;
border-radius: 999px;
background: #f2f7e8;
color: #5a6650;
font-size: 11px;
}
.box_01 table tr td .btn_t1 {
background: #f6faef;
border: 1px solid #dce8c5;
}
.box_01 .basket_protection {
margin-top: 12px;
background: #f7fbe9;
border: 1px solid #dce8c5;
border-radius: 12px;
padding: 10px 12px;
}
.box_01 .basket_protection .ticket_protection {
color: #4f6f1f;
font-weight: 700;
text-decoration: none;
}
.box_01 .basket_protection .ticket_protection:hover {
text-decoration: underline;
}
.box_01 .basket_protection__text p {
color: #5a6650;
font-size: 13px;
}
.box_02 .basket_user_data .basket_user_data__wrapper {
border-radius: 16px;
border: 1px solid #dfe8cd;
box-shadow: 0 10px 26px rgba(30, 43, 18, 0.08);
padding: 24px;
}
.box_02 .basket_user_data .basket_user_data__wrapper h2 {
font-weight: 900;
margin-bottom: 16px;
}
.box_02 .basket_user_data .basket_user_data__wrapper .form-control {
border-radius: 10px;
border-color: #d6dfc6;
min-height: 42px;
}
.box_02 .basket_user_data .basket_user_data__wrapper .form-control:focus {
border-color: #72b81b;
box-shadow: 0 0 0 0.2rem rgba(114, 184, 27, 0.2);
}
.box_02 .basket_summary .basket_summary__wrapper {
border-radius: 16px;
border: 1px solid #dfe8cd;
box-shadow: 0 10px 26px rgba(30, 43, 18, 0.08);
background: linear-gradient(165deg, #ffffff 0%, #f5faea 100%);
padding: 28px 18px;
}
.box_02 .basket_summary .basket_summary__wrapper h2 {
font-weight: 800;
}
.box_02 .basket_summary .basket_summary__wrapper p {
font-size: 2rem;
line-height: 1.1;
}
.box_02 .basket_summary .basket_summary__wrapper .buy-btn {
border-radius: 999px;
background: #72b81b;
color: #fff;
min-width: 150px;
padding: 10px 16px;
}
.box_02 .basket_summary .basket_summary__wrapper .buy-btn:hover {
background: #5e9c15;
}
@include respond-below(md) {
.basket_content {
width: calc(100% - 20px);
margin-top: 26px;
}
.box_02 .basket_user_data .basket_user_data__wrapper,
.box_02 .basket_summary .basket_summary__wrapper {
padding: 16px 12px;
border-radius: 12px;
}
}
@include respond-below(sm) {
.basket_content {
margin-top: 14px;
margin-bottom: 26px;
width: calc(100% - 14px);
}
.box_01 {
margin-bottom: 10px;
}
.box_01 table tr {
margin-bottom: 8px;
padding: 8px;
}
.box_01 table td {
min-height: 34px;
padding-top: 6px !important;
padding-bottom: 6px !important;
font-size: 13px;
}
.box_01 table tr td .btn_t1 {
width: 26px;
height: 26px;
}
.box_01 .basket_protection {
margin-top: 8px;
padding: 8px 10px;
border-radius: 10px;
}
.box_01 .basket_protection .ticket_protection {
font-size: 13px;
}
.box_01 .basket_protection__text p {
font-size: 12px;
line-height: 1.35;
}
.box_02 .basket_user_data .basket_user_data__wrapper {
padding: 12px 10px;
border-radius: 10px;
}
.box_02 .basket_user_data .basket_user_data__wrapper h2 {
font-size: 1.15rem;
margin-bottom: 10px;
}
.box_02 .basket_user_data .basket_user_data__wrapper .form-group {
margin-bottom: 0.35rem !important;
}
.box_02 .basket_user_data .basket_user_data__wrapper .form-control {
min-height: 36px;
font-size: 14px;
padding-top: 6px;
padding-bottom: 6px;
}
.box_02 .basket_user_data .basket_user_data__wrapper .col-form-label,
.box_02 .basket_user_data .basket_user_data__wrapper .form-check-label {
font-size: 14px;
line-height: 1.25;
}
.box_02 .basket_user_data .basket_user_data__wrapper textarea.form-control {
min-height: 74px;
}
.box_02 .basket_summary .basket_summary__wrapper {
margin-top: 10px;
padding: 14px 10px;
border-radius: 10px;
}
.box_02 .basket_summary .basket_summary__wrapper h2 {
font-size: 1.1rem;
margin-bottom: 6px;
}
.box_02 .basket_summary .basket_summary__wrapper p {
font-size: 1.5rem;
margin-bottom: 10px;
}
.box_02 .basket_summary .basket_summary__wrapper .buy-btn {
width: 100%;
min-width: 0;
padding: 9px 12px;
font-size: 14px;
}
}
}
/* Tickets main view final fixes */
#tickets-main-view {
--tickets-main-width: min(1120px, calc(100% - 30px));
.tickets_calendar {
width: var(--tickets-main-width);
margin-left: auto;
margin-right: auto;
}
#dateForm {
width: 100%;
max-width: none;
}
.tickets {
width: var(--tickets-main-width);
}
.shopping-cart-container--active .shopping-cart {
opacity: 1;
transform: translateX(0);
}
.shopping-cart {
opacity: 0;
transform: translateX(120%);
}
@include respond-below(sm) {
--tickets-main-width: calc(100% - 20px);
}
}
/* Tickets main view refinements */
#tickets-main-view {
#dateForm {
max-width: none;
width: min(1120px, calc(100% - 30px));
}
.tickets .ticket .price {
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
padding: 0;
min-width: 78px;
color: #2b3721;
}
.shopping-cart {
width: 320px;
right: 18px;
top: 84px;
border-radius: 16px;
padding: 12px;
gap: 10px;
color: #24311c;
background: rgba(255, 255, 255, 0.98);
border: 1px solid #dfe9cf;
box-shadow: 0 18px 34px rgba(28, 43, 14, 0.22);
transform: translateX(120%);
}
.shopping-cart .basket {
font-size: 22px;
color: #72b81b;
}
.shopping-cart .quantity {
width: 100%;
text-align: center;
font-size: 20px;
font-weight: 900;
line-height: 1.1;
}
.shopping-cart .tickets__list {
width: 100%;
padding: 0;
border: 0;
max-height: 44svh;
background: transparent;
}
.shopping-cart .tickets__list .ticket {
margin: 0 0 8px;
border: 1px solid #dde6ce;
border-radius: 10px;
padding: 8px;
font-size: 13px;
background: #f9fcf4;
grid-template-columns: 1fr 18px;
}
.shopping-cart .tickets__list .ticket__description {
font-size: 14px;
line-height: 1.2;
}
.shopping-cart .tickets__list .ticket__price {
font-size: 12px;
color: #566149;
}
.shopping-cart .tickets__list .ticket .ticket__button {
color: #b53b30;
}
.shopping-cart .sum-section {
width: 100%;
border-radius: 10px;
padding: 8px 10px;
background: #f3f8e7;
border: 1px solid #dce8c5;
font-size: 20px;
line-height: 1.1;
}
.shopping-cart .buy-btn {
width: 100%;
text-align: center;
border-radius: 10px;
padding: 11px 14px;
background: #72b81b;
color: #fff;
font-size: 16px;
font-weight: 800;
}
.shopping-cart .off-btn {
top: 8px;
right: 10px;
color: #72815f;
font-size: 16px;
}
@include respond-below(sm) {
#dateForm {
width: calc(100% - 20px);
}
.tickets .ticket .price {
min-width: 0;
}
.shopping-cart {
width: calc(100vw - 20px);
right: 10px;
top: 74px;
max-height: calc(100svh - 90px);
overflow: hidden;
}
}
}
/* Tickets cards v2 - more compact, bolder look */
#tickets-main-view {
.tickets {
gap: 28px;
}
.tickets .ticket-container {
padding: 18px 16px 12px;
gap: 12px;
border-radius: 18px;
}
.tickets .ticket {
gap: 12px 14px;
padding: 8px;
border-radius: 14px;
border: 1px solid #e7eddc;
box-shadow: 0 6px 16px rgba(25, 33, 19, 0.06);
position: relative;
overflow: hidden;
}
.tickets .ticket::after {
content: '';
position: absolute;
inset: auto 0 0 0;
height: 2px;
background: linear-gradient(90deg, rgba(114, 184, 27, 0.6), rgba(19, 143, 203, 0.2));
}
.tickets .ticket .ticket-description {
min-height: 64px;
padding: 10px 10px;
border-radius: 10px;
}
.tickets .ticket .ticket-description .ticket__name {
font-size: clamp(17px, 2.2vw, 23px);
line-height: 1.08;
letter-spacing: 0.1px;
}
.tickets .ticket .ticket-description .description {
font-size: 11px;
margin-top: 2px;
}
.tickets .ticket .ticket-description .ticket-alert {
margin-top: 6px;
font-size: 11px;
opacity: 0.95;
}
.tickets .ticket .price {
font-size: clamp(22px, 2.5vw, 30px);
font-weight: 900;
min-width: 88px;
text-align: center;
padding: 8px 10px;
border-radius: 999px;
background: #f5f9ed;
border: 1px solid #dfeac9;
color: #2f3a24;
}
.tickets .ticket .ticket-quantity .button {
min-width: 146px;
padding: 9px 12px;
font-size: 11px;
letter-spacing: 0.5px;
font-weight: 800;
}
@include respond-below(md) {
.tickets .ticket {
grid-template-columns: 1fr auto auto;
grid-template-areas: "desc price button";
align-items: center;
}
.tickets .ticket .ticket-description {
grid-area: desc;
}
.tickets .ticket .price {
grid-area: price;
text-align: center;
min-width: 82px;
padding: 7px 8px;
}
.tickets .ticket .ticket-quantity {
grid-area: button;
justify-self: end;
}
}
@include respond-below(sm) {
.tickets .ticket {
grid-template-columns: 1fr;
grid-template-areas:
"desc"
"price"
"button";
justify-items: stretch;
}
.tickets .ticket .price {
justify-self: start;
font-size: 24px;
min-width: 0;
}
.tickets .ticket .ticket-quantity {
justify-self: stretch;
}
.tickets .ticket .ticket-quantity .button {
width: 100%;
min-width: 0;
}
}
}

View File

@@ -0,0 +1,167 @@
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/pl.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<div id="tickets-calendar-admin">
<div class="container">
<div class="tickets-orders">
<h1>Kalendarz</h1>
<div class="calendar-controls">
<div class="mb-3">
<label for="ticket_group" class="form-label"><strong>Rodzaj biletu</strong></label>
<select id="ticket_group" class="form-select">
<?php foreach ($this->calendar_groups as $groupKey => $groupData) : ?>
<option value="<?= htmlspecialchars($groupKey, ENT_QUOTES, 'UTF-8'); ?>">
<?= htmlspecialchars($groupData['label'], ENT_QUOTES, 'UTF-8'); ?>
</option>
<?php endforeach; ?>
</select>
</div>
<input type="hidden" id="calendar_csrf_token" value="<?= htmlspecialchars($this->csrf_token, ENT_QUOTES, 'UTF-8'); ?>">
<div id="admin-flatpickr"></div>
<div class="calendar-buttons mt-3">
<button type="button" class="btn btn-outline-success" id="enable-month">Odblokuj cały miesiąc</button>
<button type="button" class="btn btn-outline-danger" id="disable-month">Zablokuj cały miesiąc</button>
<button type="button" class="btn btn-primary" id="save-calendar">Zapisz</button>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$(function() {
let enabledDatesSet = new Set();
const calendarInstance = flatpickr("#admin-flatpickr", {
inline: true,
mode: "multiple",
dateFormat: "Y-m-d",
disableMobile: true,
locale: "pl",
onChange: function(selectedDates, dateStr, instance) {
syncSetWithPicker(instance);
}
});
function formatDateToYmd(dateObject) {
const year = dateObject.getFullYear();
const month = String(dateObject.getMonth() + 1).padStart(2, "0");
const day = String(dateObject.getDate()).padStart(2, "0");
return year + "-" + month + "-" + day;
}
function syncSetWithPicker(instance) {
enabledDatesSet = new Set(instance.selectedDates.map(function(dateObject) {
return formatDateToYmd(dateObject);
}));
}
function renderCurrentSetToPicker() {
calendarInstance.setDate(Array.from(enabledDatesSet), false, "Y-m-d");
}
function getCurrentGroup() {
return $('#ticket_group').val();
}
function handleError(message) {
if ($.alert) {
$.alert(message);
} else {
alert(message);
}
}
function fetchDatesForGroup() {
$.ajax({
type: 'POST',
cache: false,
url: '/apanel/calendar_dates/',
data: {
ticket_group: getCurrentGroup()
},
success: function(rawResponse) {
const response = $.parseJSON(rawResponse);
if (response.status !== 'ok') {
handleError(response.message || 'Nie udało się pobrać danych kalendarza.');
return;
}
enabledDatesSet = new Set(response.enabled_dates || []);
renderCurrentSetToPicker();
},
error: function() {
handleError('Błąd połączenia podczas pobierania kalendarza.');
}
});
}
function updateWholeMonth(enableMonth) {
const year = calendarInstance.currentYear;
const monthIndex = calendarInstance.currentMonth;
const daysInMonth = new Date(year, monthIndex + 1, 0).getDate();
for (let day = 1; day <= daysInMonth; day++) {
const dateObject = new Date(year, monthIndex, day);
const dateString = formatDateToYmd(dateObject);
if (enableMonth) {
enabledDatesSet.add(dateString);
} else {
enabledDatesSet.delete(dateString);
}
}
renderCurrentSetToPicker();
}
function saveCalendar() {
$.ajax({
type: 'POST',
cache: false,
url: '/apanel/calendar_save/',
data: {
ticket_group: getCurrentGroup(),
dates: Array.from(enabledDatesSet),
csrf_token: $('#calendar_csrf_token').val()
},
success: function(rawResponse) {
const response = $.parseJSON(rawResponse);
if (response.status !== 'ok') {
handleError(response.message || 'Nie udało się zapisać kalendarza.');
return;
}
if ($.alert) {
$.alert('Kalendarz został zapisany.');
}
},
error: function() {
handleError('Błąd połączenia podczas zapisu kalendarza.');
}
});
}
$('body').on('change', '#ticket_group', function() {
fetchDatesForGroup();
});
$('body').on('click', '#enable-month', function() {
updateWholeMonth(true);
});
$('body').on('click', '#disable-month', function() {
updateWholeMonth(false);
});
$('body').on('click', '#save-calendar', function() {
saveCalendar();
});
fetchDatesForGroup();
});
</script>

View File

@@ -74,6 +74,11 @@
Cennik Cennik
</a> </a>
</li> </li>
<li>
<a href="/apanel/calendar/">
Kalendarz
</a>
</li>
<li> <li>
<a href="/apanel/settings/"> <a href="/apanel/settings/">
Ustawienia Ustawienia
@@ -139,4 +144,4 @@
</script> </script>
</body> </body>
</html> </html>

View File

@@ -10,6 +10,14 @@
} }
?> ?>
<?php if (!empty($_GET['calendar_error'])) : ?>
<div class="container mt-3">
<div class="alert alert-danger">
Co najmniej jeden bilet jest już niedostępny w wybranym terminie. Zmień datę w koszyku.
</div>
</div>
<?php endif; ?>
<div id="basket_page"> <div id="basket_page">
<div class="basket_content"> <div class="basket_content">
<div class="container"> <div class="container">
@@ -202,6 +210,15 @@ $(function() {
}, },
success: function(data) { success: function(data) {
response = jQuery.parseJSON(data); response = jQuery.parseJSON(data);
if (response.status === 'error') {
if ($.alert) {
$.alert(response.message);
} else {
alert(response.message);
}
return;
}
$('.box_01').html(response.basket_form); $('.box_01').html(response.basket_form);
$('.box_02 .basket_summary').html(response.basket_summary); $('.box_02 .basket_summary').html(response.basket_summary);
} }
@@ -296,4 +313,4 @@ $(function() {
} }
} }
}); });
</script> </script>

View File

@@ -1,4 +1,13 @@
<?php <?php
$ticketAvailability = $this->ticket_availability ?? [];
$isTicketAvailable = static function ($ticketId) use ($ticketAvailability) {
if (!array_key_exists($ticketId, $ticketAvailability)) {
return true;
}
return (bool) $ticketAvailability[$ticketId];
};
if (isset($_POST['selected_date'])) { if (isset($_POST['selected_date'])) {
$selected_date = $_POST['selected_date']; $selected_date = $_POST['selected_date'];
$selected = DateTime::createFromFormat('d-m-Y', $selected_date); $selected = DateTime::createFromFormat('d-m-Y', $selected_date);
@@ -35,6 +44,7 @@ if (isset($_POST['selected_date'])) {
<script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/pl.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/pl.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<div id="tickets-main-view">
<div class="tickets_calendar container" > <div class="tickets_calendar container" >
<form id="dateForm" method="POST"> <form id="dateForm" method="POST">
<div class="_title">Wybierz datę przyjazdu:</div> <div class="_title">Wybierz datę przyjazdu:</div>
@@ -62,6 +72,7 @@ if (isset($_POST['selected_date'])) {
</div> </div>
<?php for ($i = 0; $i < $count; $i++) : ?> <?php for ($i = 0; $i < $count; $i++) : ?>
<?php if (isset($ulgowe[$i])) : $item = $ulgowe[$i]; ?> <?php if (isset($ulgowe[$i])) : $item = $ulgowe[$i]; ?>
<?php $canBuyTicket = $isTicketAvailable($item); ?>
<div class="ticket"> <div class="ticket">
<div class="ticket-description ticket-description--<?= $this->settings['tickets'][$item]['color']; ?>"> <div class="ticket-description ticket-description--<?= $this->settings['tickets'][$item]['color']; ?>">
<h3 class="ticket__name"><?= $this->settings['tickets'][$item]['name']; ?></h3> <h3 class="ticket__name"><?= $this->settings['tickets'][$item]['name']; ?></h3>
@@ -74,11 +85,14 @@ if (isset($_POST['selected_date'])) {
<?php echo date_price($this->settings['tickets'][$item], $days_diff, $is_weekend); ?> <?php echo date_price($this->settings['tickets'][$item], $days_diff, $is_weekend); ?>
</h3> </h3>
<div class="ticket-quantity"> <div class="ticket-quantity">
<button class="add button" ticket_id="<?= $item; ?>">Dodaj do koszyka</button> <button class="add button" ticket_id="<?= $item; ?>" <?= $canBuyTicket ? '' : 'disabled'; ?>>
<?= $canBuyTicket ? 'Dodaj do koszyka' : 'Niedostępny'; ?>
</button>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
<?php if (isset($normalne[$i])) : $item = $normalne[$i]; ?> <?php if (isset($normalne[$i])) : $item = $normalne[$i]; ?>
<?php $canBuyTicket = $isTicketAvailable($item); ?>
<div class="ticket"> <div class="ticket">
<div class="ticket-description ticket-description--<?= $this->settings['tickets'][$item]['color']; ?>"> <div class="ticket-description ticket-description--<?= $this->settings['tickets'][$item]['color']; ?>">
<h3 class="ticket__name"><?= $this->settings['tickets'][$item]['name']; ?></h3> <h3 class="ticket__name"><?= $this->settings['tickets'][$item]['name']; ?></h3>
@@ -91,7 +105,9 @@ if (isset($_POST['selected_date'])) {
<?php echo date_price($this->settings['tickets'][$item], $days_diff, $is_weekend); ?> <?php echo date_price($this->settings['tickets'][$item], $days_diff, $is_weekend); ?>
</h3> </h3>
<div class="ticket-quantity"> <div class="ticket-quantity">
<button class="add button" ticket_id="<?= $item; ?>">Dodaj do koszyka</button> <button class="add button" ticket_id="<?= $item; ?>" <?= $canBuyTicket ? '' : 'disabled'; ?>>
<?= $canBuyTicket ? 'Dodaj do koszyka' : 'Niedostępny'; ?>
</button>
</div> </div>
</div> </div>
<?php endif; ?> <?php endif; ?>
@@ -107,6 +123,7 @@ if (isset($_POST['selected_date'])) {
</div> </div>
<? foreach($this -> settings['bilety-all-open'] as $key => $item) : ?> <? foreach($this -> settings['bilety-all-open'] as $key => $item) : ?>
<?php $canBuyTicket = $isTicketAvailable($item); ?>
<? $pattern = "/All Open -/"; ?> <? $pattern = "/All Open -/"; ?>
<div class="ticket"> <div class="ticket">
<div class="ticket-description ticket-description--<?= $this -> settings['tickets'][$item]['color']; ?>"> <div class="ticket-description ticket-description--<?= $this -> settings['tickets'][$item]['color']; ?>">
@@ -123,7 +140,9 @@ if (isset($_POST['selected_date'])) {
?> ?>
</h3> </h3>
<div class="ticket-quantity"> <div class="ticket-quantity">
<button class="add button" ticket_id="<?= $item; ?>">Dodaj do koszyka</button> <button class="add button" ticket_id="<?= $item; ?>" <?= $canBuyTicket ? '' : 'disabled'; ?>>
<?= $canBuyTicket ? 'Dodaj do koszyka' : 'Niedostępny'; ?>
</button>
</div> </div>
</div> </div>
<? endforeach; ?> <? endforeach; ?>
@@ -137,6 +156,7 @@ if (isset($_POST['selected_date'])) {
<p class="description">bilety dla rodzin</p> <p class="description">bilety dla rodzin</p>
</div> </div>
<? foreach($this -> settings['bilety-rodzinne'] as $key => $item) : ?> <? foreach($this -> settings['bilety-rodzinne'] as $key => $item) : ?>
<?php $canBuyTicket = $isTicketAvailable($item); ?>
<div class="ticket"> <div class="ticket">
<div class="ticket-description ticket-description--<?= $this -> settings['tickets'][$item]['color']; ?>"> <div class="ticket-description ticket-description--<?= $this -> settings['tickets'][$item]['color']; ?>">
<h3 class="ticket__name"><?= $this -> settings['tickets'][$item]['name']; ?></h3> <h3 class="ticket__name"><?= $this -> settings['tickets'][$item]['name']; ?></h3>
@@ -151,7 +171,9 @@ if (isset($_POST['selected_date'])) {
?> ?>
</h3> </h3>
<div class="ticket-quantity"> <div class="ticket-quantity">
<button class="add button" ticket_id="<?= $item; ?>">Dodaj do koszyka</button> <button class="add button" ticket_id="<?= $item; ?>" <?= $canBuyTicket ? '' : 'disabled'; ?>>
<?= $canBuyTicket ? 'Dodaj do koszyka' : 'Niedostępny'; ?>
</button>
</div> </div>
</div> </div>
<? endforeach; ?> <? endforeach; ?>
@@ -164,6 +186,7 @@ if (isset($_POST['selected_date'])) {
<p class="description">Na cały sezon</p> <p class="description">Na cały sezon</p>
</div> </div>
<? foreach($this -> settings['karnety'] as $key => $item) : ?> <? foreach($this -> settings['karnety'] as $key => $item) : ?>
<?php $canBuyTicket = $isTicketAvailable($item); ?>
<div class="ticket"> <div class="ticket">
<div class="ticket-description ticket-description--<?= $this -> settings['tickets'][$item]['color']; ?>"> <div class="ticket-description ticket-description--<?= $this -> settings['tickets'][$item]['color']; ?>">
<h3 class="ticket__name"><?= $this -> settings['tickets'][$item]['name']; ?></h3> <h3 class="ticket__name"><?= $this -> settings['tickets'][$item]['name']; ?></h3>
@@ -179,7 +202,9 @@ if (isset($_POST['selected_date'])) {
?> ?>
</h3> </h3>
<div class="ticket-quantity"> <div class="ticket-quantity">
<button class="add button" ticket_id="<?= $item; ?>">Dodaj do koszyka</button> <button class="add button" ticket_id="<?= $item; ?>" <?= $canBuyTicket ? '' : 'disabled'; ?>>
<?= $canBuyTicket ? 'Dodaj do koszyka' : 'Niedostępny'; ?>
</button>
</div> </div>
</div> </div>
<? endforeach; ?> <? endforeach; ?>
@@ -242,6 +267,7 @@ if (isset($_POST['selected_date'])) {
<img src="/layout/images/shopping-basket.svg" alt=""> <img src="/layout/images/shopping-basket.svg" alt="">
</div> </div>
</div> </div>
</div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
@@ -297,6 +323,15 @@ $(function() {
beforeSend: function() {}, beforeSend: function() {},
success: function(data) { success: function(data) {
response = jQuery.parseJSON(data); response = jQuery.parseJSON(data);
if (response.status === 'error') {
if ($.alert) {
$.alert(response.message);
} else {
alert(response.message);
}
return;
}
$('.shopping-cart-container').html(response.shopping_cart); $('.shopping-cart-container').html(response.shopping_cart);
// animacja przycisku // animacja przycisku
@@ -350,7 +385,6 @@ document.addEventListener('onChange', () => {
}) })
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var dates = [ "19-05-2025", "20-05-2025", "21-05-2025", "22-05-2025", "23-05-2025", "26-05-2025", "27-05-2025", "28-05-2025", "29-05-2025", "30-05-2025", "02-06-2025", "03-06-2025", "04-06-2025", "05-06-2025", "06-06-2025", "09-06-2025", "10-06-2025", "11-06-2025", "12-06-2025", "13-06-2025", "16-06-2025", "17-06-2025", "18-06-2025", "20-06-2025", "23-06-2025", "24-06-2025", "25-06-2025", "26-06-2025", "27-06-2025" ];
flatpickr("#flatpickr", { flatpickr("#flatpickr", {
inline: true, inline: true,
dateFormat: "d-m-Y", dateFormat: "d-m-Y",
@@ -358,11 +392,10 @@ document.addEventListener('DOMContentLoaded', function() {
minDate: "today", minDate: "today",
locale: "pl", locale: "pl",
theme: "material_blue", theme: "material_blue",
disable: dates,
onChange: function(selectedDates, dateStr, instance) { onChange: function(selectedDates, dateStr, instance) {
document.getElementById('dateForm').submit(); document.getElementById('dateForm').submit();
} }
}); });
}); });
</script> </script>

View File

@@ -29,6 +29,13 @@ $migrations = [
'ticket_prices_add_price_weekend' => "ALTER TABLE ticket_prices ADD COLUMN IF NOT EXISTS price_weekend DECIMAL(10,2) DEFAULT NULL AFTER price", 'ticket_prices_add_price_weekend' => "ALTER TABLE ticket_prices ADD COLUMN IF NOT EXISTS price_weekend DECIMAL(10,2) DEFAULT NULL AFTER price",
'ticket_calendar_availability' => "CREATE TABLE IF NOT EXISTS ticket_calendar_availability (
ticket_group VARCHAR(64) NOT NULL,
available_date DATE NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (ticket_group, available_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8",
]; ];
$ok = 0; $ok = 0;