feat: inform user about automatic reassignments or reset selections
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
import { Controller } from '@hotwired/stimulus'
|
||||||
|
import Toastify from 'toastify-js'
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
|
||||||
|
static values = { text: String, class: String }
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
// Show toast from Stimulus values (existing functionality)
|
||||||
|
if (this.hasTextValue) {
|
||||||
|
this.showToast(this.textValue, this.classValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for HTMX notification events
|
||||||
|
document.addEventListener('showNotifications', this.handleNotifications.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
document.removeEventListener('showNotifications', this.handleNotifications.bind(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNotifications(event) {
|
||||||
|
const notifications = event.detail?.notifications || []
|
||||||
|
|
||||||
|
notifications.forEach(notification => {
|
||||||
|
const className = this.getClassForType(notification.type)
|
||||||
|
this.showToast(notification.message, className)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(text, className = '') {
|
||||||
|
Toastify({
|
||||||
|
duration: 5000,
|
||||||
|
text: text,
|
||||||
|
gravity: 'top',
|
||||||
|
position: 'right',
|
||||||
|
className: className,
|
||||||
|
close: true,
|
||||||
|
}).showToast()
|
||||||
|
}
|
||||||
|
|
||||||
|
getClassForType(type) {
|
||||||
|
const typeMap = {
|
||||||
|
'success': 'toastify--success',
|
||||||
|
'warning': 'toastify--warning',
|
||||||
|
'info': 'toastify--info',
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeMap[type] || 'toastify--info'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
@import "components/button.css";
|
@import "components/button.css";
|
||||||
@import "components/menu.css";
|
@import "components/menu.css";
|
||||||
@import "components/tooltip.css";
|
@import "components/tooltip.css";
|
||||||
@import "components/table-responsive.css";
|
@import "components/table-responsive.css";
|
||||||
|
@import "components/toast.css";
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
@import "toastify-js/src/toastify.css";
|
||||||
|
|
||||||
|
.toastify {
|
||||||
|
@apply shadow-lg rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastify--success {
|
||||||
|
@apply text-gray-100;
|
||||||
|
background: theme('colors.emerald.600');
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastify--warning {
|
||||||
|
@apply text-gray-100;
|
||||||
|
background: theme('colors.red.600');
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastify--info {
|
||||||
|
@apply text-gray-100;
|
||||||
|
background: theme('colors.primary');
|
||||||
|
}
|
||||||
Generated
+8
-1
@@ -8,7 +8,8 @@
|
|||||||
"license": "WTFPL",
|
"license": "WTFPL",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iframe-resizer/child": "^5.3.2",
|
"@iframe-resizer/child": "^5.3.2",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7",
|
||||||
|
"toastify-js": "^1.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.0",
|
"@babel/core": "^7.17.0",
|
||||||
@@ -6537,6 +6538,12 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toastify-js": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-interface-checker": {
|
"node_modules/ts-interface-checker": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
|
|||||||
+2
-1
@@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iframe-resizer/child": "^5.3.2",
|
"@iframe-resizer/child": "^5.3.2",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7",
|
||||||
|
"toastify-js": "^1.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ class CreateStep2Controller extends AbstractController
|
|||||||
|
|
||||||
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
|
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
|
||||||
|
|
||||||
|
// Collect notifications from all participants
|
||||||
|
$notifications = $this->collectParticipantNotifications($bookingCreateDto);
|
||||||
|
|
||||||
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
|
$summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
|
||||||
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto);
|
$roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto);
|
||||||
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
|
$availableRooms = $bookingCreateDto->travel->getAvailableRooms();
|
||||||
@@ -139,7 +142,7 @@ class CreateStep2Controller extends AbstractController
|
|||||||
|
|
||||||
// The DTO is now updated with the latest selection and submitted data has been cleaned.
|
// The DTO is now updated with the latest selection and submitted data has been cleaned.
|
||||||
// We can now render the blocks with the fresh data.
|
// We can now render the blocks with the fresh data.
|
||||||
return $this->htmxOobResponse(
|
$response = $this->htmxOobResponse(
|
||||||
'booking/create_step_2.html.twig',
|
'booking/create_step_2.html.twig',
|
||||||
['participants_form', 'booking_summary'],
|
['participants_form', 'booking_summary'],
|
||||||
[
|
[
|
||||||
@@ -152,6 +155,15 @@ class CreateStep2Controller extends AbstractController
|
|||||||
'groupedSelectedRooms' => $groupedSelectedRooms,
|
'groupedSelectedRooms' => $groupedSelectedRooms,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add notifications to HTMX trigger header if any exist
|
||||||
|
if ([] !== $notifications) {
|
||||||
|
$response->headers->set('HX-Trigger', json_encode([
|
||||||
|
'showNotifications' => ['notifications' => $notifications],
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,4 +245,28 @@ class CreateStep2Controller extends AbstractController
|
|||||||
$this->roomAssignmentService->assignParticipantsToRooms($bookingCreateDto);
|
$this->roomAssignmentService->assignParticipantsToRooms($bookingCreateDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects all notifications from participants and clears them.
|
||||||
|
*
|
||||||
|
* @param BookingCreateDto $bookingCreateDto The booking DTO containing participants
|
||||||
|
*
|
||||||
|
* @return array<array{type: string, message: string}> Array of notification messages
|
||||||
|
*/
|
||||||
|
private function collectParticipantNotifications(BookingCreateDto $bookingCreateDto): array
|
||||||
|
{
|
||||||
|
$notifications = [];
|
||||||
|
|
||||||
|
foreach ($bookingCreateDto->participants as $participant) {
|
||||||
|
if ([] !== $participant->notifications) {
|
||||||
|
foreach ($participant->notifications as $notification) {
|
||||||
|
$notifications[] = $notification;
|
||||||
|
}
|
||||||
|
// Clear notifications after collection
|
||||||
|
$participant->notifications = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $notifications;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,11 @@ class ParticipantDto
|
|||||||
// Bulk insurance booking flag (applicant only: when checked, assigns same insurance type to all participants)
|
// Bulk insurance booking flag (applicant only: when checked, assigns same insurance type to all participants)
|
||||||
public bool $bulkInsuranceBooking = false;
|
public bool $bulkInsuranceBooking = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array{type: string, message: string}> Notification messages for user feedback
|
||||||
|
*/
|
||||||
|
public array $notifications = [];
|
||||||
|
|
||||||
public static function fromPersonalData(PersonalData $personalData): static
|
public static function fromPersonalData(PersonalData $personalData): static
|
||||||
{
|
{
|
||||||
$instance = new static();
|
$instance = new static();
|
||||||
@@ -164,4 +169,18 @@ class ParticipantDto
|
|||||||
{
|
{
|
||||||
return $this->insurance?->price ?? 0.0;
|
return $this->insurance?->price ?? 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a notification message for user feedback.
|
||||||
|
*
|
||||||
|
* @param string $type The notification type (info, warning, success)
|
||||||
|
* @param string $message The notification message
|
||||||
|
*/
|
||||||
|
public function addNotification(string $type, string $message): void
|
||||||
|
{
|
||||||
|
$this->notifications[] = [
|
||||||
|
'type' => $type,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,13 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
$bookingDto
|
$bookingDto
|
||||||
);
|
);
|
||||||
$participant->insurance = $reassignedInsurance;
|
$participant->insurance = $reassignedInsurance;
|
||||||
|
|
||||||
|
if (null !== $reassignedInsurance) {
|
||||||
|
$participant->addNotification(
|
||||||
|
'info',
|
||||||
|
sprintf('Versicherung automatisch angepasst: %s', $reassignedInsurance->label)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Insurance not found - clear selection
|
// Insurance not found - clear selection
|
||||||
@@ -174,6 +181,14 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
$participant,
|
$participant,
|
||||||
$bookingDto
|
$bookingDto
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (null !== $reassignedInsurance && $reassignedInsurance->id !== $currentInsurance->id) {
|
||||||
|
$participant->addNotification(
|
||||||
|
'info',
|
||||||
|
sprintf('Versicherung automatisch angepasst an neuen Preis: %s', $reassignedInsurance->label)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$participant->insurance = $reassignedInsurance; // null if no suitable match found
|
$participant->insurance = $reassignedInsurance; // null if no suitable match found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,9 +64,18 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store previous rental state BEFORE checking skipass
|
||||||
|
$previousRentals = $participant->rentals;
|
||||||
|
|
||||||
// Check if rentals should be available based on skipass selection
|
// Check if rentals should be available based on skipass selection
|
||||||
if (null === $participant->skiPass) {
|
if (null === $participant->skiPass) {
|
||||||
// No skipass selected = clear all rental selections
|
// No skipass selected = clear all rental selections
|
||||||
|
if ([] !== $previousRentals) {
|
||||||
|
$participant->addNotification(
|
||||||
|
'warning',
|
||||||
|
'Ausrüstung wurde entfernt (kein Skipass ausgewählt)'
|
||||||
|
);
|
||||||
|
}
|
||||||
$participant->rentals = [];
|
$participant->rentals = [];
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -85,6 +94,14 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
$participantIndex
|
$participantIndex
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Notify if rentals were cleared due to skipass duration change
|
||||||
|
if ([] !== $previousRentals && [] === $validSelections) {
|
||||||
|
$participant->addNotification(
|
||||||
|
'warning',
|
||||||
|
'Ausrüstung wurde entfernt (andere Skipass-Dauer ausgewählt)'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$participant->rentals = $validSelections;
|
$participant->rentals = $validSelections;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +185,7 @@ class ParticipantRentalsFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
* - rental.dateFrom === skipass.dateFrom
|
* - rental.dateFrom === skipass.dateFrom
|
||||||
* - rental.dateTo === skipass.dateTo
|
* - rental.dateTo === skipass.dateTo
|
||||||
*
|
*
|
||||||
* @param array $rentals All available rental services
|
* @param array $rentals All available rental services
|
||||||
* @param \App\Form\Model\ParticipantDto $participant The participant with skipass selection
|
* @param \App\Form\Model\ParticipantDto $participant The participant with skipass selection
|
||||||
*
|
*
|
||||||
* @return array Filtered rentals matching the skipass duration
|
* @return array Filtered rentals matching the skipass duration
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include '_partials/_flashes.html.twig' %}
|
{% include '_partials/_flashes.html.twig' %}
|
||||||
|
<div {{ stimulus_controller('toast') }}></div>
|
||||||
<h1>Neue Buchung</h1>
|
<h1>Neue Buchung</h1>
|
||||||
<div class="grid grid-cols-3 gap-8">
|
<div class="grid grid-cols-3 gap-8">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user