320 Commits
Author SHA1 Message Date
Björn Fromme 97f8baba06 fix: adopt general styles to error page template 2026-01-06 08:34:24 +01:00
Björn Fromme 3ffa80e2bc chore: update deployer config 2026-01-06 08:30:56 +01:00
Björn Fromme ab1a21dffc chore: update project dependencies 2026-01-06 08:26:22 +01:00
Björn Fromme bccd8c6fc3 chore: merge master into feature branch 2026-01-05 18:25:14 +01:00
Björn Fromme 2ea298854d fix: restore .user.ini from master 2026-01-05 18:17:38 +01:00
Björn Fromme b7e4f29813 chore: update upgrade plan with current state of the project 2026-01-05 17:34:02 +01:00
Björn Fromme 7780646601 chore: replace claude code with opencode inside ddev container 2026-01-05 17:24:41 +01:00
Björn Fromme 0f58f07757 chore: update project dependencies 2026-01-05 17:20:44 +01:00
Björn Fromme ef263e064a feat: add env variable for website base url 2026-01-05 17:20:18 +01:00
Björn Fromme 7127b4d050 chore: install claude code in ddev container for security reasons 2025-12-31 14:02:53 +01:00
Björn Fromme 42744fa30f chore: complete scheduler config for email notifications 2025-12-22 11:07:56 +01:00
Björn Fromme be3f6e6288 feat: remove outdated log entries scheduled 2025-12-22 11:07:33 +01:00
Björn Fromme 9291adfdff fix: fail gracefully when remote XML data cannot be read during sync 2025-12-22 08:29:57 +01:00
Björn Fromme c9426eff3b fix: catch travel data not found errors to show proper flash message 2025-12-19 17:54:25 +01:00
Björn Fromme cd350c212e chore: update project dependencies 2025-12-19 17:29:33 +01:00
Björn Fromme 1f1ac1a0cc docs: add inline documentation where missing 2025-12-19 17:26:59 +01:00
Björn Fromme 2dd5b7ad64 chore: cleanup 2025-12-19 17:26:39 +01:00
Björn Fromme 62ac8038dc feat: cron controlled cli command to sync xml data with remote 2025-12-19 17:06:27 +01:00
Björn Fromme 5f94f0487b chore: cleanup flysystem config 2025-12-19 16:07:12 +01:00
Björn Fromme 64880355e8 chore: update json test-data for pickups planning 2025-12-17 17:16:41 +01:00
Björn Fromme 1dc8489239 fix: avoid duplicated pickup planning entries 2025-12-17 17:05:38 +01:00
Björn Fromme 5831294d33 chore: reduce noise in logs 2025-12-17 16:44:11 +01:00
Björn Fromme 073bbdc0f8 feat: add logging to api endpoint 2025-12-17 16:34:57 +01:00
Björn Fromme ef75383885 fix: don't show bulk insurance labels in summary 2025-12-17 16:04:19 +01:00
Björn Fromme bfb25a4051 feat: accept multiple travel codes per pickup planning 2025-12-17 14:23:05 +01:00
Björn Fromme f55febd545 chore: update project dependencies 2025-12-17 10:23:06 +01:00
Björn Fromme d27b78cec1 chore: rotate auth log 2025-12-17 10:22:37 +01:00
Björn Fromme 1bb145e3be feat: forcibly invalidate availabilities cache on booking start 2025-12-16 13:46:54 +01:00
Björn Fromme 59b9a42009 feat: loading indicator on initial page load 2025-12-16 13:44:25 +01:00
Björn Fromme a576fa2d23 fix: adopt changes in test 2025-12-16 13:31:06 +01:00
Björn Fromme 14fe9bd44f feat: load availability data once per booking session 2025-12-16 13:30:45 +01:00
Björn Fromme cce59b8ed6 chore: disable saving cookies for api requests 2025-12-16 12:23:40 +01:00
Björn Fromme 8904e1c7fe feat: log pickups planning data api calls 2025-12-16 12:17:41 +01:00
Björn Fromme a0f763af63 chore: update project dependencies 2025-12-16 09:59:15 +01:00
Björn Fromme 667c9d4677 chore: reduce logger noise 2025-12-14 17:17:19 +01:00
Björn Fromme 1622f2be6a chore: retain xml dumps for three days to save on webspace 2025-12-14 17:10:09 +01:00
Björn Fromme 28914d6586 feat: rotate auth logfile 2025-12-12 20:15:56 +01:00
Björn Fromme cfc3957fec chore: update project dependencies 2025-12-12 20:06:45 +01:00
Björn Fromme aee2eb848d fix: adopt changed response class 2025-12-12 20:06:19 +01:00
Björn Fromme 42f17db542 feat: extended oauth2 user info and refactored authenticator 2025-12-12 19:57:34 +01:00
Björn Fromme 6315fbd0f7 fix: ensure template variable exists 2025-12-12 19:46:48 +01:00
Björn Fromme 79e5a3c12d feat: adjust oauth2 implementation for TYPO3 backend login 2025-12-12 16:31:32 +01:00
Björn Fromme e9c1dd8b9d feat: basic themes for event sites 2025-12-12 13:13:36 +01:00
Björn Fromme b1ff6989ec feat: api endpoints for pickups planning 2025-12-12 09:44:38 +01:00
Björn Fromme 1d97a19982 feat: always show all available ski passes but readonly if age doesn't match 2025-12-12 08:47:25 +01:00
Björn Fromme c3519124b7 feat: update labels of voucher fields
addresses #869bf8tzv
2025-12-12 08:09:34 +01:00
Björn Fromme 05c769bab2 feat: use query parameters for booking urls 2025-12-11 16:14:31 +01:00
Björn Fromme c1539a47f9 fix: properly assign readonly state to form fields for unavailable services 2025-12-11 12:07:19 +01:00
Björn Fromme f54f66cd32 fix: reset room assignments after updated selection if required 2025-12-11 11:27:48 +01:00
Björn Fromme c8a33cdf43 fix: assign correct return types 2025-12-11 11:26:46 +01:00
Björn Fromme b4c03160ca feat: consistent display of included services and prices 2025-12-11 11:17:03 +01:00
Björn Fromme 72cd36a573 feat: enable logging for contact form api endpoint 2025-12-11 09:33:41 +01:00
Björn Fromme 4d3a4b7c04 chore: update project dependencies 2025-12-10 18:01:34 +01:00
Björn Fromme b9dfeb2d3e feat: backport contact form api endpoint 2025-12-10 18:00:51 +01:00
Björn Fromme e42ef2d3c9 feat: show unavailable transportation services as readonly 2025-12-10 17:15:05 +01:00
Björn Fromme bb69a28ec2 fix: don't show incorrect notification about updated rentals 2025-12-10 15:40:17 +01:00
Björn Fromme e8c2261b7e feat: show inquiry status with rooms in step 1 2025-12-10 15:33:13 +01:00
Björn Fromme 7665385cbf feat: hide parking from participants under 18 2025-12-10 14:37:06 +01:00
Björn Fromme 346dee23e4 fix: don't auto assign discounted transport to babies 2025-12-10 14:09:52 +01:00
Björn Fromme 109f36e7f5 chore: simplify parameter handling 2025-12-10 13:57:48 +01:00
Björn Fromme 0e9c5e6e1a feat: configurable choices for body dimensions
closes #8697phk21
2025-12-10 13:48:47 +01:00
Björn Fromme 145a08e511 chore: update deployer config for prod 2025-12-10 13:10:17 +01:00
Björn Fromme 2b8046b416 feat: re-implement contact form submissions api endpoint for website 2025-12-10 09:29:19 +01:00
Björn Fromme e27b5da3c8 feat: adjust participant number size depending on value 2025-12-09 20:52:12 +01:00
Björn Fromme 613e0b9a5c feat: update deployer config to build assets upfront 2025-12-09 20:48:36 +01:00
Björn Fromme 0564e3c424 feat: align rendering of pickups field with other services 2025-12-09 20:43:58 +01:00
Björn Fromme 32c73ab7b1 fix: avoid loading indicator issues when using browser history for navigation 2025-12-09 15:13:04 +01:00
Björn Fromme 420efb25ad chore: update project dependencies 2025-12-09 12:56:31 +01:00
Björn Fromme 150998e120 fix: ensure flash message is displayed after redirect 2025-12-09 12:55:23 +01:00
Björn Fromme 31f414b4d8 feat: show room prices in room assignment form field 2025-12-09 12:55:00 +01:00
Björn Fromme 9c6860ecc6 fix: account for htmx issues in mobile safari with select fields 2025-12-09 11:42:31 +01:00
Björn Fromme 6b32b6baa4 fix: disable autocomplete in chrome for good 2025-12-09 11:33:19 +01:00
Björn Fromme 3527be9c29 feat: various improvements in styles and layout 2025-12-09 11:20:43 +01:00
Björn Fromme 0239aee89a fix: avoid stale caches 2025-12-09 10:25:11 +01:00
Björn Fromme f21f4a0385 feat: streamlined rendering of fields as static text in forms 2025-12-09 10:20:55 +01:00
Björn Fromme fa2b5e0d7f feat: render room assignment as radio buttons 2025-12-09 10:05:14 +01:00
Björn Fromme 1ac7720765 feat: replace overly complex and unnecessary form choice loader for rooms 2025-12-09 09:32:01 +01:00
Björn Fromme 4182c30b72 fix: ensure footer bar is visible on mobile safari 2025-12-09 09:21:58 +01:00
Björn Fromme e4aad8a456 feat: load cms data early to display image on login page 2025-12-08 17:39:29 +01:00
Björn Fromme d78638e6e5 feat: improved layout for participant form 2025-12-08 17:08:26 +01:00
Björn Fromme 1726523eaf fix: disable autocompletion on personal data fields 2025-12-08 16:20:20 +01:00
Björn Fromme 1aa564960d feat: body dimensions optional 2025-12-08 16:18:31 +01:00
Björn Fromme cd2765bab5 feat: improved error handling and redirect on invalid session 2025-12-08 16:00:02 +01:00
Björn Fromme 390ee81675 feat: indicate incomplete participant data in cards 2025-12-08 15:59:35 +01:00
Björn Fromme 6dbbfd87f8 feat: special treatment of services for participants of age 0-2 2025-12-08 15:26:30 +01:00
Björn Fromme 118f86b2a0 feat: increase availability cache ttl and ensure fresh data when entering form 2025-12-08 14:57:12 +01:00
Björn Fromme 27d77624da docs: upgrade plan to symfony 7.4 2025-12-07 18:48:40 +01:00
Björn Fromme 20a67e9b96 feat: optional return url 2025-12-07 18:47:18 +01:00
Björn Fromme 233748a6c5 chore: cleanup 2025-12-07 15:03:54 +01:00
Björn Fromme 67491a7f68 fix: remove obsolete htmx attributes from forms 2025-12-07 15:03:44 +01:00
Björn Fromme c7129d5791 chore: cleanup 2025-12-07 14:47:51 +01:00
Björn Fromme 4abee677f6 fix: enforce https 2025-12-07 14:24:08 +01:00
Björn Fromme 21600dc2d6 feat: implement hx-boost for reliable loading indication 2025-12-07 14:11:50 +01:00
Björn Fromme f0b84c9943 fix: ensure loading indicator is triggered 2025-12-06 18:13:38 +01:00
Björn Fromme 59418adc02 chore: update project dependencies 2025-12-06 15:05:06 +01:00
Björn Fromme de52907eec fix: remove form options cache causing issues 2025-12-06 15:02:34 +01:00
Björn Fromme bfb1a04ea0 feat: implement screendesign 2025-12-06 15:02:33 +01:00
Björn Fromme b206c3990b chore: update project dependencies 2025-12-06 15:02:13 +01:00
Björn Fromme 54a37c5bf7 feat: adjust loading indicator styles 2025-12-06 15:02:13 +01:00
Björn Fromme fa98ceac29 fix: add loading indicator to all 'back' buttons 2025-12-06 15:02:13 +01:00
Björn Fromme 1bab230f10 fix: consider mode in summary 2025-12-06 15:02:13 +01:00
Björn Fromme 5b55d0984a fix: correctly handle email uniqueness by excluding the applicant from validation 2025-12-06 15:02:13 +01:00
Björn Fromme 2b3a8f1c69 feat: request replay command for bpn api 2025-12-06 15:02:13 +01:00
Björn Fromme 696800aafd feat: refactoring and cleanup 2025-12-06 15:02:13 +01:00
Björn Fromme e7997f84eb fix: include user ini for adjusted php settings on prod host 2025-12-05 10:03:51 +01:00
Björn Fromme 8d3a7b99b4 fix: prevent type errors 2025-12-01 10:41:04 +01:00
Björn Fromme 61a8afdde7 fix: prevent null reference errors 2025-12-01 10:38:55 +01:00
Björn Fromme 1f3e79d57c fix: prevent null reference errors 2025-12-01 10:37:39 +01:00
Björn Fromme f4691ec67e feat: finalize voucher handling and discount display in summary and overview 2025-11-26 17:29:15 +01:00
Björn Fromme 32bef9a4e9 feat: special treatment of participants with age 0-2 (babies) 2025-11-26 15:38:49 +01:00
Björn Fromme 30704f936a fix: don't disable room selection field with single option 2025-11-25 18:14:33 +01:00
Björn Fromme 9167c84b4c fix: clear target path when canceling booking flow 2025-11-25 17:34:10 +01:00
Björn Fromme f5f5d43ab5 feat: conditionally render room assignment field in participant edit form 2025-11-25 17:30:23 +01:00
Björn Fromme 9b2ad3845d feat: prevent selection of baby rooms without regular rooms 2025-11-25 16:43:41 +01:00
Björn Fromme e2513d6245 chore: update claude settings 2025-11-25 16:43:05 +01:00
Björn Fromme 126fce9687 feat: step input for room type selection 2025-11-25 16:42:39 +01:00
Björn Fromme c1364a458b fix: correctly determine number of participants 2025-11-25 16:42:13 +01:00
Björn Fromme ecc4741f56 feat: correct determination of inquiry status and conditional voucher field display 2025-11-25 16:41:25 +01:00
Björn Fromme f33e55f201 feat: correct handling of vouchers 2025-11-24 10:20:21 +01:00
Björn Fromme ce1b9ece09 feat: layout improvements 2025-11-21 12:44:06 +01:00
Björn Fromme 9d28d2f102 feat: improved promotional and purchase vouchers 2025-11-21 12:43:51 +01:00
Björn Fromme aaa7e9f4c3 feat: promotional and purchase vouchers 2025-11-20 18:41:46 +01:00
Björn Fromme 26681e5c62 feat: fetch additional content from cms via api 2025-11-20 15:42:41 +01:00
Björn Fromme 33c3dc6022 feat: sort pickups by their times 2025-11-17 10:00:03 +01:00
Björn Fromme a6b0051f1b chore: update project dependencies 2025-11-17 09:51:13 +01:00
Björn Fromme d0d1edb3ae feat: sort pickups by their times 2025-11-17 09:50:00 +01:00
Björn Fromme 6e96f6fb44 fix: never validate insurance selection in edit mode 2025-11-13 12:44:19 +01:00
Björn Fromme c6b28e7f4e fix: don't validate insurance selection of participants when in bulk assignment 2025-11-13 12:29:27 +01:00
Björn Fromme 3ce4d81a4c chore: update project dependencies 2025-11-13 11:36:52 +01:00
Björn Fromme d5c572c25b feat: loading indicators for regular get requests 2025-11-13 11:34:45 +01:00
Björn Fromme bcded403be feat: improved timeout handling for api client 2025-11-13 11:15:31 +01:00
Björn Fromme aeb25f3b7e feat: remove 'remember me' functionality from login form in booking process 2025-11-13 10:54:40 +01:00
Björn Fromme 421851cc4c feat: surcharges and discounts per participant and in summary 2025-11-13 09:30:49 +01:00
Björn Fromme 66eb392151 fix: check field existence before rendering 2025-11-11 15:45:14 +01:00
Björn Fromme beaccd93e0 fix: show rental insurance field only when applicable 2025-11-11 15:39:56 +01:00
Björn Fromme 111cda95f3 chore: update project dependencies 2025-11-11 15:13:15 +01:00
Björn Fromme 84ed41f325 fix: tooltips for form fields not showing 2025-11-11 15:12:28 +01:00
Björn Fromme e7cf172b9f feat: add subtypes PAR and LVS as bookable additional services 2025-11-11 14:14:44 +01:00
Björn Fromme e52cb98163 chore: update project dependencies 2025-11-07 10:39:39 +01:00
Björn Fromme d85e81e8b9 fix: ensure valid applicant data 2025-11-07 10:39:16 +01:00
Björn Fromme 1e5fe23159 fix: don't rely on individual prices being available for all participants 2025-11-06 15:32:39 +01:00
Björn Fromme 2f78ddb8ad fix: don't rely on individual prices being available for all participants 2025-11-06 15:24:51 +01:00
Björn Fromme f19762d686 chore: update project dependencies 2025-11-06 14:55:23 +01:00
Björn Fromme eba2f8e503 fix: skip availability check for services that are not included in API response 2025-11-06 14:55:00 +01:00
Björn Fromme 0a0bef9e82 feat: ensure inquiry info is always visible 2025-10-31 16:40:14 +01:00
Björn Fromme 29f415a91f feat: improved dynamic validation 2025-10-31 16:23:06 +01:00
Björn Fromme a619add4e3 fix: ensure validation of participant data 2025-10-31 15:07:22 +01:00
Björn Fromme 34a8d42fc3 fix: keep browser history intact when navigating between cards view and participant form 2025-10-31 12:46:37 +01:00
Björn Fromme cfd346d5df wip: integrate with typo3 to fetch product and hotel data 2025-10-28 16:13:07 +01:00
Björn Fromme 3d694ed7eb fix: streamlined single room selection handling 2025-10-28 16:12:27 +01:00
Björn Fromme 5021f414d1 feat: improved mutability checks for personal data in edit or create mode 2025-10-28 15:34:09 +01:00
Björn Fromme 51dbf20118 feat: improved handling of fields to be rendered as static text 2025-10-27 13:38:40 +01:00
Björn Fromme a50bd205cd chore: update project dependencies 2025-10-27 08:33:59 +01:00
Björn Fromme da7b4dfb48 fix: temporarily disable all participant validation 2025-10-27 08:31:39 +01:00
Björn Fromme eee7be0c1e feat: gravatar integration 2025-10-25 14:09:19 +02:00
Björn Fromme d55a201681 feat: integrate with scheduler bundle 2025-10-24 17:57:13 +02:00
Björn Fromme ba0eab9e96 fix: adapt changes in create mode for edit mode, cleanup 2025-10-24 17:30:41 +02:00
Björn Fromme b63804df88 feat: authenticated booking 2025-10-24 17:07:08 +02:00
Björn Fromme 647a25a78f feat: performance improvements 2025-10-24 15:21:15 +02:00
Björn Fromme 6b0dfb3426 chore: fix deprecations in preparation uf upgrading to symfony 7.3 2025-10-24 13:59:54 +02:00
Björn Fromme c8bb051ae6 fix: incorrect and unnecessary template inheritance 2025-10-24 12:32:16 +02:00
Björn Fromme b326510b04 chore: consolidate documentation 2025-10-24 12:17:42 +02:00
Björn Fromme 293574187f feat: price info tooltip for bulk insurance booking
addresses #869axbjzz
2025-10-24 11:23:19 +02:00
Björn Fromme b74f84e8ad feat: rearrange field order
addresses #869axbhz3
2025-10-24 09:32:16 +02:00
Björn Fromme 19a272db96 feat: group rentals and rental insurance fields visually
addresses #869ay16cp
2025-10-24 09:28:41 +02:00
Björn Fromme 67843c2f2e feat: dedicated section for discounts in summary
addresses #869ay1724
2025-10-24 09:19:34 +02:00
Björn Fromme a01e966d6d feat: automatic unselection of discounted transportation 2025-10-24 09:04:20 +02:00
Björn Fromme 9140b97a71 fix: improved position of info icon 2025-10-23 11:54:44 +02:00
Björn Fromme 405f03ee7a fix: adopt wrapper dto to dynamic form field 2025-10-23 11:38:34 +02:00
Björn Fromme b88ad24c3d feat: display service descriptions as tooltip with info icon
addresses #869axbnj0
2025-10-23 11:07:47 +02:00
Björn Fromme bb630da5ed feat: remove room selection field entirely for single room type
addresses #869axcukn
2025-10-23 10:49:14 +02:00
Björn Fromme 39d944351c chore: cleanup, update documentation 2025-10-23 10:29:37 +02:00
Björn Fromme 2b272ef789 fix: validation error indication on cards 2025-10-23 10:19:18 +02:00
Björn Fromme 3e54a58b9b wip: email uniqueness validation 2025-10-23 09:29:27 +02:00
Björn Fromme 60176d19f4 wip: improved validation and feedback 2025-10-23 08:50:45 +02:00
Björn Fromme 9c716477aa feat: unique email addresses of participants unless child, cleanup
addresses #869axbn21
2025-10-22 17:42:17 +02:00
Björn Fromme be2fffad23 feat: additional conformation checkbox for terms and conditions
addresses #869axbm44
2025-10-22 16:19:05 +02:00
Björn Fromme 34230e5520 feat: improved layout for pickup selection to make price visible
addresses #869axbjmd
2025-10-22 16:01:17 +02:00
Björn Fromme 1949bf9b97 feat: prefer discounted transportation services
addresses #869axbjbz
2025-10-22 15:53:54 +02:00
Björn Fromme 7ab60b1048 feat: improved form field layout for date of birth 2025-10-22 10:37:20 +02:00
Björn Fromme 0e6c31b8ce feat: show placeholders for individual date parts in date of birth field 2025-10-22 10:10:02 +02:00
Björn Fromme a2cfb693e0 feat: replace date field for participant's birthday 2025-10-22 10:02:30 +02:00
Björn Fromme 10df3ebb35 wip: implement e2e tests with cypress 2025-10-21 20:25:18 +02:00
Björn Fromme ab91c138d5 feat: unassign rooms from other participants when unavailable 2025-10-21 17:02:26 +02:00
Björn Fromme cc1d1e5746 feat: automatically assign rooms only for unique room type selection 2025-10-20 15:29:33 +02:00
Björn Fromme 8c75d0070d chore: update project dependencies 2025-10-20 10:23:34 +02:00
Björn Fromme 4df04d0504 chore: cleanup, updated docs 2025-10-20 09:32:54 +02:00
Björn Fromme 38bc594ca4 feat: consolidated insurance service 2025-10-20 09:22:03 +02:00
Björn Fromme fb9c9d91ee feat: integrate with spatie/ray for debugging and performance optimization 2025-10-20 08:08:17 +02:00
Björn Fromme ee69346eb4 feat: extended pricing breakdown on confirmation page 2025-10-18 17:59:42 +02:00
Björn Fromme e6c47019d5 fix: fix floating point precision error 2025-10-18 17:44:24 +02:00
Björn Fromme b894bbf5e6 chore: update documentation 2025-10-18 17:17:49 +02:00
Björn Fromme 0f8fed605c chore: adjust and cleanup tests 2025-10-18 17:15:31 +02:00
Björn Fromme 479b177e90 wip: bulk insurance refactoring 2025-10-18 16:39:57 +02:00
Björn Fromme 08210b8e52 wip: bulk insurance booking fix 2025-10-18 13:07:27 +02:00
Björn Fromme b5dfe17b73 wip: finalize implementation 2025-10-18 12:11:24 +02:00
Björn Fromme 49b5ccf308 wip: finalize implementation 2025-10-18 11:46:12 +02:00
Björn Fromme 434c4d0a11 wip: finalize implementation 2025-10-17 18:46:25 +02:00
Björn Fromme dc7ed0728a feat: readonly room selection for single room types 2025-10-16 09:36:04 +02:00
Björn Fromme b1dbb15ad7 feat: display booked insurance in edit mode 2025-10-16 09:35:30 +02:00
Björn Fromme e76170b6dd chore: remove debug statements 2025-10-16 09:35:12 +02:00
Björn Fromme 8225d4e92d fix: show feedback modal 2025-10-16 09:12:43 +02:00
Björn Fromme a5addf8d33 fix: adopt changed template name 2025-10-15 21:29:12 +02:00
Björn Fromme 0542a669bd fix: adjust deployer config 2025-10-15 18:32:49 +02:00
Björn Fromme 49b7a3fe34 feat: refactor to cards 2025-10-15 18:31:10 +02:00
Björn Fromme ead2c092da fix: ensure skipass is selected 2025-10-15 11:40:51 +02:00
Björn Fromme 6e818c3480 wip: refactor to cards with individual participant forms 2025-10-14 21:27:32 +02:00
Björn Fromme a02490ea36 wip: add documentation for plan 2025-10-14 20:30:51 +02:00
Björn Fromme 9eda12981a feat: htmx powered requests, improved loading indicator
feat: loading indicator and htmx form submit
2025-10-14 18:07:59 +02:00
Björn Fromme 4939a1a9ad feat: inquiry bookings 2025-10-09 18:52:23 +02:00
Björn Fromme 989f599b54 chore: remove outdated documentation 2025-10-09 18:51:58 +02:00
Björn Fromme 67c642bc2f wip: modernized edit flow, fix insurance tier calculation 2025-10-09 17:42:39 +02:00
Björn Fromme cba0f747cd wip: modernized edit flow 2025-10-08 21:09:53 +02:00
Björn Fromme c3008d7f7a wip: insurance booking in edit flow 2025-10-07 18:09:16 +02:00
Björn Fromme 7304fb50c6 fix: set correct default agency 2025-10-07 17:27:26 +02:00
Björn Fromme ec1bba7c46 wip: backport booking create flow form handling to edit flow 2025-10-07 16:38:55 +02:00
Björn Fromme b66d453881 feat: unify pickup selection 2025-10-06 18:07:07 +02:00
Björn Fromme b7b80296e9 feat: comprehensive booking overview for confirmation 2025-10-06 16:43:55 +02:00
Björn Fromme 2f582f0548 fix: use correct country tokens 2025-10-06 15:58:44 +02:00
Björn Fromme fad28c2442 chore: update project dependencies 2025-10-06 11:22:42 +02:00
Björn Fromme 7f1ef4059d wip: submit booking to api 2025-10-06 11:22:02 +02:00
Björn Fromme 139ec3c1db wip: confirmation step 2025-10-05 13:46:19 +02:00
Björn Fromme 798beae597 feat: implement payment step 2025-10-05 13:29:19 +02:00
Björn Fromme 3edf5a80aa fix: don't submit data of removed fields 2025-10-04 18:08:11 +02:00
Björn Fromme fc93b28af0 wip: backport form handing system from create flow to edit flow 2025-10-04 12:12:16 +02:00
Björn Fromme cd82b70139 fix: ignore past travels in list view when patching data 2025-10-04 11:49:36 +02:00
Björn Fromme fba9edc931 feat: inform user about automatic reassignments or reset selections 2025-10-02 17:57:56 +02:00
Björn Fromme 8e43edcbaf feat: insurance info urls in tooltips 2025-10-02 17:28:53 +02:00
Björn Fromme 8984b2edd5 feat: customizable label for unavailable services 2025-10-02 16:56:37 +02:00
Björn Fromme 32ec2c1deb feat: make room selection fields read-only for single room type 2025-10-02 16:40:18 +02:00
Björn Fromme 25029dff5d feat: extended insurance parsing, pass info urls to view 2025-10-02 16:32:50 +02:00
Björn Fromme 875b181aff fix: filter rentals by skipass duration 2025-10-02 14:42:04 +02:00
Björn Fromme 0f3d9b87fa feat: auto-assign rooms 2025-10-02 12:25:42 +02:00
Björn Fromme d7929dd855 wip: improved insurance selection 2025-10-02 12:11:23 +02:00
Björn Fromme 8a495346d6 wip: check eligibility based on ski pass availability 2025-10-01 18:52:50 +02:00
Björn Fromme fef4997132 chore: update docs 2025-10-01 17:57:39 +02:00
Björn Fromme b4eaf695c7 feat: bulk insurance booking by applicant 2025-10-01 17:52:21 +02:00
Björn Fromme 372490ed5a feat: improved form layout 2025-09-30 17:11:30 +02:00
Björn Fromme e37b6234e8 chore: remove debugging statements 2025-09-30 16:47:29 +02:00
Björn Fromme 71b42786f7 feat: show room pricing details 2025-09-30 16:47:17 +02:00
Björn Fromme 009eabd09c fix: layout improvements 2025-09-30 16:08:59 +02:00
Björn Fromme 49a4393980 fix: consider participant's age at travel date 2025-09-30 16:08:20 +02:00
Björn Fromme c0e20a15fe fix: ensure mandatory services are selected 2025-09-30 16:08:02 +02:00
Björn Fromme 061317ed4c wip: insurance booking 2025-09-30 15:13:20 +02:00
Björn Fromme a5dce776de wip: insurance booking 2025-09-30 14:37:49 +02:00
Björn Fromme 90ae78262a wip: insurance booking 2025-09-29 19:54:20 +02:00
Björn Fromme 0809f07d46 wip: insurance booking phase 5 2025-09-26 16:54:48 +02:00
Björn Fromme 199ef08e35 wip: insurance booking phase 4 2025-09-26 16:48:48 +02:00
Björn Fromme 4930a2ba42 wip: insurance booking phase 3 2025-09-26 16:46:04 +02:00
Björn Fromme 0ceabe465b wip: insurance booking phases 1 and 2 2025-09-26 16:34:32 +02:00
Björn Fromme bd7b70e465 feat: correct calculation and display of individual pricing 2025-09-26 15:32:22 +02:00
Björn Fromme 55a24b99f0 chore: update project dependencies 2025-09-26 09:01:32 +02:00
Björn Fromme 122df1a3f5 feat: improved error handling in controllers, cleanup 2025-09-26 09:00:40 +02:00
Björn Fromme 391dd63ebf feat: improved error handling of xml parsers and loaders 2025-09-26 08:21:57 +02:00
Björn Fromme 9d44197246 feat: parser and loader for insurance data 2025-09-24 15:28:43 +02:00
Björn Fromme 582fb9b52d feat: functionality to determine family status of a booking 2025-09-24 12:58:42 +02:00
Björn Fromme eed7ba4d7b feat: prevent booking flow without available rooms 2025-09-24 11:14:45 +02:00
Björn Fromme 5642193610 feat: validate room selection 2025-09-24 10:59:23 +02:00
Björn Fromme 1a1e120cf6 feat: render tooltips 2025-09-24 10:44:01 +02:00
Björn Fromme cf66b85993 feat: streamlined tooltip data attributes 2025-09-24 10:30:14 +02:00
Björn Fromme 8677e9a8a5 chore: update project dependencies 2025-09-22 17:37:11 +02:00
Björn Fromme 906f591979 feat: mark unavailable services as read-only and add tooltip 2025-09-22 17:36:49 +02:00
Björn Fromme 97cc0ac422 feat: simplify validation 2025-09-19 16:37:48 +02:00
Björn Fromme bdc7100f74 feat: simplify handling of ski pass service 2025-09-19 16:19:01 +02:00
Björn Fromme 29a51ed66d feat: cached api results for availabilities 2025-09-19 16:09:41 +02:00
Björn Fromme c9a258a350 chore: streamline property naming 2025-09-19 16:09:27 +02:00
Björn Fromme 096dd96db0 feat: enrich travel data with live availabilities 2025-09-19 12:40:20 +02:00
Björn Fromme ddd5971c06 feat: replace xss extension with symfony component 2025-09-19 12:40:02 +02:00
Björn Fromme b7638f5e9e fix: use correct ids for user validation 2025-09-10 16:26:58 +02:00
Björn Fromme b197152f12 feat: restrict access to booking edit and invoice to applicant 2025-09-10 15:30:36 +02:00
Björn Fromme 297becf7a0 fix: filter selectable services by participant's age 2025-09-10 15:25:30 +02:00
Björn Fromme b6f991b291 wip: skipass-duration based rentals filtering 2025-09-10 14:26:18 +02:00
Björn Fromme 5469c834a6 wip: transportation services with discount 2025-09-10 14:08:44 +02:00
Björn Fromme a3ea02a136 wip: service descriptions and car license plate field 2025-09-10 12:53:33 +02:00
Björn Fromme 195246af89 wip: rentals insurance field 2025-09-03 17:54:19 +02:00
Björn Fromme 2953ed0b2d wip: improved refresh logic 2025-09-03 16:48:23 +02:00
Björn Fromme ccb2fa75ac wip: transportation services in summary 2025-09-03 14:58:24 +02:00
Björn Fromme a33e6a0dd4 wip: update documentation 2025-09-02 20:09:29 +02:00
Björn Fromme 435e3a79e8 wip: dynamic availability of services 2025-09-02 20:03:22 +02:00
Björn Fromme 21054f0c45 wip: implement transportation, pickups and parking 2025-09-02 19:32:02 +02:00
Björn Fromme 605b6ca64c wip: transportation services 2025-09-02 18:28:51 +02:00
Björn Fromme bc9616e7de wip: prepare transportation services 2025-09-02 17:52:42 +02:00
Björn Fromme 491169b9d7 wip: update docs 2025-09-02 17:21:50 +02:00
Björn Fromme 0e0ce254a9 wip: show pricing 2025-09-02 16:59:55 +02:00
Björn Fromme 7b25fbf28b chore: adjust schedule for XML cleanup to run more often 2025-09-02 09:29:36 +02:00
Björn Fromme 2a5100e76d feat: update scheduler config 2025-09-02 08:51:12 +02:00
Björn Fromme e98695ea06 feat: integrate with zenstruck schedule bundle 2025-09-02 08:47:57 +02:00
Björn Fromme 1c2421fb7e fix: respect environment variable to toggle BPN debug mode 2025-09-02 08:40:17 +02:00
Björn Fromme 76aeb101bd chore: update project dependencies 2025-09-02 08:29:22 +02:00
Björn Fromme 60b655a195 wip: dynamic services fields 2025-08-27 17:49:49 +02:00
Björn Fromme 369d660e24 wip: age based filtering of options 2025-08-27 15:02:45 +02:00
Björn Fromme 47703dc205 wip: age based filtering of options 2025-08-27 13:46:00 +02:00
Björn Fromme 25bbf3d44f wip: age based filtering of options 2025-08-27 12:42:02 +02:00
Björn Fromme 803b10f981 chore: update project dependencies 2025-08-27 10:00:01 +02:00
Björn Fromme b992e4a5ea wip: conditional mandatory body dimensions 2025-07-25 21:47:00 +02:00
Björn Fromme 9c5597eb23 wip: additional services with constraints 2025-07-25 18:12:55 +02:00
Björn Fromme 2ec61d5086 wip: courses 2025-07-25 16:53:48 +02:00
Björn Fromme 8bae958e24 wip: booking process 2025-07-25 15:47:11 +02:00
Björn Fromme e7f0c09b36 wip: major refactoring 2025-07-25 11:54:07 +02:00
Björn Fromme ae2ed306b9 wip: form refactoring 2025-07-25 09:28:13 +02:00
Björn Fromme 2b122a7cc4 wip: improved booking summary 2025-07-24 12:44:38 +02:00
Björn Fromme 787e76c21c wip: refactor 2025-07-24 11:59:40 +02:00
Björn Fromme 7b1f2e7468 wip: booking summary for step 1 2025-07-24 11:59:27 +02:00
Björn Fromme 3f94c51bc8 wip: refactor 2025-07-24 11:12:06 +02:00
Björn Fromme 64d51c01b2 wip: refactor 2025-07-24 10:03:02 +02:00
Björn Fromme 955dbd2f05 fix: correct handling of readonly checkboxes with tooltips 2025-07-24 09:33:27 +02:00
Björn Fromme 318a326fdb chore: update browserslist 2025-07-24 09:33:05 +02:00
Björn Fromme 86223dc8a6 wip: field states and dependencies 2025-07-23 18:38:31 +02:00
Björn Fromme 4bb62830a4 wip: refactor forms 2025-07-23 18:10:27 +02:00
Björn Fromme 4ce731bfcb wip: refactor forms 2025-07-23 17:15:39 +02:00
Björn Fromme f225dac5fe wip: collapsible forms 2025-07-23 15:48:59 +02:00
Björn Fromme 5e1f2a8711 wip: refactor forms 2025-07-23 15:34:54 +02:00
Björn Fromme 8746397d83 wip: feedback about booked and assigned rooms 2025-07-23 11:39:03 +02:00
Björn Fromme e8c27cb51b wip: refactoring 2025-07-23 11:08:29 +02:00
Björn Fromme d2a80ea8fd wip: refactoring 2025-07-23 10:28:38 +02:00
Björn Fromme c85c72863c wip: refactoring 2025-07-23 10:27:04 +02:00
Björn Fromme 77afc0fe7b Merge branch 'master' into booking 2025-07-22 09:03:38 +02:00
Björn Fromme 7a53908b80 chore: update deployer config with extended rsync excludes 2025-07-22 09:02:47 +02:00
Björn Fromme ca4495169f Merge branch 'master' into booking 2025-07-22 08:59:19 +02:00
Björn Fromme d1344bbfb4 chore: remove project name from ddev config to enable git worktrees 2025-07-22 08:58:44 +02:00
365 changed files with 45859 additions and 11678 deletions
-9
View File
@@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(php -l:*)",
"Bash(php:*)"
],
"deny": []
}
}
@@ -0,0 +1,11 @@
name: opencode
repository: /Users/fromme/Development/Misc/ddev-opencode
version: ""
install_date: "2026-01-05T17:21:57+01:00"
project_files:
- web-build/Dockerfile.opencode
- commands/web/opencode
- config.opencode.yaml
- opencode/.gitignore
global_files: []
removal_actions: []
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
#ddev-generated
## Description: OpenCode AI coding assistant
## Usage: opencode
## Example: ddev opencode [...]
set -e
opencode $@
+6
View File
@@ -0,0 +1,6 @@
#ddev-generated
webimage_extra_packages:
- xdg-utils
hooks:
post-start:
- exec: "mkdir -p /var/www/html/.ddev/opencode/.opencode && ln -sf /var/www/html/.ddev/opencode/.opencode ~/.opencode"
-1
View File
@@ -1,4 +1,3 @@
name: myep-next
type: php
docroot: public
php_version: "8.2"
+4
View File
@@ -0,0 +1,4 @@
services:
web:
external_links:
- "ddev-router:ep-reisen.ddev.site"
+5
View File
@@ -0,0 +1,5 @@
#ddev-generated
# OpenCode internal config
/.opencode/
+4
View File
@@ -0,0 +1,4 @@
#ddev-generated
RUN npm install -g opencode-ai
+13 -1
View File
@@ -45,8 +45,13 @@ APP_BPN_PASSWORD=
APP_BPN_IP=
APP_BPN_PORT=
APP_BPN_DEBUG=false
APP_BPN_CONNECTION_TIMEOUT=5
APP_BPN_STREAM_TIMEOUT=30
APP_BPN_TOTAL_TIMEOUT=45
APP_TRAVEL_INFO_BASE_URL=https://www.ep-reisen.de/reiseinformationen/
APP_TRAVEL_INFO_BASE_URL="https://www.ep-reisen.de/reiseinformationen/"
APP_CMS_API_BASE_URL="https://www.ep-reisen.de/"
APP_WEBSITE_BASE_URL="https://www.ep-reisen.de/"
# Travel Data Service Configuration
APP_TRAVEL_PREFER_REMOTE=false
@@ -55,6 +60,7 @@ APP_TRAVEL_ENABLE_FALLBACK=true
API_KEYS=
XML_EXPORT_PATH="%kernel.project_dir%/var/xmlexport"
JSON_EXPORT_PATH="%kernel.project_dir%/var/jsonexport"
SFTP_XML_EXPORT_HOST=
SFTP_XML_EXPORT_PORT=
@@ -66,3 +72,9 @@ OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/secret/private.key
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/secret/public.key
OAUTH_ENCRYPTION_KEY=580084fd179e67399467f59ee96658ac
###< league/oauth2-server-bundle ###
###> symfony/lock ###
# Choose one of the stores below
# postgresql+advisory://db_user:db_password@localhost/db_name
LOCK_DSN=flock
###< symfony/lock ###
+3 -4
View File
@@ -2,12 +2,11 @@
$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude('var')
;
->exclude('var');
return (new PhpCsFixer\Config())
->setRules([
'@Symfony' => true,
'@DoctrineAnnotation' => true,
])
->setFinder($finder)
;
->setFinder($finder);
+48 -1
View File
@@ -1,70 +1,117 @@
### user profile minimum
# @no-cookie-jar
GET {{base_url}}/api/userinfo
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_email")}}
### user profile full
# @no-cookie-jar
GET {{base_url}}/api/userinfo
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_profile")}}
### user crm attributes
# @no-cookie-jar
GET {{base_url}}/api/crm-attributes
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_profile")}}
### API last update at
# @no-cookie-jar
GET {{base_url}}/api/last-update
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API countries
# @no-cookie-jar
GET {{base_url}}/api/countries
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API hotels
# @no-cookie-jar
GET {{base_url}}/api/hotels
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API hotel
# @no-cookie-jar
GET {{base_url}}/api/hotels/198924
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API pickups
# @no-cookie-jar
GET {{base_url}}/api/pickups
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API pickup
# @no-cookie-jar
GET {{base_url}}/api/pickups/1
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API travels
# @no-cookie-jar
GET {{base_url}}/api/travels
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API travel
GET {{base_url}}/api/travels/DWWWE211125
# @no-cookie-jar
GET {{base_url}}/api/travels/SBW131225
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API travel remote
# @no-cookie-jar
GET {{base_url}}/api/travels/11603
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API hotel availability remote
# @no-cookie-jar
GET {{base_url}}/api/travels/11603/152546/2026-01-03/availability
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API products remote
# @no-cookie-jar
GET {{base_url}}/api/products
Accept: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
### API contact form
# @no-cookie-jar
POST {{base_url}}/api/contactform
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
{
"lastName": "Duschen",
"firstName": "Isolde",
"gender": "f",
"street": "Fooroad 1",
"zipCode": "12345",
"city": "Bartown",
"email": "[email protected]",
"phone": "12345"
}
### API pickups planning webhook
# @no-cookie-jar
POST {{base_url}}/api/pickups-planning
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
< ./assets/json/zustiegplanung.json
### API pickups planning
# @no-cookie-jar
GET {{base_url}}/api/pickups-planning/DPWWTS191125
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{$auth.token("oauth2_api")}}
+6 -4
View File
@@ -1,13 +1,15 @@
import './bootstrap.js'
import './loading.js'
import './styles/app.css'
import '@iframe-resizer/child'
import htmx from 'htmx.org'
window.htmx = htmx
htmx.config.includeIndicatorStyles = false
htmx.config.historyEnabled = false
htmx.config.historyCacheSize = 0
htmx.config.historyEnabled = true
htmx.config.historyCacheSize = 10
htmx.config.allowScriptTags = false
htmx.config.withCredentials = true
htmx.config.selfRequestsOnly = false
htmx.config.timeout = 50000 // 50 seconds - slightly higher than backend timeout (45s)
-29
View File
@@ -1,29 +0,0 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['field']
static values = { availabilities: Object }
toggle(event) {
let field = event.target
let id = field.value
let availabilities = this.availabilitiesValue
if (field.checked) {
availabilities[id].available--
} else {
availabilities[id].available++
}
field.disabled = 0 >= availabilities[id].available
field.classList.toggle('cursor-not-allowed', field.disabled)
this.availabilitiesValue = availabilities
}
availabilitiesValueChanged(availabilities) {
this.fieldTargets.forEach((field) => {
let id = field.value
if (false === field.checked && 0 >= availabilities[id].available) {
field.disabled = true
}
})
}
}
@@ -0,0 +1,32 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static classes = [ 'hidden' ]
static targets = [ 'container', 'checkboxGroup' ]
static values = { visible: Boolean }
connect() {
this.updateVisibility()
}
check() {
this.updateVisibility()
}
updateVisibility() {
if (!this.hasCheckboxGroupTarget) {
return
}
const checkboxes = this.checkboxGroupTarget.querySelectorAll('input[type="checkbox"]')
const hasChecked = Array.from(checkboxes).some(checkbox => checkbox.checked)
this.visibleValue = hasChecked
}
visibleValueChanged(visible) {
if (this.hasContainerTarget) {
this.containerTarget.classList.toggle(this.hiddenClass, false === visible)
}
}
}
@@ -1,23 +1,23 @@
import { Controller } from '@hotwired/stimulus'
import {Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'fields', 'field', 'addButton', 'index' ]
static targets = ['fields', 'field', 'addButton', 'index']
static values = {
prototype: String,
maxItems: Number,
itemsCount: Number,
}
connect () {
this.index = this.itemsCountValue = this.fieldTargets.length
connect() {
this.itemsCountValue = this.fieldTargets.length
}
addItem() {
let prototype = JSON.parse(this.prototypeValue)
const newField = prototype.replace(/__name__/g, this.index)
const index = this.fieldTargets.length + 1
const prototype = JSON.parse(this.prototypeValue)
const newField = prototype.replace(/__name__/g, index)
this.fieldsTarget.insertAdjacentHTML('beforeend', newField)
this.index++
this.itemsCountValue++
}
-14
View File
@@ -1,14 +0,0 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static values = { offsetTop: { type: Number}}
connect() {
if ('parentIframe' in window) {
window.iFrameResizer = {
onReady: function () {
window.parentIFrame.scrollTo(0, this.offsetTopValue)
}
}
}
}
}
-12
View File
@@ -1,12 +0,0 @@
import { Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'indicator' ]
static classes = [ 'hidden' ]
toggle() {
this.indicatorTarget.classList.remove(this.hiddenClass)
this.element.scrollIntoView({ block: 'start', behavior: 'smooth'})
}
}
@@ -0,0 +1,44 @@
import {Controller} from '@hotwired/stimulus'
export default class extends Controller {
static targets = ['field', 'display', 'buttonInc', 'buttonDec']
static values = {
current: Number,
min: Number,
max: Number
}
connect() {
this.currentValue = parseInt(this.fieldTarget.value) || 0
this.updateForm()
}
increase() {
if (null !== this.maxValue && this.currentValue >= this.maxValue) {
return
}
this.currentValue = this.currentValue + 1
this.updateForm()
}
decrease() {
if (null !== this.minValue && this.currentValue <= this.minValue) {
return
}
this.currentValue = this.currentValue - 1
this.updateForm()
}
updateForm() {
this.displayTarget.innerText = this.currentValue
this.fieldTarget.value = this.currentValue
this.buttonIncTarget.disabled = null !== this.maxValue && this.currentValue >= this.maxValue
this.buttonDecTarget.disabled = null !== this.minValue && this.currentValue <= this.minValue
// Trigger change event to fire HTMX request
this.fieldTarget.dispatchEvent(new Event('change', {bubbles: true}))
}
}
+69
View File
@@ -0,0 +1,69 @@
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)
}
// Bind handler for proper cleanup
this.boundHandleNotifications = this.handleNotifications.bind(this)
// Listen for custom HTMX notification events on document.body
// Events from HX-Trigger response headers bubble up to document.body
document.body.addEventListener('showNotifications', this.boundHandleNotifications)
}
disconnect() {
document.body.removeEventListener('showNotifications', this.boundHandleNotifications)
}
handleNotifications(event) {
console.log('[VOUCHER DEBUG] showNotifications event received:', event)
console.log('[VOUCHER DEBUG] event.detail:', event.detail)
// HTMX wraps the trigger value in an object with 'value' and 'elt' properties
// event.detail.value contains the actual array from the HX-Trigger header
const notifications = event.detail?.value || event.detail || []
console.log('[VOUCHER DEBUG] notifications array:', notifications)
console.log('[VOUCHER DEBUG] notifications count:', Array.isArray(notifications) ? notifications.length : 'not an array')
if (Array.isArray(notifications)) {
notifications.forEach((notification, index) => {
console.log(`[VOUCHER DEBUG] Processing notification ${index}:`, notification)
const className = this.getClassForType(notification.type)
this.showToast(notification.message, className)
})
} else {
console.error('[VOUCHER DEBUG] Notifications is not an array:', notifications)
}
}
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',
'error': 'toastify--error',
'info': 'toastify--info',
}
return typeMap[type] || 'toastify--info'
}
}
+43 -8
View File
@@ -1,18 +1,26 @@
import { Controller } from '@hotwired/stimulus'
import {Controller} from '@hotwired/stimulus'
export default class extends Controller {
static classes = [ 'closed' ]
static targets = [ 'toggle', 'icon' ]
static classes = ['closed', 'open', 'iconOpen']
static targets = ['content', 'icon', 'container']
static values = {
open: {
type: Boolean,
default: true,
},
storageKey: {
type: String,
default: ''
}
}
initialize() {
this.target = this.hasToggleTarget ? this.toggleTarget : this.element
this.target = this.hasContentTarget ? this.contentTarget : this.element
// Restore state from storage if storageKey is provided
if (this.hasStorageKey()) {
this.restoreState()
}
}
toggle() {
@@ -30,10 +38,37 @@ export default class extends Controller {
openValueChanged(open) {
this.target.classList.toggle(this.closedClass, false === open)
if (false === this.hasIconTarget) {
return
if (this.hasIconTarget) {
let iconClass = this.hasIconOpenClass ? this.iconOpenClass : 'rotate-90'
this.iconTarget.classList.toggle(iconClass, true === open)
}
this.iconTarget.classList.toggle('rotate-90', true === open)
// Toggle class on container element (or controller element) if specified
if (this.hasOpenClass) {
const container = this.hasContainerTarget ? this.containerTarget : this.element
this.openClasses.forEach(cls => {
container.classList.toggle(cls, true === open)
})
}
// Save state to storage if storageKey is provided
if (this.hasStorageKey()) {
this.saveState()
}
}
hasStorageKey() {
return this.storageKeyValue && this.storageKeyValue.trim() !== ''
}
saveState() {
sessionStorage.setItem(`toggle_${this.storageKeyValue}`, this.openValue.toString())
}
restoreState() {
const savedState = sessionStorage.getItem(`toggle_${this.storageKeyValue}`)
if (savedState !== null) {
this.openValue = savedState === 'true'
}
}
}
+3
View File
@@ -16,14 +16,17 @@ export default class extends Controller {
trigger: 'click',
allowHTML: true,
interactive: true,
appendTo: document.body,
}
} else {
options = {
content: this.contentValue,
placement,
touch: false,
trigger: 'mouseenter focus',
allowHTML: true,
interactive: true,
appendTo: document.body,
}
}
if (this.hasTriggerTarget) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

-236
View File
@@ -1,236 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="icon-dots" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
</symbol>
<symbol id="icon-feedback" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
</symbol>
<symbol id="icon-login" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9" />
</symbol>
<symbol id="icon-logout" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</symbol>
<symbol id="icon-alert" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</symbol>
<symbol id="icon-close" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</symbol>
<symbol id="icon-back" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
</symbol>
<symbol id="icon-list" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</symbol>
<symbol id="icon-menu" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</symbol>
<symbol id="icon-edit" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</symbol>
<symbol id="icon-plus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</symbol>
<symbol id="icon-minus" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</symbol>
<symbol id="icon-search" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</symbol>
<symbol id="icon-user" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</symbol>
<symbol id="icon-users" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</symbol>
<symbol id="icon-delete" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</symbol>
<symbol id="icon-check" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</symbol>
<symbol id="icon-upload" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
<path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
</symbol>
<symbol id="icon-download" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</symbol>
<symbol id="icon-sort" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</symbol>
<symbol id="icon-arrow-left" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
</symbol>
<symbol id="icon-arrow-right" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</symbol>
<symbol id="icon-document" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</symbol>
<symbol id="icon-filter" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z" />
</symbol>
<symbol id="icon-refresh" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</symbol>
<symbol id="icon-settings" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</symbol>
<symbol id="icon-at" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" d="M16.5 12a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm0 0c0 1.657 1.007 3 2.25 3S21 13.657 21 12a9 9 0 10-2.636 6.364M16.5 12V8.25" />
</symbol>
<symbol id="icon-mail" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
</symbol>
<symbol id="icon-house" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205l3 1m1.5.5l-1.5-.5M6.75 7.364V3h-3v18m3-13.636l10.5-3.819" />
</symbol>
<symbol id="icon-phone" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z" />
</symbol>
<symbol id="icon-mobile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</symbol>
<symbol id="icon-calendar" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z" />
</symbol>
<symbol id="icon-hand" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.05 4.575a1.575 1.575 0 10-3.15 0v3m3.15-3v-1.5a1.575 1.575 0 013.15 0v1.5m-3.15 0l.075 5.925m3.075.75V4.575m0 0a1.575 1.575 0 013.15 0V15M6.9 7.575a1.575 1.575 0 10-3.15 0v8.175a6.75 6.75 0 006.75 6.75h2.018a5.25 5.25 0 003.712-1.538l1.732-1.732a5.25 5.25 0 001.538-3.712l.003-2.024a.668.668 0 01.198-.471 1.575 1.575 0 10-2.228-2.228 3.818 3.818 0 00-1.12 2.687M6.9 7.575V12m6.27 4.318A4.49 4.49 0 0116.35 15m.002 0h-.002" />
</symbol>
<symbol id="icon-copy" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6" />
</symbol>
<symbol id="icon-photo" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</symbol>
<symbol id="icon-print" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0110.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0l.229 2.523a1.125 1.125 0 01-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0021 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 00-1.913-.247M6.34 18H5.25A2.25 2.25 0 013 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 011.913-.247m10.5 0a48.536 48.536 0 00-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5zm-3 0h.008v.008H15V10.5z" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-locked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</symbol>
<symbol id="icon-unlocked" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 119 0v3.75M3.75 21.75h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H3.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</symbol>
<symbol id="icon-mask" fill="currentColor" stroke="none" viewBox="0 0 576 512">
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M288 64C39.52 64 0 182.1 0 273.5C0 379.5 78.8 448 176 448c27.33 0 51.21-6.516 66.11-36.79l19.93-40.5C268.3 358.6 278.1 352.4 288 352.1c9.9 .3711 19.7 6.501 25.97 18.63l19.93 40.5C348.8 441.5 372.7 448 400 448c97.2 0 176-68.51 176-174.5C576 182.1 536.5 64 288 64zM400 400c-18.12 0-19.56-2.924-23.04-9.986l-20.35-41.33c-13.87-26.86-38.85-43.53-66.82-44.57L288 304L286.2 304.1c-27.96 1.049-52.94 17.71-67.24 45.41l-19.93 40.49C195.6 397.1 194.1 400 176 400c-75.36 0-128-52.04-128-126.5C48 229.3 48 112 288 112s240 117.3 240 161.5C528 347.1 475.4 400 400 400zM160 192C124.7 192 96 220.7 96 256s28.65 64 64 64c35.35 0 64-28.65 64-64S195.3 192 160 192zM416 192c-35.35 0-64 28.65-64 64s28.65 64 64 64c35.35 0 64-28.65 64-64S451.3 192 416 192z"/>
</symbol>
<symbol id="icon-excel" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM229.1 233.3L192 280.9L154.9 233.3C146.8 222.8 131.8 220.9 121.3 229.1C110.8 237.2 108.9 252.3 117.1 262.8L161.6 320l-44.53 57.25c-8.156 10.47-6.25 25.56 4.188 33.69C125.7 414.3 130.8 416 135.1 416c7.156 0 14.25-3.188 18.97-9.25L192 359.1l37.06 47.65C233.8 412.8 240.9 416 248 416c5.125 0 10.31-1.656 14.72-5.062c10.44-8.125 12.34-23.22 4.188-33.69L222.4 320l44.53-57.25c8.156-10.47 6.25-25.56-4.188-33.69C252.2 220.9 237.2 222.8 229.1 233.3z"/>
</symbol>
<symbol id="icon-pdf" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M365.3 93.38l-74.63-74.64C278.6 6.742 262.3 0 245.4 0H64C28.65 0 0 28.65 0 64l.0065 384c0 35.34 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM336 448c0 8.836-7.164 16-16 16H64.02c-8.838 0-16-7.164-16-16L48 64.13c0-8.836 7.164-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1V448zM202 286.1c.877-2.688 1.74-5.398 2.582-8.145c1.434-5.762 7.488-31.54 7.488-52.47C212.1 207 197.1 192 178.6 192C160.1 192 145.1 207 145.1 225.5c0 .2969 .1641 28.81 13.85 62.3c-7.035 19.36-15.57 38.8-25.41 57.93c-21.49 10.11-39.24 22.23-52.8 36.07c-6.234 6.438-9.367 14.74-9.367 24.72c0 18.45 15.01 33.46 33.46 33.46c10.8 0 20.98-5.227 27.22-13.98c7.322-10.28 18.38-26.9 30.47-48.95c15.8-6.352 33.88-11.72 53.88-16c13.55 9.578 28.9 17.29 45.71 22.95c4.527 1.551 9.402 2.348 14.43 2.348c20.26 0 36.13-16.19 36.13-36.86c0-20.33-16.54-36.87-36.87-36.87h-3.705c-2.727 .125-20.51 1.141-45.37 5.367C216.9 308.9 208.6 298.3 202 286.1zM110.2 410.4c-3.273 4.688-12.03 2.777-12.03-5.312c0-1.754 .6289-3.43 1.729-4.555c9.02-9.219 19.94-17.05 31.85-23.72C122.3 393.1 114.3 404.7 110.2 410.4zM178.6 218.8c3.693 0 6.703 3.008 6.703 6.703c0 15.21-4.109 34.84-5.746 42.1C172.1 245 171.9 227.2 171.9 225.5C171.9 221.8 174.9 218.8 178.6 218.8zM162.3 348.3c6.611-13.48 13.22-28.46 19.38-44.7c6.389 10.92 14.56 21.86 24.96 31.97C192.6 338.8 177.4 342.9 162.3 348.3zM272.4 339.5h3.352c5.539 0 10.05 4.5 10.05 10.79c0 5.129-4.176 9.32-9.32 9.32c-2.029 0-4.059-.3164-5.852-.9414c-12.33-4.137-23.11-9.32-32.54-15.19C258.3 340.3 272.1 339.5 272.4 339.5z"/>
</symbol>
<symbol id="icon-file" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M365.3 93.38l-74.63-74.64C278.6 6.743 262.3 0 245.4 0L64-.0001c-35.35 0-64 28.65-64 64l.0065 384c0 35.35 28.65 64 64 64H320c35.2 0 64-28.8 64-64V138.6C384 121.7 377.3 105.4 365.3 93.38zM320 464H64.02c-8.836 0-15.1-7.163-16-15.1L48 64.13c-.0004-8.837 7.163-16 16-16h160L224 128c0 17.67 14.33 32 32 32h79.1v288C336 456.8 328.8 464 320 464z"/>
</symbol>
<symbol id="icon-sigma" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M335.1 136l.0005-56H83.59l165 158.7C253.3 243.2 255.1 249.5 255.1 256s-2.656 12.78-7.375 17.31L83.59 432h252.4l-.0005-56c0-13.25 10.75-24 24-24C373.2 352 384 362.8 384 376v80c0 13.25-10.75 24-24 24H23.99c-9.782 0-18.59-5.938-22.25-15.03s-1.438-19.47 5.625-26.28L197.4 256L7.364 73.31C.3015 66.5-1.917 56.13 1.739 47.03S14.21 32 23.99 32h336C373.2 32 384 42.75 384 56v80C384 149.3 373.2 160 359.1 160C346.7 160 335.1 149.3 335.1 136z"/>
</symbol>
<symbol id="icon-percent" fill="currentColor" stroke="none" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M376.1 71.03c-9.375-9.375-24.56-9.375-33.94 0l-336 336c-9.375 9.375-9.375 24.56 0 33.94C11.72 445.7 17.84 448 24 448s12.28-2.344 16.97-7.031l336-336C386.3 95.59 386.3 80.41 376.1 71.03zM64 176c26.51 0 48-21.49 48-48S90.51 80 64 80C37.49 80 16 101.5 16 128S37.49 176 64 176zM320 336c-26.51 0-48 21.49-48 48s21.49 48 48 48c26.51 0 48-21.49 48-48S346.5 336 320 336z"/>
</symbol>
<symbol id="icon-eye" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</symbol>
<symbol id="icon-profile" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0119.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 004.5 10.5a7.464 7.464 0 01-1.15 3.993m1.989 3.559A11.209 11.209 0 008.25 10.5a3.75 3.75 0 117.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 01-3.6 9.75m6.633-4.596a18.666 18.666 0 01-2.485 5.33" />
</symbol>
<symbol id="icon-chart" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
</symbol>
<symbol id="icon-info" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</symbol>
<symbol id="icon-star" fill="currentColor" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
</symbol>
<symbol id="icon-cancel" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</symbol>
<symbol id="icon-bell" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</symbol>
<symbol id="icon-folder" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="icon-power" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
</symbol>
<symbol id="icon-no-symbol" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 0 0 5.636 5.636m12.728 12.728A9 9 0 0 1 5.636 5.636m12.728 12.728L5.636 5.636" />
</symbol>
<symbol id="icon-bus" xmlns="http://www.w3.org/2000/svg" stroke-width="1.5" viewBox="0 0 24 24" stroke="currentColor" fill="none">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M6 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M18 17m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path d="M4 17h-2v-11a1 1 0 0 1 1 -1h14a5 7 0 0 1 5 7v5h-2m-4 0h-8" />
<path d="M16 5l1.5 7l4.5 0" />
<path d="M2 10l15 0" />
<path d="M7 5l0 5" />
<path d="M12 5l0 5" />
</symbol>
<symbol id="icon-ski" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M380.7 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM2.7 268.9c6.1-11.8 20.6-16.3 32.4-10.2L239.8 365l49.6-74.5-82.7-87.9c-9.5-10.1-14.4-22.4-15.1-34.9l87.7 42 49.4 52.5c12.8 13.6 14.5 34.1 4.2 49.6l-50.2 75.4 135.8 70.5c13.6 7.1 29.8 7.2 43.6 .3l15.2-7.6c11.9-5.9 26.3-1.1 32.2 10.7s1.1 26.3-10.7 32.2l-15.2 7.6c-27.5 13.7-59.9 13.5-87.2-.7L12.9 301.3C1.2 295.2-3.4 280.7 2.7 268.9zM118.9 65.6L137 74.2l8.7-17.4c4-7.9 13.6-11.1 21.5-7.2s11.1 13.6 7.2 21.5l-8.5 16.9 55.5 26.6c1.7-.9 3.5-1.7 5.4-2.5l80.9-32.4c32.4-13 68.6 6.5 75.6 40.7l12.3 59.7 62.9 30.1c12 5.7 17 20 11.3 32s-20 17-32 11.3l-54.2-25.9c-1-.3-1.9-.6-2.8-1l-229.1-110-9.2 18.4c-4 7.9-13.6 11.1-21.5 7.2s-11.1-13.6-7.2-21.5l9-18-17.6-8.4c-8-3.8-11.3-13.4-7.5-21.3s13.4-11.3 21.3-7.5zm217.3 64.7c-1-4.9-6.2-7.7-10.8-5.8l-45.7 18.3 65.5 31.5-9-43.9z"/>
</symbol>
<symbol id="icon-snowboard" fill="currentColor" stroke="nonw" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M204.3 3c11.6-6.4 26.2-2.3 32.6 9.3l31.9 57.4c1.3 2.3 3.6 3.8 6.2 4.1l33.8 3.4c24.1 2.4 46.9 12 65.4 27.6L503.5 213.6c10.1 8.5 11.4 23.7 2.9 33.8s-23.7 11.4-33.8 2.9l-68.2-57.5-82.3 43.9 24.7 18.9c24.9 19 34.6 51.9 24.1 81.4l-28.2 79c-1.4 3.8-3.6 7-6.3 9.6l93.3 35.7c4.6 1.8 9.4 2.6 14.3 2.6H472c13.3 0 24 10.7 24 24s-10.7 24-24 24H443.8c-10.8 0-21.4-2-31.5-5.8L60.1 371.3c-11.5-4.4-22-11.2-30.8-20L7 329c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l22.4 22.4c4 4 8.7 7.1 14 9.1l19.4 7.4c-2.9-11.4 2.9-23.4 14-28.1L192 272l0-68.2c0-21.2 12-40.6 31-50.1L284.4 123l-14.1-1.4c-18.3-1.8-34.5-12.5-43.4-28.5L195 35.7c-6.4-11.6-2.3-26.2 9.3-32.6zm91.8 407.2c-.3-3.4 .1-6.9 1.3-10.3l28.2-79c3.5-9.8 .3-20.8-8-27.1L240 234.4l0 42.9c0 16.1-9.7 30.7-24.6 36.9L134 348.2l162.1 62.1zM259.6 189l20.5 15.7 84.6-45.1-17.2-14.5L259.6 189zM384 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0z"/>
</symbol>
<symbol id="icon-hourglass" fill="currentColor" stroke="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512">
<!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M24 0C10.7 0 0 10.7 0 24S10.7 48 24 48h8V67c0 40.3 16 79 44.5 107.5L158.1 256 76.5 337.5C48 366 32 404.7 32 445v19H24c-13.3 0-24 10.7-24 24s10.7 24 24 24H360c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8V445c0-40.3-16-79-44.5-107.5L225.9 256l81.5-81.5C336 146 352 107.3 352 67V48h8c13.3 0 24-10.7 24-24s-10.7-24-24-24H24zM192 289.9l81.5 81.5C293 391 304 417.4 304 445v19H80V445c0-27.6 11-54 30.5-73.5L192 289.9zm0-67.9l-81.5-81.5C91 121 80 94.6 80 67V48H304V67c0 27.6-11 54-30.5 73.5L192 222.1z"/>
</symbol>
<symbol id="icon-flag-a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd"><path fill="#fff" d="M640 480H0V0h640z"/><path fill="#ed2939" d="M640 480H0V320h640zm0-319.9H0V.1h640z"/></g></symbol>
<symbol id="icon-flag-ch" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#d52b1e" d="M0 0h640v480H0z"/><g fill="#fff"><path d="M170 195h300v90H170z"/><path d="M275 90h90v300h-90z"/></g></g></symbol>
<symbol id="icon-flag-d" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><path fill="#ffce00" d="M0 320h640v160H0z"/><path d="M0 0h640v160H0z"/><path fill="#d00" d="M0 160h640v160H0z"/></symbol>
<symbol id="icon-flag-f" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#00267f" d="M0 0h213.3v480H0z"/><path fill="#f31830" d="M426.7 0H640v480H426.7z"/></g></symbol>
<symbol id="icon-flag-i" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480"><g fill-rule="evenodd" stroke-width="1pt"><path fill="#fff" d="M0 0h640v480H0z"/><path fill="#009246" d="M0 0h213.3v480H0z"/><path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/></g></symbol>
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<symbol id="icon-spinner" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="0s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="0s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="-0.9s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="-0.9s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
</g>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 31 KiB

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+673
View File
@@ -0,0 +1,673 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 502.8 337.7" style="enable-background:new 0 0 502.8 337.7;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_2_);fill:url(#SVGID_3_);}
.st1{fill:url(#SVGID_4_);}
.st2{clip-path:url(#SVGID_6_);fill:url(#SVGID_7_);}
.st3{fill:url(#SVGID_8_);}
.st4{fill:url(#SVGID_9_);}
.st5{fill:url(#SVGID_10_);}
.st6{fill:#F3A21A;}
.st7{fill:url(#SVGID_11_);}
.st8{fill:url(#SVGID_12_);}
.st9{fill:url(#SVGID_13_);}
.st10{fill:#EB9D18;}
.st11{fill:url(#SVGID_14_);}
.st12{fill:url(#SVGID_15_);}
.st13{fill:#FFFFFF;}
.st14{fill:#ADACB0;}
.st15{fill:#494948;}
.st16{fill:#C10A27;}
</style>
<g id="Blason">
<g>
<g>
<defs>
<path id="SVGID_1_" d="M186.7,1.6c-7.7,3.8-8.3,11.4-6.8,19c1.4,6.9,4.8,14.1,8,20.4c26.6,48.7,52.7,97.6,78,147
c7.3,14.2,14.5,28.4,21.6,42.6c6.8-4.7,13.7-9.5,20.5-14.3c-7.2-14.5-14.4-29.1-21.4-43.6C262.8,124,240,75,217.3,25.7
c-3.4-7-7.5-14.4-13.1-20c-2.6-2.6-5.4-4.5-8.5-5.3C192.9-0.3,189.9,0,186.7,1.6"/>
</defs>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="185.9588" y1="703.2838" x2="298.626" y2="480.6175" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="1.840490e-02" style="stop-color:#2E2E2D"/>
<stop offset="6.957448e-02" style="stop-color:#414140"/>
<stop offset="0.1472" style="stop-color:#5F5E5E"/>
<stop offset="0.2218" style="stop-color:#494948"/>
<stop offset="0.3098" style="stop-color:#373636"/>
<stop offset="0.4193" style="stop-color:#1E1D1D"/>
<stop offset="0.5731" style="stop-color:#070707"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<polygon class="st0" points="181.3,-3.2 352.9,40.6 303.4,234.6 131.7,190.8 "/>
</g>
</g>
<g>
<g>
<path d="M283.2,191.8c-2.4-7.3-4.6-14.7-7-21.8c-8.5-25.5-15.8-51.1-24.2-76.3c-3.6-10.6-6.6-21.3-10.1-31.7
c-1.1-3.4-2.1-6.7-3.1-9.8c-0.7-2.1-0.8-4.4-1.9-6c0.1-0.9,0.4-1,0.5-1.9h0.5c0.5,2.2,1.6,3.7,2.6,5.3c1.4,0.3,2.1,0.3,3.8,0.2
c0.4-0.4,0.9-0.4,1.4-0.7c1.9-1.3,3.7-4.7,4.3-7.2c0.5-2-0.2-6.2,0.7-7.7c-0.2-0.9-0.3-1-1-1.4c0-0.9,0.1-1.4,0.5-1.9
c-0.4-0.7-1.1-0.6-1.4-1.2c-0.1-0.6-0.2-1.1-0.2-1.7c-1-2.5-2-6.2-3.6-8.4c-0.7-1-2.3-1.6-3.4-2.4c-1.7-1.3-2.8-3.6-4.1-5.3
c-0.7-0.9-2-1.4-2.9-2.2c-3.2,0.1-4.3,1.8-6.7,2.2c0-0.6,0-0.9-0.2-1.2v-0.5c-5.9-0.2-6.9,1.9-10.6,4.3c0.6,2.3,3.8,2.1,4.6,4.6
c0.7,1,0.4,2.3-0.2,2.9c0.3,1,2.1,1.9,2.9,2.4c0.3,1.9,0.5,3.2-0.2,4.8c1.3,0.7,2.7,1.6,3.1,3.1c0.4,1.4-0.7,3.4-0.5,4.1
c0.3,0.2,0.6,0.5,1,0.7c0.8,1.2,2.1,4.5,1.2,5.8c0.3,2.1,1,0.7,2.2,1.7c0.5,0.4,0.6,1.4,1,1.9c0.2,0.3,1,0.4,1.2,0.7
c0.6,2.2,1.3,4.5,1.9,6.7c1.9,4.8,3.6,10.4,5.3,15.4c10.1,30.2,18.9,60.3,29,90.5c3.4,10.2,6.6,20.7,10.1,31.2
c1.3,3.9,4.1,9.8,4.6,13.9l0.2,0.2h3.1C286.5,200.5,284.6,196.1,283.2,191.8z M233.5,27.4c-2.1-2.7-2-8.2-4.3-10.8
c0.2-1.2,0.8-1.2,1.4-1.9h1.2c1,2,1.9,4.5,2.4,7C232.7,23.1,233.4,24.7,233.5,27.4z M234.7,14.9c-0.8-0.5-1.6-1-2.4-1.4h-0.2
c0.3-1,0.8-1.3,1.4-1.9h1.7c2.2,2.3,4.4,4.7,6.7,7c0.9,0.9,2.2,1.6,2.9,2.6c3.8,6.1,1.7,20.5-1.7,26.4c-0.7,0-1.1,0-1.4-0.2
c-1.7-2.2-1.4-8.1-2.2-11.3c-1.3-5.7-2-12-3.6-17C235.5,17.6,235.5,16.1,234.7,14.9z"/>
</g>
</g>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="134.3743" y1="628.9778" x2="244.3741" y2="544.978" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="1.840490e-02" style="stop-color:#2E2E2D"/>
<stop offset="9.394112e-02" style="stop-color:#414140"/>
<stop offset="0.2086" style="stop-color:#5F5E5E"/>
<stop offset="0.2778" style="stop-color:#494948"/>
<stop offset="0.3594" style="stop-color:#373636"/>
<stop offset="0.4611" style="stop-color:#1E1D1D"/>
<stop offset="0.6038" style="stop-color:#070707"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<path class="st1" d="M254.8,140.4c0,0-48.7-35.8-63.8-52.3c-15.1-16.6-31.7-21.6-40.6-20.6s-14.6,7.1-14.6,7.1s-7,7.2-7.7,13.3
c-0.7,5.8-1.1,7.6,0.1,12c1.2,4.4,2.2,14.9,27.2,28.7c35.9,19.7,83.3,56.8,83.3,56.8L254.8,140.4z"/>
<g>
<g>
<defs>
<path id="SVGID_5_" d="M369.8,1.6c7.7,3.8,8.3,11.4,6.8,19c-1.4,6.9-4.8,14.1-8,20.4c-26.6,48.7-52.7,97.6-78,147
c-7.3,14.2-14.5,28.4-21.6,42.6c-6.8-4.7-13.7-9.5-20.5-14.3c7.2-14.5,14.4-29.1,21.4-43.6c23.8-48.7,46.6-97.7,69.3-147
c3.4-7,7.5-14.4,13-20c2.6-2.6,5.4-4.5,8.5-5.3C363.6-0.3,366.6,0,369.8,1.6"/>
</defs>
<clipPath id="SVGID_6_">
<use xlink:href="#SVGID_5_" style="overflow:visible;"/>
</clipPath>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-15.0273" y1="703.2908" x2="97.6398" y2="480.6245" gradientTransform="matrix(-1 0 0 -1 355.5174 707.5911)">
<stop offset="1.840490e-02" style="stop-color:#2E2E2D"/>
<stop offset="6.957448e-02" style="stop-color:#414140"/>
<stop offset="0.1472" style="stop-color:#5F5E5E"/>
<stop offset="0.2218" style="stop-color:#494948"/>
<stop offset="0.3098" style="stop-color:#373636"/>
<stop offset="0.4193" style="stop-color:#1E1D1D"/>
<stop offset="0.5731" style="stop-color:#070707"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<polygon class="st2" points="375.2,-3.2 203.6,40.6 253.1,234.6 424.8,190.8 "/>
</g>
</g>
<g>
<g>
<path d="M269,205h3.1l0.2-0.2c0.4-4.2,3.3-10,4.6-13.9c3.5-10.5,6.7-21,10.1-31.2c10.1-30.2,19-60.3,29-90.5
c1.7-5,3.3-10.5,5.3-15.4c0.6-2.2,1.3-4.5,1.9-6.7c0.2-0.3,1-0.4,1.2-0.7c0.4-0.5,0.5-1.5,1-1.9c1.1-1,1.9,0.4,2.2-1.7
c-0.9-1.3,0.4-4.5,1.2-5.8c0.3-0.2,0.6-0.5,1-0.7c0.2-0.7-0.9-2.7-0.5-4.1c0.5-1.5,1.8-2.4,3.1-3.1c-0.8-1.6-0.5-2.9-0.2-4.8
c0.8-0.5,2.6-1.4,2.9-2.4c-0.6-0.6-0.9-1.9-0.2-2.9c0.7-2.5,4-2.2,4.6-4.6c-3.6-2.4-4.6-4.5-10.6-4.3v0.5
c-0.2,0.3-0.2,0.6-0.2,1.2c-2.4-0.4-3.5-2-6.7-2.2c-0.9,0.8-2.2,1.2-2.9,2.2c-1.3,1.7-2.4,4-4.1,5.3c-1,0.8-2.6,1.4-3.4,2.4
c-1.6,2.2-2.6,5.9-3.6,8.4c-0.1,0.6-0.2,1.1-0.2,1.7c-0.3,0.6-1.1,0.5-1.4,1.2c0.4,0.5,0.5,1,0.5,1.9c-0.6,0.4-0.8,0.5-1,1.4
c0.9,1.5,0.2,5.7,0.7,7.7c0.7,2.5,2.4,5.9,4.3,7.2c0.5,0.4,1,0.3,1.4,0.7c1.7,0.1,2.4,0.1,3.8-0.2c1-1.6,2.1-3,2.6-5.3h0.5
c0.1,0.9,0.3,1,0.5,1.9c-1.2,1.6-1.2,3.9-1.9,6c-1.1,3.2-2,6.5-3.1,9.8c-3.5,10.4-6.5,21.1-10.1,31.7
c-8.4,25.2-15.7,50.9-24.2,76.3c-2.4,7.1-4.5,14.5-7,21.8C271.9,196.1,270.1,200.5,269,205z M322.3,21.7c0.5-2.4,1.4-4.9,2.4-7
h1.2c0.7,0.7,1.2,0.7,1.4,1.9c-2.4,2.6-2.2,8.1-4.3,10.8C323.1,24.7,323.8,23.1,322.3,21.7z M320.6,19c-1.6,5-2.3,11.3-3.6,17
c-0.8,3.2-0.5,9.1-2.2,11.3c-0.3,0.2-0.8,0.2-1.4,0.2c-3.4-5.9-5.5-20.3-1.7-26.4c0.7-1.1,2-1.8,2.9-2.6c2.3-2.3,4.5-4.7,6.7-7
h1.7c0.6,0.6,1.1,0.9,1.4,1.9h-0.2c-0.8,0.5-1.6,1-2.4,1.4C321,16.1,321.1,17.6,320.6,19z"/>
</g>
</g>
<linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="-66.6074" y1="629.002" x2="43.393" y2="545.0015" gradientTransform="matrix(-1 0 0 -1 355.5174 707.5911)">
<stop offset="1.840490e-02" style="stop-color:#2E2E2D"/>
<stop offset="9.394112e-02" style="stop-color:#414140"/>
<stop offset="0.2086" style="stop-color:#5F5E5E"/>
<stop offset="0.2778" style="stop-color:#494948"/>
<stop offset="0.3594" style="stop-color:#373636"/>
<stop offset="0.4611" style="stop-color:#1E1D1D"/>
<stop offset="0.6038" style="stop-color:#070707"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<path class="st3" d="M317.8,185.3c0,0,47.4-37.1,83.3-56.8c25.1-13.7,26-24.2,27.2-28.7c1.2-4.4,0.8-6.2,0.1-12
c-0.7-6.1-7.7-13.3-7.7-13.3s-5.8-6.1-14.6-7.1c-8.9-1-25.4,4.1-40.6,20.6c-15.1,16.6-63.8,52.3-63.8,52.3L317.8,185.3z"/>
<linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="278.3112" y1="431.7694" x2="278.3112" y2="615.46" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="0" style="stop-color:#DA9B24"/>
<stop offset="1" style="stop-color:#FDC762"/>
</linearGradient>
<path class="st4" d="M277.9,92.2c4.7,0.1,28.4-1.5,66.9,14.2c3,1.2,3.4,1.7,4.6,4.6c6.2,18.9,8.7,44.3,7.2,60.2
c-1.5,32.2-20.5,68.7-43.8,84.7c-4.2,3.4-23.2,16.3-30.9,19.4c-2.9,1.5-4.3,1.8-8.6-0.4c-4.3-2.2-17.5-8.1-29.1-18.3
c-11.3-8.9-41.2-33.7-44.6-85.9c-0.1-41.9,5.4-52.6,7.1-59.2c1.9-3.3,1.4-4.5,5.7-5.8C216.7,104.3,247.7,91.8,277.9,92.2z"/>
<linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="278.349" y1="605.4353" x2="278.349" y2="441.3781" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="0" style="stop-color:#F2BF4B"/>
<stop offset="1" style="stop-color:#EBAE24"/>
</linearGradient>
<path class="st5" d="M278,99.6c4.3,0.1,26.2-1.4,61.6,13c2.8,1.1,3.1,1.6,4.2,4.3c5.7,17.4,8,40.7,6.6,55.4
c-1.4,29.7-18.9,63.2-40.3,78c-3.8,3.1-21.4,15-28.4,17.8c-2.7,1.3-3.9,1.6-7.9-0.4c-3.9-2-16.1-7.5-26.8-16.8
c-10.4-8.2-37.9-31-41.1-79.1c-0.1-38.5,5-48.4,6.5-54.5c1.7-3,1.3-4.1,5.2-5.4C221.6,110.7,250.1,99.2,278,99.6z"/>
<path class="st6" d="M278.5,245.6c-0.9,0.1-1.9-0.2-3.4-1c-2.8-1.5-11.6-5.4-19.4-12.1c-7.5-5.9-27.3-22.4-29.7-57.1
c0-27.8,3.6-34.9,4.7-39.3c0.6-1.1,0.8-1.8,1.3-2.4l46.5,44.9V245.6z"/>
<linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="304.4899" y1="551.4356" x2="304.4899" y2="478.9356" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="0" style="stop-color:#F8B640"/>
<stop offset="1" style="stop-color:#F9C660"/>
</linearGradient>
<path class="st7" d="M278.5,245.6c0.7,0,1.4-0.3,2.2-0.8c5.1-2,17.7-10.6,20.5-12.9c15.5-10.7,28.1-34.9,29.1-56.3
c1-10.6-0.7-27.4-4.8-40c-0.4-1.1-0.7-1.6-1.2-2.1l-45.9,44.9L278.5,245.6L278.5,245.6z"/>
<linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="232" y1="556.7395" x2="324.4" y2="556.7395" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="0" style="stop-color:#F8B640"/>
<stop offset="1" style="stop-color:#F9C660"/>
</linearGradient>
<path class="st8" d="M324.4,133.6c-0.4-0.4-0.9-0.6-1.8-1c-25.6-10.4-41.4-9.3-44.5-9.4c-20.1-0.3-40.7,8.1-43.6,9
c-1.4,0.5-2.1,0.9-2.5,1.5l46.5,44.9L324.4,133.6z"/>
<radialGradient id="SVGID_13_" cx="278.2325" cy="523.2651" r="49.5127" gradientTransform="matrix(1 0 0 -1 0 707.5911)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#C2172D"/>
<stop offset="1" style="stop-color:#951922"/>
</radialGradient>
<path class="st9" d="M278.1,131.2c2.7,0.1,16.4-0.9,38.6,8.2c1.7,0.7,1.9,1,2.6,2.7c3.6,10.9,5,25.6,4.1,34.7
c-0.9,18.6-11.8,39.6-25.3,48.9c-2.4,2-13.4,9.4-17.8,11.2c-1.7,0.8-2.5,1-4.9-0.2c-2.5-1.3-10.1-4.7-16.8-10.5
c-6.5-5.1-23.8-19.4-25.8-49.6c0-24.2,3.1-30.3,4.1-34.2c1.1-1.9,0.8-2.6,3.3-3.4C242.7,138.2,260.6,130.9,278.1,131.2z"/>
<path class="st10" d="M281.3,219.5l-4-0.2c0,0-9.2,0.2-14-12.6l-20-57.6c0,0,17,7.2,21.5,22L281.3,219.5z"/>
<path class="st10" d="M307.2,158.7h-4.1l-6.5,24.8c-3.6,14.4-14.2,17.2-14.2,17.2l11.1-42h-12.9c0,0,1.2-8.8,10.5-8.8h27.2
C318.3,149.9,315,158.7,307.2,158.7z"/>
<linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="278.3005" y1="510.4355" x2="278.3005" y2="573.277" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="0.3006" style="stop-color:#FFFFFF;stop-opacity:0.15"/>
<stop offset="1" style="stop-color:#EBAE24;stop-opacity:0"/>
</linearGradient>
<path class="st11" d="M209.6,174.2c0,0,18.2,7.8,24,8.7c5.4,4.7,15.7,10.2,21.8,12.2c10.4,2.9,17.3,3.3,22.6,3.3
c5.3,0,15.7-0.2,24.9-4.2c5.7-2.5,14.9-7.1,20.5-13.3c7-1.6,23.7-9.7,23.7-9.7s0.9-31.3-6.8-49.4c-0.7-1.6-1.7-1.7-1.7-1.7
s-28.1-12.2-59.9-12.2s-60.5,11.9-60.5,11.9s-0.8,0-1.6,1.9C215.8,123.5,208.4,144.6,209.6,174.2z"/>
<linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="278.9793" y1="525.4617" x2="205.7293" y2="544.7117" gradientTransform="matrix(1 0 0 -1 0 707.5911)">
<stop offset="0" style="stop-color:#FFFFFF;stop-opacity:8.000000e-02"/>
<stop offset="1" style="stop-color:#EBAE24;stop-opacity:0"/>
</linearGradient>
<path class="st12" d="M281.6,268.1c-2.7,1.3-3.9,1.6-7.9-0.4c-3.9-2-16.1-7.5-26.8-16.8c-10.4-8.2-37.9-31-41.1-79.1
c-0.1-38.5,5-48.4,6.5-54.5c1.7-3,1.3-4.1,5.2-5.4c4-1.3,32.5-12.8,60.3-12.4c4.3,0.1,26.2-1.4,61.6,13c1.4,0.6,1.4,0.6,2.7,1.6
C237.8,167.7,281.6,268.1,281.6,268.1z"/>
<path class="st13" d="M248.8,34c0,0-1.3,8.4-2.7,12.1C244.7,49.9,251.8,41.9,248.8,34z"/>
<path class="st13" d="M307.8,34c0,0,1.3,8.4,2.7,12.1C311.9,49.9,304.7,41.9,307.8,34z"/>
</g>
<g id="Calque_6">
</g>
<g id="Fond_texte">
<g>
<path class="st13" d="M233.4,242.3c-12.2,0-17.7-9.7-17.7-16.5c0-5.7,2.5-10.8,7.1-14.6c-0.9-2.1-1.4-4.2-1.4-6.4
c0-6.8,6.6-14.5,16.2-14.5c3.4,0,6.6,0.7,8.8,1.9c2.9,1.7,2.9,4.3,2.9,5.1c0,1.5-0.2,5.9-0.5,7.2c-0.8,4-3.8,4.4-4.7,4.4
c-0.5,0-4.6-0.1-4.6-5.3c0-0.8-0.2-1.4-0.5-1.8c-0.5-0.5-1.3-1.1-2.5-1.1c-2.8,0-2.8,2.3-2.8,3c0,2.6,2.3,5.5,7.6,11.3
c0.9,1,3.4,3.4,5.8,5.6c0.1-0.5,0.1-0.9,0.2-1.4c-2.5-0.2-4.4-2.2-4.4-4.6c0-2.2,1.8-4.6,5.1-4.6c2.2,0,4.6,0.1,7.1,0.4h0.1
c2.8,0.4,4.7,2.5,4.7,5.3c0,4.3-2,8.9-4,12.3c1.7,1.3,3,2.1,4,2.7c1.6,1,3.6,1.6,4.5,1.6c3.7,0,5,2.8,5,4.6c0,2.6-2.2,4.6-5.2,4.6
h-5.4c-4.5,0-6.9-0.7-11.4-4C243.3,240.7,238.6,242.3,233.4,242.3z M228.9,220.4c-0.3,0.8-0.5,1.7-0.5,2.8c0,4.5,2.9,7.7,6.9,7.7
c1.3,0,2.4-0.2,3.4-0.5c-2.6-2.4-5.1-4.9-6.1-5.9C232.3,224.2,230.7,222.4,228.9,220.4z"/>
<path class="st14" d="M219.9,225.8c0-6.5,4.2-10.8,8.5-13.4c-2.1-2.9-2.7-5.2-2.7-7.7c0-4.4,4.5-10.2,11.9-10.2
c2.8,0,5.2,0.6,6.7,1.4c0.7,0.4,0.7,0.7,0.7,1.4c0,1.7-0.2,5.6-0.4,6.4c-0.2,0.8-0.3,0.9-0.6,0.9c-0.3,0-0.3-0.2-0.3-1.1
c0-1.4-0.3-3.3-1.7-4.7c-1.2-1.3-3.1-2.4-5.6-2.4c-3.3,0-7.1,2.1-7.1,7.3c0,4.3,3,7.9,8.8,14.2c1.9,2.1,8.7,8.4,10.5,9.9
c1.6-2.1,2.8-5.8,2.8-9.2c0-0.8-0.2-1.9-1.1-2.7c-0.7-0.7-1.9-0.9-2.7-0.9c-0.3,0-0.7-0.1-0.7-0.4s0.4-0.4,0.9-0.4
c1.3,0,3.6,0.1,6.7,0.4c0.8,0.1,1,0.6,1,1.1c0,4.5-3.1,10.1-5.5,13.1c3.7,3.1,6.1,4.6,7.7,5.5c1.8,1.1,4.6,2.2,6.6,2.2
c0.5,0,0.8,0.1,0.8,0.4c0,0.2-0.4,0.4-0.9,0.4h-5.4c-4.1,0-5.4-0.4-11.5-5.2c-1.3,1.3-5.7,6-13.9,6
C223.6,238,219.9,230.6,219.9,225.8z M245.8,231c-3.1-2.6-8.6-7.8-10.1-9.4c-1.4-1.6-5-5.5-6.6-7.8c-2.9,1.9-5,4.9-5,9.3
c0,6.9,4.9,11.9,11.1,11.9C240.5,235.1,243.9,232.9,245.8,231z"/>
</g>
<g>
<path class="st14" d="M219.9,225.8c0-6.5,4.2-10.8,8.5-13.4c-2.1-2.9-2.7-5.2-2.7-7.7c0-4.4,4.5-10.2,11.9-10.2
c2.8,0,5.2,0.6,6.7,1.4c0.7,0.4,0.7,0.7,0.7,1.4c0,1.7-0.2,5.6-0.4,6.4c-0.2,0.8-0.3,0.9-0.6,0.9c-0.3,0-0.3-0.2-0.3-1.1
c0-1.4-0.3-3.3-1.7-4.7c-1.2-1.3-3.1-2.4-5.6-2.4c-3.3,0-7.1,2.1-7.1,7.3c0,4.3,3,7.9,8.8,14.2c1.9,2.1,8.7,8.4,10.5,9.9
c1.6-2.1,2.8-5.8,2.8-9.2c0-0.8-0.2-1.9-1.1-2.7c-0.7-0.7-1.9-0.9-2.7-0.9c-0.3,0-0.7-0.1-0.7-0.4s0.4-0.4,0.9-0.4
c1.3,0,3.6,0.1,6.7,0.4c0.8,0.1,1,0.6,1,1.1c0,4.5-3.1,10.1-5.5,13.1c3.7,3.1,6.1,4.6,7.7,5.5c1.8,1.1,4.6,2.2,6.6,2.2
c0.5,0,0.8,0.1,0.8,0.4c0,0.2-0.4,0.4-0.9,0.4h-5.4c-4.1,0-5.4-0.4-11.5-5.2c-1.3,1.3-5.7,6-13.9,6
C223.6,238,219.9,230.6,219.9,225.8z M245.8,231c-3.1-2.6-8.6-7.8-10.1-9.4c-1.4-1.6-5-5.5-6.6-7.8c-2.9,1.9-5,4.9-5,9.3
c0,6.9,4.9,11.9,11.1,11.9C240.5,235.1,243.9,232.9,245.8,231z"/>
</g>
<path class="st14" d="M110.2,249.4v-26.6c-0.9-1.3-1.7-2.7-2.3-4.2c-1.2-2.8-1.8-5.8-1.8-8.9s0.6-6.1,1.8-8.9
c1.2-2.7,2.8-5.1,4.8-7.2c2-2,4.5-3.7,7.2-4.8c2.8-1.2,5.8-1.8,8.9-1.8h35.6v26.6c0.9,1.3,1.7,2.7,2.3,4.2c1.2,2.8,1.8,5.8,1.8,8.9
s-0.6,6.1-1.8,8.9c-1.2,2.7-2.8,5.2-4.9,7.2c-2.1,2.1-4.5,3.7-7.2,4.9c-2.8,1.2-5.8,1.8-8.9,1.8L110.2,249.4L110.2,249.4z"/>
<path class="st13" d="M114.8,244.9v-19.7h4.4c-1.2-0.7-2.3-1.6-3.2-2.6c-1.6-1.6-2.9-3.6-3.8-5.8c-1-2.2-1.4-4.6-1.4-7.2
c0-2.5,0.5-4.9,1.4-7.2c0.9-2.2,2.2-4.1,3.9-5.8c1.6-1.6,3.6-2.9,5.8-3.9s4.6-1.4,7.2-1.4h31.1V211h-4.5c1.1,0.7,2.2,1.5,3.2,2.5
c1.7,1.6,3,3.6,4,5.8c1,2.2,1.4,4.6,1.4,7.2c0,2.5-0.5,4.9-1.4,7.2c-0.9,2.2-2.3,4.1-3.9,5.8c-1.7,1.7-3.6,3-5.8,3.9
c-2.2,1-4.6,1.4-7.2,1.4h-31.2V244.9z"/>
<path d="M154.3,197v8.4h-25.4c-1.2,0-2.2,0.4-3,1.2c-0.8,0.8-1.2,1.8-1.2,3s0.4,2.2,1.2,3.1c0.8,0.8,1.8,1.3,3,1.3h16.9
c1.8,0,3.4,0.3,4.9,1s2.9,1.5,4,2.7c1.2,1.1,2.1,2.5,2.7,4c0.7,1.5,1,3.2,1,4.9c0,1.8-0.3,3.4-1,4.9s-1.6,2.9-2.7,4
c-1.2,1.2-2.5,2.1-4,2.7c-1.5,0.7-3.2,1-4.9,1h-25.3v-8.4h25.3c1.3,0,2.3-0.4,3.1-1.2c0.8-0.8,1.2-1.8,1.2-3.1c0-1.2-0.4-2.2-1.2-3
s-1.8-1.2-3.1-1.2h-16.9c-1.8,0-3.4-0.3-4.9-1s-2.9-1.6-4-2.7c-1.1-1.2-2-2.5-2.7-4s-1-3.2-1-4.9c0-1.8,0.3-3.4,1-4.9
s1.5-2.9,2.7-4c1.1-1.1,2.5-2,4-2.7s3.2-1,4.9-1L154.3,197L154.3,197z"/>
<polygon class="st14" points="195.8,241 195.8,186.8 224.6,186.8 224.6,244.2 "/>
<polygon class="st13" points="200.4,236.9 200.4,191.4 220,191.4 220,239.1 "/>
<path d="M206,231.9v-34.8h8.4v35.7"/>
<path class="st14" d="M193.4,294.4c-4.3,0-8.4-0.8-12.2-2.5c-3.8-1.6-7.1-3.9-10-6.7c-2.9-2.9-5.1-6.2-6.7-10
c-1.6-3.8-2.5-7.9-2.5-12.2c0-4.3,0.8-8.5,2.5-12.3c1.6-3.7,3.9-7.1,6.8-9.9c2.8-2.8,6.2-5,9.9-6.6c3.8-1.6,7.9-2.5,12.2-2.5h2.5
v-16.9h28.7v48.1c0,4.2-0.8,8.3-2.5,12.2c-1.6,3.7-3.8,7.1-6.6,9.9c-2.8,2.9-6.2,5.2-10,6.8C201.7,293.6,197.6,294.4,193.4,294.4z
M193.4,260.6c-0.4,0-0.7,0.1-1,0.2s-0.6,0.3-0.8,0.5s-0.4,0.5-0.6,0.8c-0.1,0.2-0.2,0.4-0.2,0.9c0,0.4,0.1,0.7,0.2,1
c0.1,0.3,0.3,0.6,0.5,0.8c0.2,0.2,0.5,0.4,0.8,0.5s0.6,0.2,1,0.2c0.5,0,0.7-0.1,0.9-0.2c0.3-0.1,0.6-0.3,0.8-0.6
c0.2-0.2,0.4-0.5,0.5-0.8s0.2-0.6,0.2-1v-2.5h-2.3V260.6z"/>
<path class="st13" d="M193.4,289.9c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7c3.3-1.4,6.8-2.1,10.4-2.1h7
v-16.9h19.7V263c0,3.6-0.7,7.1-2.1,10.4c-1.4,3.2-3.3,6.1-5.7,8.5c-2.4,2.5-5.3,4.4-8.5,5.8C200.5,289.1,197,289.9,193.4,289.9z
M193.4,256.1c-1,0-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7s0.2,1.9,0.5,2.8
c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5c0.9,0.4,1.8,0.5,2.8,0.5s1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5
c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8v-7H193.4z"/>
<path d="M214.4,225.1V263c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.5,6.8c-1.9,1.9-4.1,3.5-6.7,4.6c-2.6,1.1-5.3,1.6-8.2,1.6
s-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2c0-2.9,0.5-5.7,1.6-8.2
c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5s5.3-1.6,8.2-1.6h12.7v-16.9 M193.4,275.8c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-12.7h-12.7c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7C189.9,275.5,191.6,275.8,193.4,275.8z"/>
<path class="st14" d="M31.4,294.6c-4.3,0-8.3-0.8-12.2-2.5c-3.8-1.6-7.1-3.9-10-6.7c-2.9-2.8-5.1-6.2-6.7-10
c-1.6-3.8-2.5-7.9-2.5-12.2v-48.1h28.8V232h2.6c4.3,0,8.4,0.8,12.3,2.5c3.7,1.6,7,3.8,9.8,6.7c2.8,2.8,5.1,6.1,6.7,9.8
c1.6,3.8,2.5,7.9,2.5,12.3c0,4.2-0.8,8.3-2.5,12.2c-1.6,3.7-3.8,7.1-6.6,9.9c-2.8,2.9-6.2,5.1-9.9,6.8
C39.8,293.7,35.7,294.6,31.4,294.6z M28.8,263.3c0,0.4,0.1,0.7,0.2,1c0.1,0.4,0.3,0.6,0.5,0.8c0.2,0.2,0.5,0.4,0.8,0.5
s0.6,0.2,1,0.2c0.5,0,0.7-0.1,0.9-0.2c0.3-0.1,0.6-0.3,0.8-0.6c0.2-0.2,0.4-0.5,0.5-0.8s0.2-0.6,0.2-1c0-0.5-0.1-0.7-0.2-0.9
c-0.1-0.3-0.3-0.6-0.6-0.8c-0.2-0.2-0.5-0.4-0.8-0.6c-0.2-0.1-0.4-0.2-0.9-0.2h-2.6v2.6H28.8z"/>
<path class="st13" d="M31.4,290c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4v-43.6h19.7v16.9h7.1c3.7,0,7.2,0.7,10.5,2.1c3.2,1.4,6,3.3,8.4,5.7c2.4,2.4,4.3,5.2,5.7,8.4
c1.4,3.2,2.1,6.8,2.1,10.5c0,3.6-0.7,7.1-2.1,10.4c-1.4,3.2-3.3,6.1-5.7,8.5c-2.4,2.5-5.3,4.4-8.5,5.8
C38.6,289.3,35.1,290,31.4,290z M24.3,263.3c0,1,0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5s1.8,0.5,2.8,0.5
s1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8s-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3
c-0.6-0.6-1.4-1.1-2.3-1.5c-0.8-0.4-1.7-0.5-2.7-0.5h-7.1C24.3,256.3,24.3,263.3,24.3,263.3z"/>
<path d="M39.6,243.9c2.5,1.1,4.8,2.6,6.7,4.5c1.9,1.9,3.4,4.1,4.5,6.7c1.1,2.5,1.6,5.3,1.6,8.2c0,2.9-0.5,5.6-1.6,8.2
s-2.6,4.8-4.5,6.8c-1.9,1.9-4.1,3.5-6.7,4.6c-2.5,1.1-5.3,1.6-8.2,1.6c-2.9,0-5.6-0.5-8.2-1.6s-4.8-2.6-6.8-4.6
c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2v-37.9h8.4v16.8h12.8C34.3,242.2,37,242.8,39.6,243.9z M31.4,276
c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5s-0.3-3.4-1-4.9s-1.6-2.9-2.7-4
c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1H18.6v12.7c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7
C27.9,275.7,29.6,276,31.4,276z"/>
<path class="st14" d="M75.9,294.6c-4.3,0-8.3-0.8-12.2-2.5c-3.8-1.6-7.1-3.9-10-6.8c-2.8-2.8-5-6.2-6.6-9.9
c-1.6-3.8-2.5-7.9-2.5-12.2c0-4.3,0.8-8.5,2.5-12.3c1.6-3.7,3.8-7,6.7-9.8c2.8-2.8,6.2-5.1,9.9-6.7c3.8-1.6,7.9-2.5,12.2-2.5
c4.3,0,8.4,0.8,12.3,2.5c3.7,1.6,7,3.8,9.8,6.6c2.9,2.8,5.2,6.2,6.8,9.9c1.6,3.8,2.5,7.9,2.5,12.3c0,4.2-0.8,8.3-2.5,12.2
c-1.6,3.8-3.9,7.1-6.7,10c-2.8,2.8-6.2,5.1-9.9,6.7C84.4,293.7,80.3,294.6,75.9,294.6z M75.9,260.8c-0.4,0-0.7,0.1-1,0.2
s-0.6,0.3-0.8,0.5c-0.2,0.2-0.4,0.5-0.6,0.8c-0.1,0.2-0.2,0.4-0.2,0.9c0,0.4,0.1,0.7,0.2,1c0.1,0.3,0.3,0.6,0.5,0.8
c0.2,0.2,0.5,0.4,0.8,0.5s0.6,0.2,1,0.2c0.4,0,0.7-0.1,1-0.2s0.6-0.3,0.8-0.5c0.2-0.2,0.4-0.5,0.5-0.8c0.1-0.3,0.2-0.6,0.2-1
c0-0.5-0.1-0.7-0.2-0.9c-0.1-0.3-0.3-0.6-0.6-0.8c-0.2-0.2-0.5-0.4-0.8-0.5C76.6,260.8,76.3,260.8,75.9,260.8z"/>
<path class="st13" d="M75.9,290c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.3-5.3-5.7-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.7-8.4c2.4-2.4,5.3-4.3,8.5-5.7c3.3-1.4,6.8-2.1,10.4-2.1
c3.7,0,7.2,0.7,10.5,2.1c3.2,1.4,6,3.3,8.4,5.7c2.5,2.4,4.4,5.3,5.8,8.5c1.4,3.2,2.1,6.8,2.1,10.5c0,3.6-0.7,7.1-2.1,10.4
c-1.4,3.2-3.3,6.1-5.8,8.5c-2.4,2.4-5.3,4.4-8.5,5.7C83.2,289.3,79.6,290,75.9,290z M75.9,256.3c-1,0-1.9,0.2-2.8,0.5
c-0.9,0.4-1.6,0.9-2.3,1.5c-0.6,0.6-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7s0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3
c0.6,0.6,1.4,1.1,2.3,1.5c0.9,0.4,1.8,0.5,2.8,0.5s1.9-0.2,2.8-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3
s0.5-1.8,0.5-2.8s-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3c-0.6-0.6-1.4-1.1-2.3-1.5S76.9,256.3,75.9,256.3z"/>
<path d="M54.9,263.3c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.5-6.7c1.9-1.9,4.1-3.4,6.7-4.5c2.6-1.1,5.3-1.6,8.2-1.6
s5.7,0.5,8.2,1.6s4.8,2.6,6.7,4.5s3.5,4.1,4.6,6.7c1.1,2.5,1.6,5.3,1.6,8.2c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.6,6.8
c-1.9,1.9-4.2,3.5-6.7,4.6s-5.3,1.6-8.2,1.6s-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.7-4.6c-1.9-1.9-3.4-4.2-4.5-6.8
C55.5,268.9,54.9,266.1,54.9,263.3z M63.2,263.3c0,1.8,0.3,3.4,1,5c0.7,1.6,1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7
c1.6,0.7,3.2,1,5,1s3.4-0.3,5-1c1.6-0.7,2.9-1.6,4.1-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5s-0.3-3.4-1-4.9s-1.6-2.9-2.7-4
c-1.2-1.2-2.5-2.1-4.1-2.7c-1.6-0.7-3.2-1-5-1s-3.4,0.3-5,1c-1.6,0.7-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
C63.5,259.9,63.2,261.5,63.2,263.3z"/>
<path class="st14" d="M120.9,294.4c-4.3,0-8.3-0.8-12.2-2.5c-3.8-1.6-7.1-3.9-10-6.7c-2.9-2.8-5.1-6.2-6.7-10
c-1.6-3.8-2.5-7.9-2.5-12.2c0-4.3,0.8-8.5,2.5-12.3c1.6-3.7,3.9-7.1,6.8-9.9c2.8-2.8,6.2-5,9.9-6.6c3.8-1.6,7.9-2.5,12.2-2.5
s8.5,0.8,12.3,2.5c3.7,1.6,7,3.8,9.8,6.7c2.8,2.8,5.1,6.1,6.7,9.8c1.6,3.8,2.5,7.9,2.5,12.3v31.3h-31.3V294.4z M120.9,260.6
c-0.4,0-0.7,0.1-1,0.2s-0.6,0.3-0.8,0.5s-0.4,0.5-0.6,0.8c-0.1,0.2-0.2,0.4-0.2,0.9c0,0.4,0.1,0.7,0.2,1c0.1,0.3,0.3,0.6,0.5,0.8
c0.2,0.2,0.5,0.4,0.8,0.5s0.6,0.2,1,0.2h2.5v-2.6c0-0.5-0.1-0.7-0.2-0.9c-0.1-0.3-0.3-0.6-0.6-0.8c-0.2-0.2-0.5-0.4-0.8-0.6
C121.6,260.7,121.4,260.6,120.9,260.6z"/>
<path class="st13" d="M120.9,289.9c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7c3.3-1.4,6.8-2.1,10.4-2.1
c3.7,0,7.2,0.7,10.5,2.1c3.2,1.4,6,3.3,8.4,5.7c2.4,2.4,4.3,5.2,5.7,8.4c1.4,3.2,2.1,6.8,2.1,10.5v26.8h-26.7V289.9z M120.9,256.1
c-1,0-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7s0.2,1.9,0.5,2.8
c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5c0.9,0.4,1.8,0.5,2.8,0.5h7v-7.1c0-1-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3
c-0.6-0.6-1.4-1.1-2.3-1.5C122.8,256.2,121.9,256.1,120.9,256.1z"/>
<path d="M120.9,242c2.9,0,5.7,0.5,8.2,1.6s4.8,2.6,6.7,4.5s3.4,4.1,4.5,6.7c1.1,2.5,1.6,5.3,1.6,8.2v21.2h-21
c-2.9,0-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2c0-2.9,0.5-5.7,1.6-8.2
c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5C115.3,242.6,118,242,120.9,242z M133.6,275.8V263c0-1.8-0.3-3.4-1-4.9
s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7c1.6,0.7,3.2,1,5,1L133.6,275.8L133.6,275.8z"/>
<g>
<path class="st14" d="M150.9,169.9h28.7v33.9c0.8-0.1,1.7-0.1,2.6-0.1c0.5,0,0.7-0.1,0.9-0.2c0.3-0.1,0.6-0.3,0.8-0.6
c0.2-0.2,0.4-0.5,0.5-0.8s0.2-0.6,0.2-1v-14.3h28.8v14.4c0,5.2-1.2,10-3.5,14.4c-0.5,0.9-1,1.7-1.5,2.5c0.5,0.8,1,1.7,1.5,2.6
c2.3,4.4,3.5,9.2,3.5,14.3v14.4h-28.8V235c0-0.5-0.1-0.7-0.2-0.9c-0.1-0.3-0.3-0.6-0.6-0.8c-0.2-0.2-0.5-0.4-0.8-0.6
c-0.2-0.1-0.4-0.2-0.9-0.2l0,0c-0.4,0-0.7,0.1-1,0.2s-0.6,0.3-0.8,0.5s-0.4,0.5-0.6,0.8c-0.1,0.2-0.2,0.4-0.2,0.9v16.7l-28.8,1.6
v-83.3H150.9z"/>
<path class="st13" d="M155.4,174.5h19.7v34.7c2.3-0.6,4.7-1,7.1-1c1,0,1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5
c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8v-9.8h19.7v9.8c0,4.4-1,8.5-3,12.3c-0.9,1.7-1.9,3.2-3.1,4.6c1.2,1.4,2.2,3,3.1,4.7
c2,3.8,2.9,7.9,2.9,12.2v9.9h-19.7v-9.9c0-1-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3c-0.6-0.6-1.4-1.1-2.3-1.5
c-0.8-0.4-1.7-0.5-2.7-0.5s-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.6,0.6-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7v12.4
l-19.7,1.1v-73.9H155.4z"/>
<path d="M161.1,242.5V235v-54.8h8.4v38.1c1.8-1.4,3.8-2.4,6-3.2c2.2-0.8,4.4-1.1,6.8-1.1c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-4.1h8.4v4.1c0,3.5-0.8,6.7-2.3,9.6s-3.6,5.3-6.2,7.3c2.6,1.9,4.7,4.4,6.2,7.3
s2.3,6.1,2.3,9.6v4.2h-8.4V235c0-1.8-0.3-3.4-1-4.9s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1
c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v7.1"/>
<path class="st14" d="M134.2,294.4v-31.3c0-4.3,0.8-8.5,2.5-12.3c1.6-3.7,3.9-7.1,6.8-9.9c2.8-2.8,6.2-5,9.9-6.6
c1.8-0.8,3.7-1.4,5.6-1.8l0.8-0.2l19.9-1.1v29.3h-14.2c-0.5,0-0.7,0.1-0.9,0.2c-0.3,0.1-0.6,0.3-0.8,0.6c-0.2,0.2-0.4,0.5-0.6,0.8
c-0.1,0.2-0.2,0.4-0.2,0.9v31.3h-28.8V294.4z"/>
<path class="st13" d="M138.7,289.9v-26.8c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7
c1.5-0.7,3.2-1.2,4.8-1.5l0.4-0.1l14.8-0.8v20h-9.6c-1,0-1.9,0.2-2.7,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.2,1.4-1.5,2.3
c-0.4,0.8-0.5,1.7-0.5,2.7v26.8h-19.8V289.9z"/>
<path d="M169.4,242v8.4h-3.9c-1.8,0-3.4,0.3-4.9,1s-2.9,1.6-4,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v21.2h-8.4v-21.1
c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5c1.2-0.5,2.5-0.9,3.8-1.2"/>
</g>
<path class="st14" d="M238.4,294.6c-4.2,0-8.2-0.8-12-2.4s-7.2-3.9-10.2-6.8c-3.1-3.1-5.4-6.6-6.9-10.5c-1.4-3.8-2.2-7.7-2.2-11.6
c0-4,0.7-7.9,2.2-11.6c1.5-3.9,3.8-7.4,6.9-10.4c3-3,6.6-5.4,10.4-6.9s7.7-2.3,11.7-2.3s7.9,0.8,11.7,2.3c3.9,1.5,7.4,3.9,10.4,6.9
l10.1,10.1l-12,12l12,12l-10.1,10.1c-2.9,2.9-6.4,5.2-10.2,6.8C246.6,293.8,242.6,294.6,238.4,294.6z"/>
<path class="st13" d="M238.4,290c-3.6,0-7-0.7-10.2-2s-6.2-3.3-8.7-5.8c-2.6-2.6-4.6-5.6-5.9-9c-1.2-3.2-1.9-6.6-1.9-10
s0.6-6.7,1.9-9.9c1.3-3.3,3.3-6.3,5.9-8.9s5.6-4.6,8.9-5.9s6.6-1.9,10-1.9s6.8,0.7,10,1.9c3.3,1.3,6.3,3.3,8.9,5.9l6.9,6.9l-12,12
l12,12l-6.9,6.9c-2.5,2.5-5.4,4.5-8.7,5.8C245.4,289.4,242,290,238.4,290z"/>
<path d="M250.3,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6c-2.8,0-5.5-0.5-8.1-1.6s-4.9-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9l-23.4,23.4c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L250.3,269.4z M229.4,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C233.7,251.1,231.4,252.2,229.4,254.2z"/>
<path class="st14" d="M248.8,294.4v-31.3c0-4.3,0.8-8.5,2.5-12.3c1.6-3.7,3.9-7.1,6.8-9.9c2.8-2.8,6.2-5,9.9-6.6
c3.8-1.6,7.9-2.5,12.2-2.5h14.4v28.8h-14.4c-0.5,0-0.7,0.1-0.9,0.2c-0.3,0.1-0.6,0.3-0.8,0.6c-0.2,0.2-0.4,0.5-0.6,0.8
c-0.1,0.2-0.2,0.4-0.2,0.9v31.3H248.8z"/>
<path class="st13" d="M253.4,289.9v-26.8c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7
c3.3-1.4,6.8-2.1,10.4-2.1h9.9V256h-9.9c-1,0-1.9,0.2-2.7,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.6,0.6-1.1,1.4-1.5,2.3
c-0.4,0.8-0.5,1.7-0.5,2.7v26.8h-19.8V289.9z"/>
<path d="M284.4,242v8.4h-4.2c-1.8,0-3.4,0.3-4.9,1s-2.9,1.6-4,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v21.2h-8.4v-21.1
c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5c2.6-1.1,5.3-1.6,8.2-1.6L284.4,242L284.4,242z"/>
<path class="st14" d="M341.7,294.5c-5.1,0-9.9-1.2-14.3-3.5c-0.9-0.5-1.8-1-2.6-1.5c-0.8,0.5-1.7,1-2.5,1.5
c-4.4,2.3-9.2,3.5-14.4,3.5c-4.3,0-8.3-0.8-12.2-2.5c-3.8-1.6-7.1-3.9-10-6.7c-2.9-2.9-5.1-6.2-6.7-10s-2.5-7.9-2.5-12.2v-31.2
h28.9v31.3c0,0.4,0.1,0.7,0.2,1s0.3,0.6,0.5,0.8c0.2,0.2,0.5,0.4,0.8,0.6c0.2,0.1,0.4,0.2,0.9,0.2c0.4,0,0.7-0.1,1-0.2
c0.4-0.1,0.6-0.3,0.8-0.5c0.2-0.2,0.4-0.5,0.5-0.8s0.2-0.6,0.2-1v-31.2h28.8v31.1c0,0.4,0.1,0.7,0.2,1c0.1,0.4,0.3,0.6,0.5,0.8
c0.2,0.2,0.5,0.4,0.8,0.6c0.2,0.1,0.4,0.2,0.9,0.2c0.4,0,0.7-0.1,1-0.2s0.6-0.3,0.8-0.5c0.2-0.2,0.4-0.5,0.5-0.8s0.2-0.6,0.2-1
v-31.2h28.8v31.1c0,4.2-0.8,8.3-2.5,12.2c-1.6,3.8-3.9,7.1-6.7,10c-2.8,2.8-6.2,5.1-9.9,6.7C350.2,293.6,346.1,294.5,341.7,294.5z"
/>
<path class="st13" d="M341.7,289.9c-4.3,0-8.5-1-12.2-2.9c-1.7-0.9-3.3-1.9-4.7-3.1c-1.4,1.2-3,2.2-4.6,3.1c-3.7,2-7.8,3-12.3,3
c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5c-1.4-3.3-2.1-6.8-2.1-10.4v-26.7h19.8v26.7
c0,1,0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5c0.8,0.4,1.7,0.5,2.7,0.5s1.9-0.2,2.8-0.5
c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8v-26.7h19.7v26.7c0,1,0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3
c0.6,0.6,1.4,1.1,2.3,1.5c0.8,0.4,1.7,0.5,2.7,0.5s1.9-0.2,2.8-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3
s0.5-1.8,0.5-2.8v-26.7h19.7v26.7c0,3.6-0.7,7.1-2.1,10.4c-1.4,3.2-3.3,6.1-5.8,8.5c-2.4,2.4-5.3,4.4-8.5,5.7
C349,289.2,345.4,289.9,341.7,289.9z"/>
<path d="M362.9,242.1v21c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.6,6.8c-1.9,1.9-4.2,3.5-6.7,4.6s-5.3,1.6-8.2,1.6
c-3.4,0-6.6-0.8-9.6-2.3c-2.9-1.5-5.4-3.6-7.3-6.2c-1.9,2.6-4.4,4.7-7.3,6.2s-6.1,2.3-9.6,2.3c-2.9,0-5.6-0.5-8.2-1.6
s-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2v-21h8.4v21c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1s2.5,2.1,4,2.7
c1.5,0.7,3.2,1,4.9,1c1.8,0,3.4-0.3,5-1s2.9-1.6,4.1-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-21h8.4v21c0,1.8,0.3,3.4,1,5
s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4,2.7c1.5,0.7,3.2,1,4.9,1c1.8,0,3.4-0.3,5-1s2.9-1.6,4.1-2.7s2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5
v-21H362.9z"/>
<path class="st14" d="M387,294.6c-4.2,0-8.2-0.8-12-2.4s-7.2-3.9-10.2-6.8c-3.1-3.1-5.4-6.6-6.9-10.5c-1.4-3.8-2.2-7.7-2.2-11.6
c0-4,0.7-7.9,2.2-11.6c1.5-3.9,3.8-7.4,6.9-10.4c3-3,6.6-5.4,10.4-6.9s7.7-2.3,11.7-2.3s7.9,0.8,11.7,2.3c3.9,1.5,7.4,3.9,10.4,6.9
l10.1,10.1l-12,12l12,12L409,285.5c-2.9,2.9-6.4,5.2-10.2,6.8C395.2,293.8,391.1,294.6,387,294.6z"/>
<path class="st13" d="M387,290c-3.6,0-7-0.7-10.2-2s-6.2-3.3-8.7-5.8c-2.6-2.6-4.6-5.6-5.9-9c-1.2-3.2-1.9-6.6-1.9-10
s0.6-6.7,1.9-9.9c1.3-3.3,3.3-6.3,5.9-8.9s5.6-4.6,8.9-5.9s6.6-1.9,10-1.9s6.8,0.7,10,1.9c3.3,1.3,6.3,3.3,8.9,5.9l6.9,6.9l-12,12
l12,12l-6.9,6.9c-2.5,2.5-5.4,4.5-8.7,5.8C394,289.4,390.5,290,387,290z"/>
<path d="M398.9,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6s-5.5-0.5-8.1-1.6s-4.8-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9l-23.4,23.4c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L398.9,269.4z M378,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C382.3,251.1,380,252.2,378,254.2z"/>
<path class="st14" d="M428.8,294.6c-4.2,0-8.2-0.8-12-2.4s-7.2-3.9-10.2-6.8c-3.1-3.1-5.4-6.6-6.9-10.5c-1.4-3.8-2.2-7.7-2.2-11.6
c0-4,0.7-7.9,2.2-11.6c1.5-3.9,3.8-7.4,6.9-10.4c3-3,6.6-5.4,10.4-6.9s7.7-2.3,11.7-2.3s7.9,0.8,11.7,2.3c3.9,1.5,7.4,3.9,10.4,6.9
l10.1,10.1l-12,12l12,12l-10.1,10.1c-2.9,2.9-6.4,5.2-10.2,6.8C436.9,293.8,432.9,294.6,428.8,294.6z"/>
<path class="st13" d="M428.8,290c-3.6,0-7-0.7-10.2-2s-6.2-3.3-8.7-5.8c-2.6-2.6-4.6-5.6-5.9-9c-1.2-3.2-1.9-6.6-1.9-10
s0.6-6.7,1.9-9.9c1.3-3.3,3.3-6.3,5.9-8.9s5.6-4.6,8.9-5.9s6.6-1.9,10-1.9s6.8,0.7,10,1.9c3.3,1.3,6.3,3.3,8.9,5.9l6.9,6.9l-12,12
l12,12l-6.9,6.9c-2.5,2.5-5.4,4.5-8.7,5.8C435.7,289.4,432.3,290,428.8,290z"/>
<path d="M440.7,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6c-2.8,0-5.5-0.5-8.1-1.6s-4.8-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9L423,274.8c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L440.7,269.4z M419.7,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C424.1,251.1,421.7,252.2,419.7,254.2z"/>
<path class="st14" d="M473,294.4V280c0-0.5-0.1-0.7-0.2-0.9c-0.1-0.3-0.3-0.6-0.6-0.8c-0.2-0.2-0.5-0.4-0.8-0.6
c-0.2-0.1-0.4-0.2-0.9-0.2c-0.4,0-0.7,0.1-1,0.2s-0.6,0.3-0.8,0.5s-0.4,0.5-0.6,0.8c-0.1,0.2-0.2,0.4-0.2,0.9v14.4h-28.8v-79.4
h28.8v33.9c0.8-0.1,1.7-0.1,2.6-0.1c0.5,0,0.7-0.1,0.9-0.2c0.3-0.1,0.6-0.3,0.8-0.6c0.2-0.2,0.4-0.5,0.5-0.8s0.2-0.6,0.2-1v-14.3
h28.8v14.4c0,5.2-1.2,10-3.5,14.4c-0.5,0.9-1,1.7-1.5,2.5c0.5,0.8,1,1.7,1.5,2.6c2.3,4.4,3.5,9.2,3.5,14.3v14.4L473,294.4
L473,294.4z"/>
<path class="st13" d="M477.5,289.9V280c0-1-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3c-0.6-0.6-1.4-1.1-2.3-1.5
c-0.8-0.4-1.7-0.5-2.7-0.5s-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7v9.9
h-19.7v-70.4h19.7v34.7c2.3-0.6,4.7-1,7.1-1c1,0,1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8
v-9.8h19.7v9.8c0,4.4-1,8.5-3,12.3c-0.9,1.7-1.9,3.2-3.1,4.6c1.2,1.4,2.2,3,3.1,4.7c2,3.7,2.9,7.9,2.9,12.2v9.9L477.5,289.9
L477.5,289.9z"/>
<path d="M489.2,255.8c-1.5,2.9-3.6,5.3-6.2,7.3c2.6,1.9,4.7,4.4,6.2,7.3s2.3,6.1,2.3,9.6v4.2h-8.4V280c0-1.8-0.3-3.4-1-4.9
s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1c-1.8,0-3.4,0.3-5,1c-1.6,0.7-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9v4.2h-8.4V280v-54.8h8.4v38.1c1.8-1.4,3.8-2.4,6-3.2s4.4-1.1,6.8-1.1c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-4.1h8.4v4.1C491.5,249.7,490.8,252.9,489.2,255.8z"/>
</g>
<g id="Fond_texte_1_">
<path class="st13" d="M114.8,244.9v-19.7h4.4c-1.2-0.7-2.3-1.6-3.2-2.6c-1.6-1.6-2.9-3.6-3.8-5.8c-1-2.2-1.4-4.6-1.4-7.2
c0-2.5,0.5-4.9,1.4-7.2c0.9-2.2,2.2-4.1,3.9-5.8c1.6-1.6,3.6-2.9,5.8-3.9s4.6-1.4,7.2-1.4h31.1V211h-4.5c1.1,0.7,2.2,1.5,3.2,2.5
c1.7,1.6,3,3.6,4,5.8c1,2.2,1.4,4.6,1.4,7.2c0,2.5-0.5,4.9-1.4,7.2c-0.9,2.2-2.3,4.1-3.9,5.8c-1.7,1.7-3.6,3-5.8,3.9
c-2.2,1-4.6,1.4-7.2,1.4h-31.2V244.9z"/>
<path d="M154.3,197v8.4h-25.4c-1.2,0-2.2,0.4-3,1.2c-0.8,0.8-1.2,1.8-1.2,3s0.4,2.2,1.2,3.1c0.8,0.8,1.8,1.3,3,1.3h16.9
c1.8,0,3.4,0.3,4.9,1s2.9,1.5,4,2.7c1.2,1.1,2.1,2.5,2.7,4c0.7,1.5,1,3.2,1,4.9c0,1.8-0.3,3.4-1,4.9s-1.6,2.9-2.7,4
c-1.2,1.2-2.5,2.1-4,2.7c-1.5,0.7-3.2,1-4.9,1h-25.3v-8.4h25.3c1.3,0,2.3-0.4,3.1-1.2c0.8-0.8,1.2-1.8,1.2-3.1c0-1.2-0.4-2.2-1.2-3
s-1.8-1.2-3.1-1.2h-16.9c-1.8,0-3.4-0.3-4.9-1s-2.9-1.6-4-2.7c-1.1-1.2-2-2.5-2.7-4s-1-3.2-1-4.9c0-1.8,0.3-3.4,1-4.9
s1.5-2.9,2.7-4c1.1-1.1,2.5-2,4-2.7s3.2-1,4.9-1L154.3,197L154.3,197z"/>
<polygon class="st13" points="200.4,236.9 200.4,191.4 220,191.4 220,239.1 "/>
<path d="M206,231.9v-34.8h8.4v35.7"/>
<path class="st13" d="M193.4,289.9c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7c3.3-1.4,6.8-2.1,10.4-2.1h7
v-16.9h19.7V263c0,3.6-0.7,7.1-2.1,10.4c-1.4,3.2-3.3,6.1-5.7,8.5c-2.4,2.5-5.3,4.4-8.5,5.8C200.5,289.1,197,289.9,193.4,289.9z
M193.4,256.1c-1,0-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7s0.2,1.9,0.5,2.8
c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5c0.9,0.4,1.8,0.5,2.8,0.5s1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5
c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8v-7H193.4z"/>
<path d="M214.4,225.1V263c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.5,6.8c-1.9,1.9-4.1,3.5-6.7,4.6c-2.6,1.1-5.3,1.6-8.2,1.6
s-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2c0-2.9,0.5-5.7,1.6-8.2
c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5s5.3-1.6,8.2-1.6h12.7v-16.9 M193.4,275.8c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-12.7h-12.7c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7C189.9,275.5,191.6,275.8,193.4,275.8z"/>
<path class="st13" d="M31.4,290c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4v-43.6h19.7v16.9h7.1c3.7,0,7.2,0.7,10.5,2.1c3.2,1.4,6,3.3,8.4,5.7c2.4,2.4,4.3,5.2,5.7,8.4
c1.4,3.2,2.1,6.8,2.1,10.5c0,3.6-0.7,7.1-2.1,10.4c-1.4,3.2-3.3,6.1-5.7,8.5c-2.4,2.5-5.3,4.4-8.5,5.8
C38.6,289.3,35.1,290,31.4,290z M24.3,263.3c0,1,0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5s1.8,0.5,2.8,0.5
s1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8s-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3
c-0.6-0.6-1.4-1.1-2.3-1.5c-0.8-0.4-1.7-0.5-2.7-0.5h-7.1C24.3,256.3,24.3,263.3,24.3,263.3z"/>
<path d="M39.6,243.9c2.5,1.1,4.8,2.6,6.7,4.5c1.9,1.9,3.4,4.1,4.5,6.7c1.1,2.5,1.6,5.3,1.6,8.2c0,2.9-0.5,5.6-1.6,8.2
s-2.6,4.8-4.5,6.8c-1.9,1.9-4.1,3.5-6.7,4.6c-2.5,1.1-5.3,1.6-8.2,1.6c-2.9,0-5.6-0.5-8.2-1.6s-4.8-2.6-6.8-4.6
c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2v-37.9h8.4v16.8h12.8C34.3,242.2,37,242.8,39.6,243.9z M31.4,276
c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5s-0.3-3.4-1-4.9s-1.6-2.9-2.7-4
c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1H18.6v12.7c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7
C27.9,275.7,29.6,276,31.4,276z"/>
<path class="st13" d="M75.9,290c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.3-5.3-5.7-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.7-8.4c2.4-2.4,5.3-4.3,8.5-5.7c3.3-1.4,6.8-2.1,10.4-2.1
c3.7,0,7.2,0.7,10.5,2.1c3.2,1.4,6,3.3,8.4,5.7c2.5,2.4,4.4,5.3,5.8,8.5c1.4,3.2,2.1,6.8,2.1,10.5c0,3.6-0.7,7.1-2.1,10.4
c-1.4,3.2-3.3,6.1-5.8,8.5c-2.4,2.4-5.3,4.4-8.5,5.7C83.2,289.3,79.6,290,75.9,290z M75.9,256.3c-1,0-1.9,0.2-2.8,0.5
c-0.9,0.4-1.6,0.9-2.3,1.5c-0.6,0.6-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7s0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3
c0.6,0.6,1.4,1.1,2.3,1.5c0.9,0.4,1.8,0.5,2.8,0.5s1.9-0.2,2.8-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3
s0.5-1.8,0.5-2.8s-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3c-0.6-0.6-1.4-1.1-2.3-1.5S76.9,256.3,75.9,256.3z"/>
<path d="M54.9,263.3c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.5-6.7c1.9-1.9,4.1-3.4,6.7-4.5c2.6-1.1,5.3-1.6,8.2-1.6
s5.7,0.5,8.2,1.6s4.8,2.6,6.7,4.5s3.5,4.1,4.6,6.7c1.1,2.5,1.6,5.3,1.6,8.2c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.6,6.8
c-1.9,1.9-4.2,3.5-6.7,4.6s-5.3,1.6-8.2,1.6s-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.7-4.6c-1.9-1.9-3.4-4.2-4.5-6.8
C55.5,268.9,54.9,266.1,54.9,263.3z M63.2,263.3c0,1.8,0.3,3.4,1,5c0.7,1.6,1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7
c1.6,0.7,3.2,1,5,1s3.4-0.3,5-1c1.6-0.7,2.9-1.6,4.1-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5s-0.3-3.4-1-4.9s-1.6-2.9-2.7-4
c-1.2-1.2-2.5-2.1-4.1-2.7c-1.6-0.7-3.2-1-5-1s-3.4,0.3-5,1c-1.6,0.7-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
C63.5,259.9,63.2,261.5,63.2,263.3z"/>
<path class="st13" d="M120.9,289.9c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5
c-1.4-3.3-2.1-6.8-2.1-10.4c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7c3.3-1.4,6.8-2.1,10.4-2.1
c3.7,0,7.2,0.7,10.5,2.1c3.2,1.4,6,3.3,8.4,5.7c2.4,2.4,4.3,5.2,5.7,8.4c1.4,3.2,2.1,6.8,2.1,10.5v26.8h-26.7V289.9z M120.9,256.1
c-1,0-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7s0.2,1.9,0.5,2.8
c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5c0.9,0.4,1.8,0.5,2.8,0.5h7v-7.1c0-1-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3
c-0.6-0.6-1.4-1.1-2.3-1.5C122.8,256.2,121.9,256.1,120.9,256.1z"/>
<path d="M120.9,242c2.9,0,5.7,0.5,8.2,1.6s4.8,2.6,6.7,4.5s3.4,4.1,4.5,6.7c1.1,2.5,1.6,5.3,1.6,8.2v21.2h-21
c-2.9,0-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2c0-2.9,0.5-5.7,1.6-8.2
c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5C115.3,242.6,118,242,120.9,242z M133.6,275.8V263c0-1.8-0.3-3.4-1-4.9
s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7c1.6,0.7,3.2,1,5,1L133.6,275.8L133.6,275.8z"/>
<g>
<path class="st13" d="M155.4,174.5h19.7v34.7c2.3-0.6,4.7-1,7.1-1c1,0,1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5
c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8v-9.8h19.7v9.8c0,4.4-1,8.5-3,12.3c-0.9,1.7-1.9,3.2-3.1,4.6c1.2,1.4,2.2,3,3.1,4.7
c2,3.8,2.9,7.9,2.9,12.2v9.9h-19.7v-9.9c0-1-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3c-0.6-0.6-1.4-1.1-2.3-1.5
c-0.8-0.4-1.7-0.5-2.7-0.5s-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.6,0.6-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7v12.4
l-19.7,1.1v-73.9H155.4z"/>
<path d="M161.1,242.5V235v-54.8h8.4v38.1c1.8-1.4,3.8-2.4,6-3.2c2.2-0.8,4.4-1.1,6.8-1.1c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-4.1h8.4v4.1c0,3.5-0.8,6.7-2.3,9.6s-3.6,5.3-6.2,7.3c2.6,1.9,4.7,4.4,6.2,7.3
s2.3,6.1,2.3,9.6v4.2h-8.4V235c0-1.8-0.3-3.4-1-4.9s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1
c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v7.1"/>
<path class="st13" d="M138.7,289.9v-26.8c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7
c1.5-0.7,3.2-1.2,4.8-1.5l0.4-0.1l14.8-0.8v20h-9.6c-1,0-1.9,0.2-2.7,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.2,1.4-1.5,2.3
c-0.4,0.8-0.5,1.7-0.5,2.7v26.8h-19.8V289.9z"/>
<path d="M169.4,242v8.4h-3.9c-1.8,0-3.4,0.3-4.9,1s-2.9,1.6-4,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v21.2h-8.4v-21.1
c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5c1.2-0.5,2.5-0.9,3.8-1.2"/>
</g>
<path class="st13" d="M238.4,290c-3.6,0-7-0.7-10.2-2s-6.2-3.3-8.7-5.8c-2.6-2.6-4.6-5.6-5.9-9c-1.2-3.2-1.9-6.6-1.9-10
s0.6-6.7,1.9-9.9c1.3-3.3,3.3-6.3,5.9-8.9s5.6-4.6,8.9-5.9s6.6-1.9,10-1.9s6.8,0.7,10,1.9c3.3,1.3,6.3,3.3,8.9,5.9l6.9,6.9l-12,12
l12,12l-6.9,6.9c-2.5,2.5-5.4,4.5-8.7,5.8C245.4,289.4,242,290,238.4,290z"/>
<path d="M250.3,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6c-2.8,0-5.5-0.5-8.1-1.6s-4.9-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9l-23.4,23.4c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L250.3,269.4z M229.4,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C233.7,251.1,231.4,252.2,229.4,254.2z"/>
<path class="st13" d="M253.4,289.9v-26.8c0-3.7,0.7-7.2,2.1-10.5c1.4-3.2,3.3-6,5.8-8.5c2.4-2.4,5.3-4.3,8.5-5.7
c3.3-1.4,6.8-2.1,10.4-2.1h9.9V256h-9.9c-1,0-1.9,0.2-2.7,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.6,0.6-1.1,1.4-1.5,2.3
c-0.4,0.8-0.5,1.7-0.5,2.7v26.8h-19.8V289.9z"/>
<path d="M284.4,242v8.4h-4.2c-1.8,0-3.4,0.3-4.9,1s-2.9,1.6-4,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v21.2h-8.4v-21.1
c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5c2.6-1.1,5.3-1.6,8.2-1.6L284.4,242L284.4,242z"/>
<path class="st13" d="M341.7,289.9c-4.3,0-8.5-1-12.2-2.9c-1.7-0.9-3.3-1.9-4.7-3.1c-1.4,1.2-3,2.2-4.6,3.1c-3.7,2-7.8,3-12.3,3
c-3.6,0-7.1-0.7-10.4-2.1c-3.2-1.4-6.1-3.3-8.5-5.8c-2.4-2.4-4.4-5.3-5.8-8.5c-1.4-3.3-2.1-6.8-2.1-10.4v-26.7h19.8v26.7
c0,1,0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3c0.6,0.6,1.4,1.1,2.3,1.5c0.8,0.4,1.7,0.5,2.7,0.5s1.9-0.2,2.8-0.5
c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8v-26.7h19.7v26.7c0,1,0.2,1.9,0.5,2.8c0.4,0.9,0.9,1.6,1.5,2.3
c0.6,0.6,1.4,1.1,2.3,1.5c0.8,0.4,1.7,0.5,2.7,0.5s1.9-0.2,2.8-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3
s0.5-1.8,0.5-2.8v-26.7h19.7v26.7c0,3.6-0.7,7.1-2.1,10.4c-1.4,3.2-3.3,6.1-5.8,8.5c-2.4,2.4-5.3,4.4-8.5,5.7
C349,289.2,345.4,289.9,341.7,289.9z"/>
<path d="M362.9,242.1v21c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.6,6.8c-1.9,1.9-4.2,3.5-6.7,4.6s-5.3,1.6-8.2,1.6
c-3.4,0-6.6-0.8-9.6-2.3c-2.9-1.5-5.4-3.6-7.3-6.2c-1.9,2.6-4.4,4.7-7.3,6.2s-6.1,2.3-9.6,2.3c-2.9,0-5.6-0.5-8.2-1.6
s-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2v-21h8.4v21c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1s2.5,2.1,4,2.7
c1.5,0.7,3.2,1,4.9,1c1.8,0,3.4-0.3,5-1s2.9-1.6,4.1-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-21h8.4v21c0,1.8,0.3,3.4,1,5
s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4,2.7c1.5,0.7,3.2,1,4.9,1c1.8,0,3.4-0.3,5-1s2.9-1.6,4.1-2.7s2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5
v-21H362.9z"/>
<path class="st13" d="M387,290c-3.6,0-7-0.7-10.2-2s-6.2-3.3-8.7-5.8c-2.6-2.6-4.6-5.6-5.9-9c-1.2-3.2-1.9-6.6-1.9-10
s0.6-6.7,1.9-9.9c1.3-3.3,3.3-6.3,5.9-8.9s5.6-4.6,8.9-5.9s6.6-1.9,10-1.9s6.8,0.7,10,1.9c3.3,1.3,6.3,3.3,8.9,5.9l6.9,6.9l-12,12
l12,12l-6.9,6.9c-2.5,2.5-5.4,4.5-8.7,5.8C394,289.4,390.5,290,387,290z"/>
<path d="M398.9,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6s-5.5-0.5-8.1-1.6s-4.8-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9l-23.4,23.4c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L398.9,269.4z M378,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C382.3,251.1,380,252.2,378,254.2z"/>
<path class="st13" d="M428.8,290c-3.6,0-7-0.7-10.2-2s-6.2-3.3-8.7-5.8c-2.6-2.6-4.6-5.6-5.9-9c-1.2-3.2-1.9-6.6-1.9-10
s0.6-6.7,1.9-9.9c1.3-3.3,3.3-6.3,5.9-8.9s5.6-4.6,8.9-5.9s6.6-1.9,10-1.9s6.8,0.7,10,1.9c3.3,1.3,6.3,3.3,8.9,5.9l6.9,6.9l-12,12
l12,12l-6.9,6.9c-2.5,2.5-5.4,4.5-8.7,5.8C435.7,289.4,432.3,290,428.8,290z"/>
<path d="M440.7,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6c-2.8,0-5.5-0.5-8.1-1.6s-4.8-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9L423,274.8c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L440.7,269.4z M419.7,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C424.1,251.1,421.7,252.2,419.7,254.2z"/>
<path class="st13" d="M477.5,289.9V280c0-1-0.2-1.9-0.5-2.7c-0.4-0.9-0.9-1.6-1.5-2.3c-0.6-0.6-1.4-1.1-2.3-1.5
c-0.8-0.4-1.7-0.5-2.7-0.5s-1.9,0.2-2.8,0.5c-0.9,0.4-1.6,0.9-2.3,1.5c-0.7,0.7-1.1,1.4-1.5,2.3c-0.4,0.8-0.5,1.7-0.5,2.7v9.9
h-19.7v-70.4h19.7v34.7c2.3-0.6,4.7-1,7.1-1c1,0,1.9-0.2,2.7-0.5c0.9-0.4,1.6-0.9,2.3-1.5c0.6-0.6,1.1-1.4,1.5-2.3s0.5-1.8,0.5-2.8
v-9.8h19.7v9.8c0,4.4-1,8.5-3,12.3c-0.9,1.7-1.9,3.2-3.1,4.6c1.2,1.4,2.2,3,3.1,4.7c2,3.7,2.9,7.9,2.9,12.2v9.9L477.5,289.9
L477.5,289.9z"/>
<path d="M489.2,255.8c-1.5,2.9-3.6,5.3-6.2,7.3c2.6,1.9,4.7,4.4,6.2,7.3s2.3,6.1,2.3,9.6v4.2h-8.4V280c0-1.8-0.3-3.4-1-4.9
s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1c-1.8,0-3.4,0.3-5,1c-1.6,0.7-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9v4.2h-8.4V280v-54.8h8.4v38.1c1.8-1.4,3.8-2.4,6-3.2s4.4-1.1,6.8-1.1c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-4.1h8.4v4.1C491.5,249.7,490.8,252.9,489.2,255.8z"/>
</g>
<g id="Texte">
<path d="M154.3,197v8.4h-25.4c-1.2,0-2.2,0.4-3,1.2c-0.8,0.8-1.2,1.8-1.2,3s0.4,2.2,1.2,3.1c0.8,0.8,1.8,1.3,3,1.3h16.9
c1.8,0,3.4,0.3,4.9,1s2.9,1.5,4,2.7c1.2,1.1,2.1,2.5,2.7,4c0.7,1.5,1,3.2,1,4.9c0,1.8-0.3,3.4-1,4.9s-1.6,2.9-2.7,4
c-1.2,1.2-2.5,2.1-4,2.7c-1.5,0.7-3.2,1-4.9,1h-25.3v-8.4h25.3c1.3,0,2.3-0.4,3.1-1.2c0.8-0.8,1.2-1.8,1.2-3.1c0-1.2-0.4-2.2-1.2-3
s-1.8-1.2-3.1-1.2h-16.9c-1.8,0-3.4-0.3-4.9-1s-2.9-1.6-4-2.7c-1.1-1.2-2-2.5-2.7-4s-1-3.2-1-4.9c0-1.8,0.3-3.4,1-4.9
s1.5-2.9,2.7-4c1.1-1.1,2.5-2,4-2.7s3.2-1,4.9-1L154.3,197L154.3,197z"/>
<path d="M206,231.9v-34.8h8.4v35.7"/>
<path d="M214.4,225.1V263c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.5,6.8c-1.9,1.9-4.1,3.5-6.7,4.6c-2.6,1.1-5.3,1.6-8.2,1.6
s-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2c0-2.9,0.5-5.7,1.6-8.2
c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5s5.3-1.6,8.2-1.6h12.7v-16.9 M193.4,275.8c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-12.7h-12.7c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7C189.9,275.5,191.6,275.8,193.4,275.8z"/>
<path d="M39.6,243.9c2.5,1.1,4.8,2.6,6.7,4.5c1.9,1.9,3.4,4.1,4.5,6.7c1.1,2.5,1.6,5.3,1.6,8.2c0,2.9-0.5,5.6-1.6,8.2
s-2.6,4.8-4.5,6.8c-1.9,1.9-4.1,3.5-6.7,4.6c-2.5,1.1-5.3,1.6-8.2,1.6c-2.9,0-5.6-0.5-8.2-1.6s-4.8-2.6-6.8-4.6
c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2v-37.9h8.4v16.8h12.8C34.3,242.2,37,242.8,39.6,243.9z M31.4,276
c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5s-0.3-3.4-1-4.9s-1.6-2.9-2.7-4
c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1H18.6v12.7c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7
C27.9,275.7,29.6,276,31.4,276z"/>
<path d="M54.9,263.3c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.5-6.7c1.9-1.9,4.1-3.4,6.7-4.5c2.6-1.1,5.3-1.6,8.2-1.6
s5.7,0.5,8.2,1.6s4.8,2.6,6.7,4.5s3.5,4.1,4.6,6.7c1.1,2.5,1.6,5.3,1.6,8.2c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.6,6.8
c-1.9,1.9-4.2,3.5-6.7,4.6s-5.3,1.6-8.2,1.6s-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.7-4.6c-1.9-1.9-3.4-4.2-4.5-6.8
C55.5,268.9,54.9,266.1,54.9,263.3z M63.2,263.3c0,1.8,0.3,3.4,1,5c0.7,1.6,1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7
c1.6,0.7,3.2,1,5,1s3.4-0.3,5-1c1.6-0.7,2.9-1.6,4.1-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5s-0.3-3.4-1-4.9s-1.6-2.9-2.7-4
c-1.2-1.2-2.5-2.1-4.1-2.7c-1.6-0.7-3.2-1-5-1s-3.4,0.3-5,1c-1.6,0.7-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
C63.5,259.9,63.2,261.5,63.2,263.3z"/>
<path d="M120.9,242c2.9,0,5.7,0.5,8.2,1.6s4.8,2.6,6.7,4.5s3.4,4.1,4.5,6.7c1.1,2.5,1.6,5.3,1.6,8.2v21.2h-21
c-2.9,0-5.6-0.5-8.2-1.6c-2.6-1.1-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2c0-2.9,0.5-5.7,1.6-8.2
c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5C115.3,242.6,118,242,120.9,242z M133.6,275.8V263c0-1.8-0.3-3.4-1-4.9
s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4.1,2.7c1.6,0.7,3.2,1,5,1L133.6,275.8L133.6,275.8z"/>
<g>
<path d="M161.1,242.5V235v-54.8h8.4v38.1c1.8-1.4,3.8-2.4,6-3.2c2.2-0.8,4.4-1.1,6.8-1.1c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-4.1h8.4v4.1c0,3.5-0.8,6.7-2.3,9.6s-3.6,5.3-6.2,7.3c2.6,1.9,4.7,4.4,6.2,7.3
s2.3,6.1,2.3,9.6v4.2h-8.4V235c0-1.8-0.3-3.4-1-4.9s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1
c-1.8,0-3.4,0.3-5,1s-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v7.1"/>
<path d="M169.4,242v8.4h-3.9c-1.8,0-3.4,0.3-4.9,1s-2.9,1.6-4,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v21.2h-8.4v-21.1
c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5c1.2-0.5,2.5-0.9,3.8-1.2"/>
</g>
<path d="M250.3,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6c-2.8,0-5.5-0.5-8.1-1.6s-4.9-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9l-23.4,23.4c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L250.3,269.4z M229.4,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C233.7,251.1,231.4,252.2,229.4,254.2z"/>
<path d="M284.4,242v8.4h-4.2c-1.8,0-3.4,0.3-4.9,1s-2.9,1.6-4,2.7c-1.2,1.2-2.1,2.5-2.7,4c-0.7,1.5-1,3.2-1,4.9v21.2h-8.4v-21.1
c0-2.9,0.5-5.7,1.6-8.2c1.1-2.5,2.6-4.8,4.6-6.7c1.9-1.9,4.2-3.4,6.8-4.5c2.6-1.1,5.3-1.6,8.2-1.6L284.4,242L284.4,242z"/>
<path d="M362.9,242.1v21c0,2.9-0.5,5.6-1.6,8.2s-2.6,4.8-4.6,6.8c-1.9,1.9-4.2,3.5-6.7,4.6s-5.3,1.6-8.2,1.6
c-3.4,0-6.6-0.8-9.6-2.3c-2.9-1.5-5.4-3.6-7.3-6.2c-1.9,2.6-4.4,4.7-7.3,6.2s-6.1,2.3-9.6,2.3c-2.9,0-5.6-0.5-8.2-1.6
s-4.8-2.6-6.8-4.6c-1.9-1.9-3.5-4.2-4.6-6.8c-1.1-2.6-1.6-5.3-1.6-8.2v-21h8.4v21c0,1.8,0.3,3.4,1,5s1.6,2.9,2.7,4.1s2.5,2.1,4,2.7
c1.5,0.7,3.2,1,4.9,1c1.8,0,3.4-0.3,5-1s2.9-1.6,4.1-2.7c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-21h8.4v21c0,1.8,0.3,3.4,1,5
s1.6,2.9,2.7,4.1c1.2,1.2,2.5,2.1,4,2.7c1.5,0.7,3.2,1,4.9,1c1.8,0,3.4-0.3,5-1s2.9-1.6,4.1-2.7s2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5
v-21H362.9z"/>
<path d="M398.9,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6s-5.5-0.5-8.1-1.6s-4.8-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9l-23.4,23.4c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L398.9,269.4z M378,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C382.3,251.1,380,252.2,378,254.2z"/>
<path d="M440.7,269.4l5.9,5.9l-2.9,2.9c-2,2-4.3,3.5-6.9,4.6s-5.3,1.6-8.1,1.6c-2.8,0-5.5-0.5-8.1-1.6s-4.8-2.6-6.9-4.6
c-2.1-2.1-3.6-4.4-4.6-7s-1.5-5.2-1.5-7.9c0-2.7,0.5-5.3,1.5-7.9c1-2.6,2.5-4.9,4.6-6.9c2.1-2.1,4.4-3.6,7-4.6s5.2-1.5,7.9-1.5
c2.7,0,5.3,0.5,7.9,1.5s4.9,2.6,7,4.6l2.9,2.9L423,274.8c2.4,1.2,4.9,1.6,7.6,1.2s5-1.6,7-3.6L440.7,269.4z M419.7,254.2
c-2.5,2.5-3.8,5.5-3.8,9c0,1.9,0.4,3.8,1.3,5.5c0.8-0.8,1.9-1.9,3.4-3.4s3.2-3.2,4.9-4.9c1.7-1.7,3.4-3.4,5-4.9
c1.6-1.6,2.8-2.8,3.7-3.7c-2.4-1.2-4.9-1.6-7.6-1.2C424.1,251.1,421.7,252.2,419.7,254.2z"/>
<path d="M489.2,255.8c-1.5,2.9-3.6,5.3-6.2,7.3c2.6,1.9,4.7,4.4,6.2,7.3s2.3,6.1,2.3,9.6v4.2h-8.4V280c0-1.8-0.3-3.4-1-4.9
s-1.6-2.9-2.7-4c-1.2-1.2-2.5-2.1-4-2.7c-1.5-0.7-3.2-1-4.9-1c-1.8,0-3.4,0.3-5,1c-1.6,0.7-2.9,1.6-4.1,2.7c-1.2,1.2-2.1,2.5-2.7,4
c-0.7,1.5-1,3.2-1,4.9v4.2h-8.4V280v-54.8h8.4v38.1c1.8-1.4,3.8-2.4,6-3.2s4.4-1.1,6.8-1.1c1.8,0,3.4-0.3,4.9-1s2.9-1.6,4-2.7
c1.2-1.2,2.1-2.5,2.7-4.1c0.7-1.6,1-3.2,1-5v-4.1h8.4v4.1C491.5,249.7,490.8,252.9,489.2,255.8z"/>
</g>
<g id="Ebene_7">
<g>
<g>
<path class="st15" d="M497.8,327.5c-1.5,0-2.8-0.2-3.7-1.4l1-0.9l0.5-0.5c0,0,0.3-0.2,0.5,0s0.4,0.3,0.6,0.4
c0.5,0.2,1.1,0.3,1.6,0.3c0.8,0,1.7-0.3,1.9-1c0.1-0.5-0.1-0.8-0.8-0.9l-1.5-0.1c-1.7-0.2-2.6-0.9-2.2-2.7c0.4-2,2.1-2.9,4-3
c1.3,0,2.7,0.5,3,1.2c0.5,0.9-1.3,1.5-1.3,1.5c-0.1-0.1-0.2-0.3-0.3-0.4c-0.4-0.3-0.9-0.4-1.5-0.4c-0.9,0-1.4,0.4-1.5,0.9
c-0.1,0.4,0,0.8,0.8,0.8l1.5,0.1c1.8,0.2,2.6,1.2,2.2,2.8C502,326.5,500,327.5,497.8,327.5"/>
<path class="st15" d="M426.3,318.3c-1.3,2.4-5.5,9.3-5.5,9.3h-1.9l-0.8-13.2c0,0,2.7,1.3,2.7,4.1l0.2,4.5l4.9-8.6
C426,314.4,427.6,315.9,426.3,318.3"/>
<path class="st15" d="M431,323.5h-1.9c-0.9,0-1.4,0.4-1.6,1.1s0.1,1.1,1.1,1.1c0.7,0,1.1-0.1,1.6-0.5c0.3-0.2,0.5-0.6,0.7-1.2
L431,323.5 M431.6,327.6c-1,0-1.4-0.8-1.4-0.8c-0.8,0.6-1.5,0.9-2.6,0.9s-1.8-0.3-2.2-0.9c-0.4-0.5-0.4-1.3-0.2-2.2
c0.4-1.5,1.8-2.8,4-2.8h2.2l0.1-0.5c0.3-1.1-0.1-1.5-1.4-1.5c-0.9,0-1.3,0.2-2,0c-0.6-0.2-1.1-0.7-1.1-0.7c1.2-1,2.2-1.3,3.8-1.3
c2.6,0,3.7,1.1,3.1,3.4l-1.7,6.4H431.6z"/>
<path class="st15" d="M449.8,316.7h-1.1l-1.7,6.4c-0.9,3.7-3.7,4.5-3.7,4.5l2.9-10.9h-3.4c0,0,0.3-2.3,2.7-2.3h7.1
C452.7,314.4,451.8,316.7,449.8,316.7"/>
<path class="st15" d="M455.3,327.6l1.6-5.9c0.3-1.3-0.3-1.7-1.1-1.7c-0.7,0-1.6,0.4-2,1.7l-0.7,2.7c-0.8,2.8-3.2,3.2-3.2,3.2
l3.5-13.2h2.3l-1.2,4.4c0.8-0.7,1.7-1,2.5-1c2.1,0,2.8,1.5,2.2,3.5l-0.6,2.1l-1.1,4.2L455.3,327.6"/>
<path class="st15" d="M437.6,324.3c-0.8,2.8-3.2,3.2-3.2,3.2l3.5-13.2h2.3L437.6,324.3"/>
<path class="st15" d="M465.9,320.3c-0.2-0.3-0.5-0.4-1-0.4s-0.9,0.1-1.2,0.4c-0.6,0.5-0.9,1.3-1.2,2.3c-0.3,1-0.4,1.9-0.1,2.4
c0.2,0.3,0.5,0.4,1,0.4s0.9-0.1,1.2-0.4c0.6-0.5,0.9-1.4,1.2-2.4S466.3,320.8,465.9,320.3 M466.2,326.4c-0.7,0.6-1.9,1.2-3.2,1.2
c-1.4,0-2.2-0.6-2.6-1.2c-0.6-0.9-0.5-2-0.1-3.8c0.5-1.7,1-2.8,2.1-3.7c0.7-0.6,1.8-1.2,3.2-1.2s2.2,0.6,2.6,1.2
c0.6,0.9,0.5,2,0.1,3.7C467.7,324.4,467.2,325.4,466.2,326.4"/>
<path class="st15" d="M475.6,320.4c-0.3-0.4-0.5-0.6-1.1-0.6c-0.7,0-1.7,0.6-2,1.8l-0.6,2.3c-0.8,3.2-3.3,3.5-3.3,3.5l2.6-9.7
h2.3l-0.2,0.9c0.6-0.6,1.6-1,2.6-1c0.9,0,1.5,0.2,2,0.9L475.6,320.4"/>
<path class="st15" d="M482.8,320.7c-0.1-0.6-0.5-1-1.3-1s-1.4,0.4-1.8,1c-0.2,0.4-0.4,0.6-0.5,1h3.6
C482.8,321.3,482.8,321,482.8,320.7 M483,323.3h-4.3c-0.3,1.2,0.2,2.2,1.5,2.2c1,0,1.6-0.3,2.4-0.9l1,1.4c-1.2,1-2.3,1.5-4,1.5
c-2.2,0-4.1-1-3-4.9c0.8-3.2,3-4.9,5.4-4.9c2.6,0,3.6,2,2.9,4.6C484.9,322.3,484.6,323.3,483,323.3"/>
<path class="st15" d="M491.3,324.2l0.7-2.6c0.3-1.3-0.3-1.8-1.1-1.8c-0.7,0-1.7,0.4-2,1.8l-1.2,4.3l-0.4,1.5H485l2.6-9.7h2.3
l-0.2,0.9c0.8-0.7,1.8-1,2.6-1c0.9,0,1.6,0.3,2,0.9c0.6,0.8,0.5,1.7,0.2,2.7l-1.6,6.2C492.8,327.4,490.3,327.5,491.3,324.2"/>
</g>
<g>
<path class="st16" d="M392.4,304.3c-7.4,0-13.7,4.8-15.9,11.5c-2.2-6.7-8.4-11.5-15.8-11.5c-9.2,0-16.7,7.5-16.7,16.7
s7.5,16.7,16.7,16.7c7.4,0,13.7-4.8,15.8-11.5c2.2,6.7,8.5,11.5,15.9,11.5c9.2,0,16.7-7.5,16.7-16.7S401.6,304.3,392.4,304.3z
M392.4,336c-8.3,0-15.1-6.7-15.1-15s6.7-15,15.1-15c8.3,0,15.1,6.7,15.1,15C407.5,329.3,400.7,336,392.4,336z"/>
<g>
<rect x="353.5" y="319.6" class="st13" width="14.4" height="0.7"/>
<rect x="357.1" y="327.5" class="st13" width="8.3" height="0.7"/>
<path class="st13" d="M349,325v-3.3h1.8v3.4c0,1.2-0.8,1.8-2.2,1.8c-1.4,0-2.2-0.6-2.2-1.8v-3.4h1.8v3.3c0,0.3,0.1,0.4,0.4,0.4
C348.9,325.4,349,325.3,349,325"/>
<polygon class="st13" points="354.7,326.8 353.7,324.8 353.6,324.8 353.6,326.8 352.1,326.8 352.1,321.7 353.8,321.7
354.8,323.8 354.8,323.8 354.8,321.7 356.4,321.7 356.4,326.8 "/>
<rect x="358" y="321.7" class="st13" width="1.7" height="5.1"/>
<polygon class="st13" points="363.7,323.1 363.7,326.8 362,326.8 362,323.1 360.8,323.1 360.9,321.7 364.9,321.7 364.8,323.1
"/>
<polygon class="st13" points="366,326.8 366,321.7 369.4,321.7 369.3,323 367.8,323 367.8,323.7 369.1,323.7 369.1,324.8
367.8,324.8 367.8,325.6 369.4,325.6 369.3,326.8 "/>
<path class="st13" d="M372.6,323h-0.2v2.5h0.2c0.6,0,0.7-0.5,0.7-1.2C373.3,323.4,373.2,323,372.6,323 M372.6,326.8h-2v-5.1h2
c1.8,0,2.4,1,2.4,2.5C375.1,325.6,374.5,326.8,372.6,326.8"/>
<polygon class="st13" points="353.5,318.9 353.5,313.7 355.2,313.7 355.2,317.5 356.7,317.5 356.5,318.9 "/>
<rect x="357.1" y="313.7" class="st13" width="1.7" height="5.1"/>
<polygon class="st13" points="363.1,318.9 360.5,318.9 359.5,313.7 361.4,313.7 361.8,316.9 361.9,316.9 362.2,313.7 364,313.7
"/>
<polygon class="st13" points="364.6,318.9 364.6,313.7 368,313.7 367.9,315.1 366.4,315.1 366.4,315.7 367.7,315.7 367.7,316.9
366.4,316.9 366.4,317.6 368,317.6 367.9,318.9 "/>
</g>
<path class="st16" d="M393.4,331.8h-1.1c0,0-2.5,0.1-3.8-3.4l-5.5-15.7c0,0,4.7,2,5.9,6L393.4,331.8"/>
<path class="st16" d="M400.5,315.2h-1.1l-1.8,6.8c-1,3.9-3.9,4.7-3.9,4.7l3-11.5h-3.5c0,0,0.3-2.4,2.9-2.4h7.5
C403.5,312.8,402.6,315.2,400.5,315.2"/>
</g>
</g>
</g>
<g id="masque">
<polygon class="st13" points="179.6,226.6 175.1,228.7 172.2,232 172.2,240.4 172.2,244.9 174,245.8 186.7,240.7 191.1,236.5
190.4,231.3 186.4,226.6 "/>
<polygon class="st13" points="140,242 139.3,245.8 141.9,249.4 143.7,251.6 147,246.8 148.2,242.7 143.2,242.2 "/>
<polygon class="st13" points="251.2,260.2 251.2,264.8 253.4,267.3 255.7,264.8 255.7,261 253.6,259.5 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 60 KiB

+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 250 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:10;">
<g id="Ebene-1" serif:id="Ebene 1">
<g>
<path d="M7.532,12.709L7.532,13.161L5.771,13.161L5.771,12.614C5.771,11.115 5.2,10.234 3.867,10.234C2.535,10.234 1.964,11.115 1.964,12.59C1.964,16.326 7.555,16.683 7.555,21.37C7.555,23.893 6.342,25.534 3.796,25.534C1.25,25.534 0.06,23.893 0.06,21.37L0.06,20.418L1.797,20.418L1.797,21.465C1.797,22.964 2.416,23.845 3.748,23.845C5.081,23.845 5.699,22.964 5.699,21.465C5.699,17.753 0.131,17.396 0.131,12.709C0.131,10.139 1.321,8.545 3.819,8.545C6.342,8.545 7.532,10.187 7.532,12.709" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M16.288,16.088L16.288,17.753L12.219,17.753L12.219,23.678L17.216,23.678L17.216,25.368L10.363,25.368L10.363,8.711L17.216,8.711L17.216,10.401L12.219,10.401L12.219,16.088L16.288,16.088Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M26.258,25.344L24.473,25.344L21.713,12.09L21.713,25.368L20.095,25.368L20.095,8.711L22.689,8.711L25.425,22.013L28.066,8.711L30.66,8.711L30.66,25.368L28.899,25.368L28.899,11.971L26.258,25.344Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M39.678,16.088L39.678,17.753L35.609,17.753L35.609,23.678L40.606,23.678L40.606,25.368L33.753,25.368L33.753,8.711L40.606,8.711L40.606,10.401L35.609,10.401L35.609,16.088L39.678,16.088Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M50.719,12.709L50.719,13.161L48.958,13.161L48.958,12.614C48.958,11.115 48.387,10.234 47.054,10.234C45.722,10.234 45.151,11.115 45.151,12.59C45.151,16.326 50.742,16.683 50.742,21.37C50.742,23.893 49.529,25.534 46.983,25.534C44.437,25.534 43.247,23.893 43.247,21.37L43.247,20.418L44.984,20.418L44.984,21.465C44.984,22.964 45.603,23.845 46.935,23.845C48.268,23.845 48.886,22.964 48.886,21.465C48.886,17.753 43.318,17.396 43.318,12.709C43.318,10.139 44.508,8.545 47.007,8.545C49.529,8.545 50.719,10.187 50.719,12.709" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M55.977,25.368L55.977,10.401L52.908,10.401L52.908,8.711L60.903,8.711L60.903,10.401L57.833,10.401L57.833,25.368L55.977,25.368Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M69.302,16.088L69.302,17.753L65.233,17.753L65.233,23.678L70.23,23.678L70.23,25.368L63.377,25.368L63.377,8.711L70.23,8.711L70.23,10.401L65.233,10.401L65.233,16.088L69.302,16.088Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M76.369,16.564C77.844,16.564 78.725,16.088 78.725,14.279L78.725,12.685C78.725,11.186 78.177,10.401 76.797,10.401L74.989,10.401L74.989,16.564L76.369,16.564ZM76.845,8.711C79.462,8.711 80.557,10.044 80.557,12.519L80.557,13.875C80.557,15.707 79.915,16.849 78.487,17.301C80.01,17.753 80.581,18.991 80.581,20.775L80.581,23.369C80.581,24.107 80.604,24.797 80.914,25.368L79.034,25.368C78.82,24.892 78.725,24.464 78.725,23.345L78.725,20.728C78.725,18.848 77.844,18.253 76.298,18.253L74.989,18.253L74.989,25.368L73.133,25.368L73.133,8.711L76.845,8.711Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M89.623,16.088L89.623,17.753L85.554,17.753L85.554,23.678L90.551,23.678L90.551,25.368L83.698,25.368L83.698,8.711L90.551,8.711L90.551,10.401L85.554,10.401L85.554,16.088L89.623,16.088Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M95.095,25.368L93.43,25.368L93.43,8.711L95.785,8.711L99.64,20.752L99.64,8.711L101.282,8.711L101.282,25.368L99.355,25.368L95.095,11.9L95.095,25.368Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M108.254,23.678C109.586,23.678 110.229,22.845 110.229,21.323L110.229,12.756C110.229,11.234 109.586,10.401 108.23,10.401L106.231,10.401L106.231,23.678L108.254,23.678ZM108.278,8.711C110.871,8.711 112.061,10.282 112.061,12.852L112.061,21.251C112.061,23.797 110.871,25.368 108.278,25.368L104.375,25.368L104.375,8.711L108.278,8.711Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M120.913,16.088L120.913,17.753L116.844,17.753L116.844,23.678L121.841,23.678L121.841,25.368L114.988,25.368L114.988,8.711L121.841,8.711L121.841,10.401L116.844,10.401L116.844,16.088L120.913,16.088Z" style="fill:rgb(90,150,200);fill-rule:nonzero;"/>
<path d="M178.95,12.681L178.95,13.13L177.201,13.13L177.201,12.586C177.201,11.097 176.634,10.223 175.31,10.223C173.986,10.223 173.419,11.097 173.419,12.563C173.419,16.273 178.973,16.627 178.973,21.283C178.973,23.789 177.768,25.419 175.239,25.419C172.71,25.419 171.529,23.789 171.529,21.283L171.529,20.338L173.254,20.338L173.254,21.378C173.254,22.867 173.868,23.741 175.192,23.741C176.515,23.741 177.13,22.867 177.13,21.378C177.13,17.691 171.599,17.336 171.599,12.681C171.599,10.128 172.781,8.545 175.263,8.545C177.768,8.545 178.95,10.176 178.95,12.681" style="fill:rgb(233,84,66);fill-rule:nonzero;"/>
<path d="M184.574,18.069L183.605,19.7L183.605,25.254L181.762,25.254L181.762,8.71L183.605,8.71L183.605,16.698L188.001,8.71L189.868,8.71L185.685,16.25L190.01,25.254L188.119,25.254L184.574,18.069Z" style="fill:rgb(233,84,66);fill-rule:nonzero;"/>
<rect x="192.609" y="8.71" width="1.843" height="16.544" style="fill:rgb(233,84,66);"/>
<path d="M200.739,16.509C202.204,16.509 203.078,16.037 203.078,14.241L203.078,12.657C203.078,11.168 202.535,10.388 201.164,10.388L199.368,10.388L199.368,16.509L200.739,16.509ZM201.211,8.71C203.811,8.71 204.898,10.034 204.898,12.492L204.898,13.839C204.898,15.659 204.26,16.793 202.842,17.242C204.355,17.691 204.922,18.92 204.922,20.693L204.922,23.269C204.922,24.001 204.946,24.686 205.253,25.254L203.386,25.254C203.173,24.781 203.078,24.356 203.078,23.245L203.078,20.645C203.078,18.778 202.204,18.187 200.668,18.187L199.368,18.187L199.368,25.254L197.525,25.254L197.525,8.71L201.211,8.71Z" style="fill:rgb(233,84,66);fill-rule:nonzero;"/>
<path d="M213.902,16.037L213.902,17.691L209.861,17.691L209.861,23.576L214.824,23.576L214.824,25.254L208.018,25.254L208.018,8.71L214.824,8.71L214.824,10.388L209.861,10.388L209.861,16.037L213.902,16.037Z" style="fill:rgb(233,84,66);fill-rule:nonzero;"/>
<rect x="217.707" y="8.71" width="1.843" height="16.544" style="fill:rgb(233,84,66);"/>
<path d="M229.783,12.681L229.783,13.13L228.034,13.13L228.034,12.586C228.034,11.097 227.467,10.223 226.144,10.223C224.82,10.223 224.253,11.097 224.253,12.563C224.253,16.273 229.807,16.627 229.807,21.283C229.807,23.789 228.602,25.419 226.073,25.419C223.544,25.419 222.362,23.789 222.362,21.283L222.362,20.338L224.088,20.338L224.088,21.378C224.088,22.867 224.702,23.741 226.026,23.741C227.349,23.741 227.964,22.867 227.964,21.378C227.964,17.691 222.433,17.336 222.433,12.681C222.433,10.128 223.615,8.545 226.096,8.545C228.602,8.545 229.783,10.176 229.783,12.681" style="fill:rgb(233,84,66);fill-rule:nonzero;"/>
<path d="M238.48,16.037L238.48,17.691L234.439,17.691L234.439,23.576L239.402,23.576L239.402,25.254L232.595,25.254L232.595,8.71L239.402,8.71L239.402,10.388L234.439,10.388L234.439,16.037L238.48,16.037Z" style="fill:rgb(233,84,66);fill-rule:nonzero;"/>
<path d="M243.915,25.254L242.261,25.254L242.261,8.71L244.601,8.71L248.429,20.669L248.429,8.71L250.06,8.71L250.06,25.254L248.146,25.254L243.915,11.877L243.915,25.254Z" style="fill:rgb(233,84,66);fill-rule:nonzero;"/>
<path d="M162.932,17.573C162.932,26.334 155.83,33.436 147.069,33.436C138.308,33.436 131.206,26.334 131.206,17.573C131.206,8.812 138.308,1.71 147.069,1.71C155.83,1.71 162.932,8.812 162.932,17.573" style="fill:rgb(91,150,199);fill-rule:nonzero;"/>
<path d="M162.932,17.573C162.932,8.818 155.824,1.71 147.069,1.71C138.314,1.71 131.206,8.818 131.206,17.573C131.206,26.328 138.314,33.436 147.069,33.436C155.824,33.436 162.932,26.328 162.932,17.573Z" style="fill:none;stroke:rgb(232,82,66);stroke-width:2.09px;"/>
<path d="M146.221,10.769C146.221,10.769 146.157,10.739 146.125,10.721L143.056,8.949C142.695,8.74 142.541,8.332 142.711,8.037L142.819,7.85C142.99,7.555 143.42,7.484 143.781,7.692L146.221,9.105L146.221,10.769Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M147.73,10.769C147.73,10.769 147.794,10.739 147.826,10.721L150.895,8.949C151.256,8.74 151.41,8.332 151.24,8.037L151.132,7.85C150.961,7.555 150.531,7.484 150.17,7.692L147.73,9.105L147.73,10.769Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M146.221,24.277C146.221,24.277 146.157,24.307 146.125,24.325L143.056,26.097C142.695,26.306 142.541,26.714 142.711,27.009L142.819,27.196C142.99,27.491 143.42,27.562 143.781,27.354L146.221,25.941L146.221,24.277Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M147.73,24.277C147.73,24.277 147.794,24.307 147.826,24.325L150.895,26.097C151.256,26.306 151.41,26.714 151.24,27.009L151.132,27.196C150.961,27.491 150.531,27.562 150.17,27.354L147.73,25.941L147.73,24.277Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M140.753,14.727C140.753,14.727 140.695,14.767 140.664,14.785L137.596,16.56C137.235,16.769 136.804,16.699 136.634,16.404L136.526,16.217C136.355,15.922 136.509,15.513 136.869,15.305L139.312,13.896L140.753,14.727Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M141.507,13.419C141.507,13.419 141.513,13.348 141.513,13.312L141.51,9.768C141.509,9.351 141.233,9.014 140.892,9.014L140.676,9.014C140.335,9.015 140.059,9.352 140.059,9.769L140.065,12.589L141.507,13.419Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M152.457,21.471C152.457,21.471 152.451,21.541 152.451,21.578L152.454,25.122C152.455,25.539 152.731,25.876 153.072,25.876L153.288,25.876C153.629,25.875 153.905,25.537 153.905,25.121L153.899,22.301L152.457,21.471Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M153.211,20.163C153.211,20.163 153.269,20.122 153.3,20.104L156.368,18.329C156.729,18.121 157.16,18.191 157.33,18.486L157.438,18.673C157.609,18.968 157.455,19.377 157.095,19.585L154.652,20.994L153.211,20.163Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M152.418,13.383C152.418,13.383 152.411,13.312 152.411,13.276L152.391,9.732C152.389,9.315 152.664,8.976 153.004,8.974L153.22,8.973C153.561,8.971 153.84,9.307 153.842,9.724L153.854,12.544L152.418,13.383Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M153.18,14.686C153.18,14.686 153.238,14.726 153.269,14.744L156.349,16.499C156.71,16.706 157.14,16.633 157.309,16.337L157.416,16.149C157.585,15.853 157.429,15.445 157.067,15.239L154.616,13.846L153.18,14.686Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M140.756,20.201C140.756,20.201 140.698,20.161 140.667,20.143L137.587,18.388C137.226,18.181 136.796,18.254 136.627,18.551L136.52,18.738C136.351,19.034 136.507,19.442 136.869,19.648L139.32,21.042L140.756,20.201Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M141.519,21.505C141.519,21.505 141.525,21.575 141.525,21.611L141.545,25.155C141.547,25.572 141.272,25.911 140.932,25.913L140.716,25.914C140.375,25.916 140.096,25.58 140.094,25.163L140.082,22.344L141.519,21.505Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M146.976,13.856L149.161,12.606C149.522,12.398 149.697,12.027 149.553,11.777L149.462,11.619C149.318,11.37 148.909,11.336 148.548,11.545L146.976,12.423L146.976,13.856Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M146.976,13.856L144.866,12.606C144.505,12.398 144.329,12.027 144.473,11.777L144.565,11.619C144.709,11.37 145.118,11.336 145.479,11.545L146.976,12.423L146.976,13.856Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M146.976,21.174L149.161,22.424C149.522,22.632 149.697,23.003 149.553,23.253L149.462,23.411C149.318,23.661 148.909,23.694 148.548,23.486L146.976,22.607L146.976,21.174Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M146.976,21.174L144.866,22.424C144.505,22.632 144.329,23.003 144.473,23.253L144.565,23.411C144.709,23.661 145.118,23.694 145.479,23.486L146.976,22.607L146.976,21.174Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M150.105,15.659L150.084,13.142C150.082,12.726 150.314,12.387 150.602,12.386L150.785,12.385C151.073,12.384 151.308,12.72 151.31,13.137L151.343,14.938L150.105,15.659Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M150.105,15.659L152.248,16.852C152.609,17.059 153.019,17.024 153.161,16.774L153.252,16.615C153.395,16.365 153.218,15.995 152.856,15.788L151.343,14.938L150.105,15.659Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M143.77,15.594L141.591,16.853C141.229,17.06 140.82,17.026 140.677,16.776L140.586,16.617C140.443,16.367 140.62,15.997 140.982,15.79L142.532,14.873L143.77,15.594Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M143.77,15.594L143.752,13.142C143.753,12.725 143.521,12.387 143.233,12.386L143.05,12.385C142.762,12.384 142.528,12.721 142.526,13.137L142.532,14.873L143.77,15.594Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M143.77,19.358L141.591,18.099C141.229,17.892 140.82,17.927 140.677,18.177L140.586,18.335C140.443,18.585 140.62,18.956 140.982,19.163L142.532,20.079L143.77,19.358Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M143.77,19.358L143.752,21.811C143.753,22.227 143.521,22.566 143.233,22.567L143.05,22.567C142.762,22.568 142.528,22.232 142.526,21.815L142.532,20.079L143.77,19.358Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M150.416,19.505L150.463,22.023C150.473,22.439 150.714,22.771 151.002,22.764L151.185,22.76C151.473,22.754 151.698,22.411 151.689,21.994L151.673,20.193L150.416,19.505Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M150.416,19.505L152.526,18.255C152.881,18.038 153.291,18.062 153.441,18.308L153.536,18.465C153.686,18.711 153.518,19.086 153.162,19.302L151.673,20.193L150.416,19.505Z" style="fill:white;fill-rule:nonzero;stroke:rgb(89,149,199);stroke-width:0.25px;"/>
<path d="M146.976,30.184C146.559,30.184 146.221,29.846 146.221,29.429L146.221,5.485C146.221,5.069 146.559,4.731 146.976,4.731C147.392,4.731 147.73,5.069 147.73,5.485L147.73,29.429C147.73,29.846 147.392,30.184 146.976,30.184" style="fill:white;fill-rule:nonzero;"/>
<path d="M136.006,23.849C135.795,23.49 135.917,23.028 136.276,22.818L156.95,10.729C157.31,10.519 157.772,10.64 157.982,10.999C158.192,11.359 158.071,11.821 157.712,12.031L137.037,24.12C136.678,24.33 136.216,24.209 136.006,23.849" style="fill:white;fill-rule:nonzero;"/>
<path d="M158.042,23.816L158.042,23.816C157.835,24.177 157.373,24.301 157.013,24.093L136.243,12.125C135.882,11.917 135.758,11.456 135.966,11.095C136.174,10.734 136.635,10.61 136.996,10.818L157.766,22.786C158.127,22.994 158.25,23.455 158.042,23.816" style="fill:white;fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long
+78
View File
@@ -0,0 +1,78 @@
const INDICATOR_ID = 'loading-indicator'
const HIDDEN_CLASS = 'invisible'
const GRACE_PERIOD_MS = 500
const DEBOUNCE_MS = 200
let debounceTimeout = null
let historyRestoreGracePeriod = false
function getIndicator() {
return document.getElementById(INDICATOR_ID)
}
function show() {
const indicator = getIndicator()
if (indicator) {
indicator.classList.remove(HIDDEN_CLASS)
}
}
function hide() {
if (debounceTimeout) {
clearTimeout(debounceTimeout)
debounceTimeout = null
}
const indicator = getIndicator()
if (indicator) {
indicator.classList.add(HIDDEN_CLASS)
}
}
function handleBeforeRequest() {
// Ignore requests triggered during history restore grace period
if (true === historyRestoreGracePeriod) {
return
}
// Don't start a new debounce if already pending
if (debounceTimeout) {
return
}
// Show indicator after delay
debounceTimeout = setTimeout(() => {
show()
}, DEBOUNCE_MS)
}
function handleHistoryRestore() {
hide()
// Set grace period to ignore change events triggered by history restore
historyRestoreGracePeriod = true
setTimeout(() => {
historyRestoreGracePeriod = false
}, GRACE_PERIOD_MS)
}
function handleTimeout() {
hide()
alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.')
}
function handleBeforeHistorySave() {
// Hide indicator before HTMX saves the page to history cache
hide()
}
// Initialize event listeners
document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest)
document.body.addEventListener('htmx:afterRequest', hide)
document.body.addEventListener('htmx:timeout', handleTimeout)
document.body.addEventListener('htmx:historyRestore', handleHistoryRestore)
document.body.addEventListener('htmx:sendError', hide)
document.body.addEventListener('htmx:responseError', hide)
document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave)
window.addEventListener('pageshow', hide)
window.addEventListener('popstate', handleHistoryRestore)
+24
View File
@@ -40,3 +40,27 @@
.htmx-request.htmx-indicator{
@apply visible;
}
.bg-outer {
@apply bg-blend-multiply bg-right-top bg-cover;
}
.bg-outer--1 {
background-image: url('../images/bg_4.jpg'), linear-gradient(190deg, rgba(24, 82, 123, 1) 0%, rgba(22, 88, 131, 1) 62%, rgba(0, 112, 224, 1) 100%);;
}
.bg-outer--2 {
background-image: url('../images/bg_5.jpg'), linear-gradient(190deg, rgba(24, 82, 123, 1) 0%, rgba(22, 88, 131, 1) 62%, rgba(0, 112, 224, 1) 100%);;
}
.sbw .bg-outer--2 {
background-image: url('../images/bg_7.jpg'), linear-gradient(190deg, rgba(24, 82, 123, 1) 0%, rgba(22, 88, 131, 1) 62%, rgba(0, 112, 224, 1) 100%);;
}
.snz .bg-outer--2 {
background-image: url('../images/bg_6.jpg'), linear-gradient(190deg, rgba(24, 82, 123, 1) 0%, rgba(22, 88, 131, 1) 62%, rgba(0, 112, 224, 1) 100%);;
}
.bg-inner {
background-image: linear-gradient(190deg, rgba(24, 82, 123, 0.8) 0%, rgba(22, 88, 131, 0.8) 62%, rgba(0, 112, 224, 0.8) 100%);;
}
+2 -2
View File
@@ -1,6 +1,6 @@
@import "components/typography.css";
@import "components/forms.css";
@import "components/button.css";
@import "components/menu.css";
@import "components/tooltip.css";
@import "components/table-responsive.css";
@import "components/toast.css";
@import "components/pagination.css";
+40 -24
View File
@@ -1,45 +1,61 @@
.button {
@apply inline-flex items-center justify-center space-x-2 px-4 py-2 md:px-8 cursor-pointer;
@apply inline-flex items-center justify-center space-x-2 h-10 leading-10 px-4 md:px-8 cursor-pointer;
@apply uppercase leading-none text-center;
@apply transition-colors outline-none focus:outline-2 focus:outline-offset-2 focus:outline-primary-light;
@apply rounded-md;
}
.button--small {
@apply px-2 py-1 md:px-4 text-xs sm:text-sm;
}
.button--full {
@apply block w-full;
.button--primary {
background: linear-gradient(120deg, #960167 0%, #9e036a 40%, #fa1a8c 100%);
@apply text-white;
@apply hover:bg-none hover:bg-pink/80;
}
.bg-button {
.button[disabled] {
@apply cursor-not-allowed;
}
.button--secondary {
@apply bg-none bg-primary-dark text-white;
@apply hover:bg-badge-gradient;
@apply hover:bg-primary-light/80;
}
.sbw {
.button--primary {
@apply bg-none bg-sbw-secondary text-white;
@apply hover:bg-none hover:bg-sbw-primary/80;
}
.bg-button--active,
.bg-button--secondary {
@apply bg-badge-gradient;
@apply hover:bg-none hover:bg-pink;
.button--secondary {
@apply bg-none bg-sbw-primary-dark text-white;
@apply hover:bg-none hover:bg-sbw-primary/80;
}
}
.bg-button--muted {
@apply bg-zinc-300 hover:bg-zinc-300;
.snz {
.button--primary {
@apply bg-none bg-snz-secondary text-white;
@apply hover:bg-none hover:bg-snz-primary/80;
}
.button--secondary {
@apply bg-none bg-snz-primary-dark text-white;
@apply hover:bg-none hover:bg-snz-primary/80;
}
}
.bg-button--dark {
@apply bg-primary-dark;
}
.ser {
.button--primary {
@apply bg-none bg-ser-secondary text-white;
@apply hover:bg-none hover:bg-ser-primary/80;
}
.bg-button--light {
@apply bg-white border-2;
@apply font-bold;
@apply border-primary-light text-primary;
}
.bg-button--light:hover,
.bg-button--light.button--active {
@apply bg-white;
@apply border-secondary text-secondary;
.button--secondary {
@apply bg-none bg-ser-primary text-white;
@apply hover:bg-none hover:bg-ser-primary/80;
}
}
+28 -1
View File
@@ -4,7 +4,13 @@ label.required:after {
}
.form-field {
@apply border-zinc-400 focus:border-zinc-800 ring-0 focus:outline-2 focus:outline-offset-2 focus:outline-primary-light mt-1 block w-full;
@apply border-zinc-400 ring-0 mt-1 block w-full rounded-md;
@apply focus:outline-primary-light focus:border-zinc-800 focus:outline-2 focus:outline-offset-2;
}
#participant-form .form-field,
#form-payment .form-field {
@apply bg-primary-bg border-primary-bg;
}
.form-field--has-error {
@@ -27,3 +33,24 @@ select[readonly] {
input[readonly] {
@apply cursor-not-allowed;
}
.sbw {
.form-checkbox,
.form-radio {
@apply text-sbw-secondary;
}
}
.snz {
.form-checkbox,
.form-radio {
@apply text-snz-primary;
}
}
.ser {
.form-checkbox,
.form-radio {
@apply text-ser-secondary;
}
}
-11
View File
@@ -1,11 +0,0 @@
.menu--main {
@apply flex items-center m-0 divide-x divide-white;
}
.menu--main a {
@apply block py-2 px-4 text-white hover:bg-secondary hover:text-zinc-800 text-sm md:text-base;
}
.menu--main .current a {
@apply bg-secondary text-zinc-800;
}
+90
View File
@@ -0,0 +1,90 @@
:root {
--pagination-skew: 2rem;
--pagination-border-width: 1px;
}
.pagination {
@apply flex h-12 lg:h-16 overflow-hidden bg-primary-bg shadow-md;
}
.pagination-item {
flex: 1 1 0;
margin-right: calc(-1 * var(--pagination-skew));
@apply relative;
}
.pagination-item:last-child {
@apply mr-0;
}
/* Outer clip-path for button shape */
.pagination-item:first-child {
clip-path: polygon(0 0, calc(100% - var(--pagination-skew)) 0, 100% 100%, 0 100%);
}
.pagination-item:not(:first-child):not(:last-child) {
clip-path: polygon(0 0, calc(100% - var(--pagination-skew)) 0, 100% 100%, var(--pagination-skew) 100%);
}
.pagination-item:last-child {
clip-path: polygon(0 0, 100% 0, 100% 100%, var(--pagination-skew) 100%);
}
/* Border and content wrappers */
.pagination-item__border {
@apply w-full h-full flex items-center justify-center bg-primary-dark/10;
}
.pagination-item__inner {
@apply w-full h-full flex items-center justify-center;
@apply lg:text-2xl font-bold;
}
a.pagination-item__border > .pagination-item__inner:hover {
@apply bg-primary-light text-white;
}
/* Inner clip-path for border effect */
.pagination-item:first-child .pagination-item__inner {
clip-path: polygon(0 0, calc(100% - var(--pagination-skew)) 0, 100% 100%, 0 100%);
}
.pagination-item:not(:first-child) .pagination-item__inner {
/* No left/bottom-left border on overlapping edge to prevent double borders */
clip-path: polygon(0 0, calc(100% - var(--pagination-skew) - var(--pagination-border-width)) 0, calc(100% - var(--pagination-border-width)) 100%, var(--pagination-skew) 100%);
}
.pagination-item:last-child .pagination-item__inner {
/* No left/bottom-left border on overlapping edge, no right border */
clip-path: polygon(0 0, 100% 0, 100% 100%, var(--pagination-skew) 100%);
}
.sbw {
.pagination-item__border {
@apply bg-sbw-primary-dark/10;
}
a.pagination-item__border > .pagination-item__inner:hover {
@apply bg-sbw-primary;
}
}
.snz {
.pagination-item__border {
@apply bg-snz-primary-dark/10;
}
a.pagination-item__border > .pagination-item__inner:hover {
@apply bg-snz-primary;
}
}
.ser {
.pagination-item__border {
@apply bg-ser-primary/10;
}
a.pagination-item__border > .pagination-item__inner:hover {
@apply bg-ser-primary;
}
}
@@ -1,20 +0,0 @@
.responsive {
@apply text-sm md:text-base;
}
.responsive thead {
@apply hidden md:table-header-group;
}
.responsive tbody tr {
@apply block mb-2 md:table-row;
}
.responsive td {
@apply grid grid-cols-2 gap-x-2 last:col-span-2 md:table-cell;
}
.responsive td:before {
content: attr(data-label);
@apply block font-bold last:hidden md:hidden;
}
+25
View File
@@ -0,0 +1,25 @@
@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--error {
@apply text-gray-100;
background: theme('colors.red.700');
}
.toastify--info {
@apply text-gray-100;
background: theme('colors.primary');
}
+54 -1
View File
@@ -3,7 +3,6 @@
.tippy-box {
background-color: theme('colors.primary');
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: theme('colors.primary');
}
@@ -16,3 +15,57 @@
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: theme('colors.primary');
}
.sbw {
.tippy-box {
background-color: theme('colors.sbw.secondary');
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: theme('colors.sbw.secondary');
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: theme('colors.sbw.secondary');
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: theme('colors.sbw.secondary');
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: theme('colors.sbw.secondary');
}
}
.snz {
.tippy-box {
background-color: theme('colors.snz.primary-dark');
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: theme('colors.snz.primary-dark');
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: theme('colors.snz.primary-dark');
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: theme('colors.snz.primary-dark');
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: theme('colors.snz.primary-dark');
}
}
.ser {
.tippy-box {
background-color: theme('colors.ser.secondary');
}
.tippy-box[data-placement^='top'] > .tippy-arrow::before {
border-top-color: theme('colors.ser.secondary');
}
.tippy-box[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: theme('colors.ser.secondary');
}
.tippy-box[data-placement^='left'] > .tippy-arrow::before {
border-left-color: theme('colors.ser.secondary');
}
.tippy-box[data-placement^='right'] > .tippy-arrow::before {
border-right-color: theme('colors.ser.secondary');
}
}
+126 -122
View File
@@ -1,126 +1,130 @@
{
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.3",
"knplabs/knp-menu-bundle": "^3.4",
"league/flysystem-bundle": "^3.4",
"league/flysystem-sftp-v3": "^3.29",
"league/oauth2-server-bundle": "^0.11.0",
"nelexa/zip": "^4.0",
"nesbot/carbon": "^3.8",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.0",
"spatie/crypto": "^2.1",
"symfony/apache-pack": "^1.0",
"symfony/asset": "6.4.*",
"symfony/console": "6.4.*",
"symfony/doctrine-messenger": "6.4.*",
"symfony/dom-crawler": "6.4.*",
"symfony/dotenv": "6.4.*",
"symfony/expression-language": "6.4.*",
"symfony/flex": "^2",
"symfony/form": "6.4.*",
"symfony/framework-bundle": "6.4.*",
"symfony/http-client": "6.4.*",
"symfony/intl": "6.4.*",
"symfony/mailer": "6.4.*",
"symfony/mime": "6.4.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "6.4.*",
"symfony/process": "6.4.*",
"symfony/property-access": "6.4.*",
"symfony/property-info": "6.4.*",
"symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.4.*",
"symfony/serializer": "6.4.*",
"symfony/stimulus-bundle": "^2.21",
"symfony/string": "6.4.*",
"symfony/translation": "6.4.*",
"symfony/twig-bundle": "6.4.*",
"symfony/uid": "6.4.*",
"symfony/validator": "6.4.*",
"symfony/web-link": "6.4.*",
"symfony/webpack-encore-bundle": "^2.2",
"symfony/yaml": "6.4.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/html-extra": "^3.15",
"twig/intl-extra": "*",
"twig/twig": "^2.12|^3.0",
"voku/anti-xss": "^4.1"
"type": "project",
"license": "proprietary",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-dom": "*",
"ext-iconv": "*",
"ext-simplexml": "*",
"ext-sockets": "*",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.13",
"doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.3",
"league/flysystem-bundle": "^3.4",
"league/flysystem-sftp-v3": "^3.29",
"league/oauth2-server-bundle": "^1.0",
"nelexa/zip": "^4.0",
"nesbot/carbon": "^3.8",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.0",
"spatie/crypto": "^2.1",
"symfony/apache-pack": "^1.0",
"symfony/asset": "6.4.*",
"symfony/console": "6.4.*",
"symfony/doctrine-messenger": "6.4.*",
"symfony/dom-crawler": "6.4.*",
"symfony/dotenv": "6.4.*",
"symfony/expression-language": "6.4.*",
"symfony/flex": "^2",
"symfony/form": "6.4.*",
"symfony/framework-bundle": "6.4.*",
"symfony/html-sanitizer": "6.4.*",
"symfony/http-client": "6.4.*",
"symfony/intl": "6.4.*",
"symfony/lock": "6.4.*",
"symfony/mailer": "6.4.*",
"symfony/mime": "6.4.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "6.4.*",
"symfony/process": "6.4.*",
"symfony/property-access": "6.4.*",
"symfony/property-info": "6.4.*",
"symfony/runtime": "6.4.*",
"symfony/security-bundle": "6.4.*",
"symfony/serializer": "6.4.*",
"symfony/stimulus-bundle": "^2.21",
"symfony/string": "6.4.*",
"symfony/translation": "6.4.*",
"symfony/twig-bundle": "6.4.*",
"symfony/uid": "6.4.*",
"symfony/validator": "6.4.*",
"symfony/web-link": "6.4.*",
"symfony/webpack-encore-bundle": "^2.2",
"symfony/yaml": "6.4.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/html-extra": "^3.15",
"twig/intl-extra": "*",
"twig/twig": "^2.12|^3.0",
"zenstruck/schedule-bundle": "^1.8"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true,
"platform": {
"php": "8.2.28"
}
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": true,
"require": "6.4.*",
"docker": true
}
},
"require-dev": {
"deployer/deployer": "^7.5",
"friendsofphp/php-cs-fixer": "^3.75",
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.4.*",
"symfony/debug-bundle": "6.4.*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^7.1",
"symfony/stopwatch": "6.4.*",
"symfony/web-profiler-bundle": "6.4.*"
"sort-packages": true,
"platform": {
"php": "8.2.28"
}
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": true,
"require": "6.4.*",
"docker": true
}
},
"require-dev": {
"deployer/deployer": "^7.5",
"friendsofphp/php-cs-fixer": "^3.75",
"phpunit/phpunit": "^9.5",
"spatie/ray": "^1.43",
"spatie/x-ray": "^1.2",
"symfony/browser-kit": "6.4.*",
"symfony/css-selector": "6.4.*",
"symfony/debug-bundle": "6.4.*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^7.1",
"symfony/stopwatch": "6.4.*",
"symfony/web-profiler-bundle": "6.4.*"
}
}
Generated
+2167 -1319
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@ return [
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
Zenstruck\ScheduleBundle\ZenstruckScheduleBundle::class => ['all' => true],
];
+2 -1
View File
@@ -11,10 +11,11 @@ doctrine:
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
controller_resolver:
auto_mapping: false
mappings:
App:
type: attribute
+11 -6
View File
@@ -4,13 +4,18 @@ flysystem:
adapter: 'local'
options:
directory: '%env(resolve:XML_EXPORT_PATH)%'
xml_source.storage:
adapter: 'sftp'
options:
host: '%env(SFTP_XML_EXPORT_HOST)%'
port: '%env(int:SFTP_XML_EXPORT_PORT)%'
username: '%env(SFTP_XML_EXPORT_USER)%'
password: '%env(SFTP_XML_EXPORT_PASSWORD)%'
xml_dump.storage:
adapter: 'local'
options:
directory: '%kernel.project_dir%/var/bpn'
# adapter: 'sftp'
# options:
# host: '%env(SFTP_XML_EXPORT_HOST)%'
# port: '%env(int:SFTP_XML_EXPORT_PORT)%'
# username: '%env(SFTP_XML_EXPORT_USER)%'
# password: '%env(SFTP_XML_EXPORT_PASSWORD)%'
json_export.storage:
adapter: 'local'
options:
directory: '%env(resolve:JSON_EXPORT_PATH)%'
+7
View File
@@ -18,6 +18,13 @@ framework:
php_errors:
log: true
http_client:
scoped_clients:
typo3.client:
base_uri: '%env(resolve:APP_CMS_API_BASE_URL)%'
headers:
accept: 'text/json'
when@test:
framework:
test: true
+1 -1
View File
@@ -11,7 +11,7 @@ league_oauth2_server:
resource_server:
public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%'
scopes:
available: ['email','profile','api']
available: ['email','id','profile','roles','api']
default: ['email']
persistence:
doctrine: null
+2
View File
@@ -0,0 +1,2 @@
framework:
lock: '%env(LOCK_DSN)%'
+3 -2
View File
@@ -73,9 +73,10 @@ when@prod:
level: info
channels: ["bpn"]
auth:
type: stream
type: rotating_file
max_files: 7
path: "%kernel.logs_dir%/%kernel.environment%.auth.log"
level: debug
level: info
channels: ["auth"]
core:
type: rotating_file
+2 -1
View File
@@ -32,7 +32,8 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED }
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED, requires_channel: https }
- { path: '^/', roles: PUBLIC_ACCESS, requires_channel: https }
when@test:
security:
+3
View File
@@ -1,6 +1,9 @@
twig:
file_name_pattern: '*.twig'
form_themes: ['forms.html.twig']
globals:
htmx_change_trigger: 'change delay:100ms'
website_base_url: '%env(APP_WEBSITE_BASE_URL)%'
when@test:
twig:
+29
View File
@@ -0,0 +1,29 @@
zenstruck_schedule:
timezone: Europe/Berlin
mailer:
service: mailer
default_to: [email protected]
default_from: [email protected]
subject_prefix: "[MyE&P]"
schedule_extensions:
email_on_failure:
to: [email protected]
tasks:
- task: app:cleanup:xml-dumps
frequency: "0 1 * * *"
description: "Removes outdated XML dumps of requests/responses to BPN API for debugging"
- task: app:bpn:xml-sync
frequency: "*/15 8-19 * * *"
description: "Sync BusPro XML data every 15 min. during peak hours"
- task: app:bpn:xml-sync
frequency: "0 20-23,0-7 * * *"
description: "Sync BusPro XML data hourly outside peak hours"
- task: app:cleanup:log-entries
frequency: "30 1 * * *"
description: "Removes outdated log entries with a retention period of 6 months"
+71 -8
View File
@@ -6,6 +6,27 @@
parameters:
travel_info_base_url: '%env(APP_TRAVEL_INFO_BASE_URL)%'
path_to_keys: '%kernel.project_dir%/config/secret'
bpn_debug: '%env(APP_BPN_DEBUG)%'
# Body dimensions choices for BodyDimensionsType
body_dimensions.height_choices:
'bis 148cm': '-148'
'149 - 157cm': '149-157'
'158 - 166cm': '158-166'
'167 - 178cm': '167-178'
'179 - 185cm': '179-185'
'186 - 194cm': '186-194'
'195cm oder mehr': '195cm+'
body_dimensions.shoe_size_min: 36
body_dimensions.shoe_size_max: 48
body_dimensions.weight_choices:
'42 - 48kg': '42-48'
'49 - 57kg': '49-57'
'58 - 66kg': '58-66'
'67 - 78kg': '67-78'
'79 - 85kg': '79-85'
'86 - 94kg': '86-94'
'95kg oder mehr': '95k+'
services:
# default configuration for services in *this* file
@@ -14,9 +35,12 @@ services:
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
$xmlExport: '@xml_export.storage'
$xmlSource: '@xml_source.storage'
$xmlDump: '@xml_dump.storage'
$jsonExport: '@json_export.storage'
$travelInfoBaseUrl: '%env(APP_TRAVEL_INFO_BASE_URL)%'
$logger: '@monolog.logger.core'
$environment: '%kernel.environment%'
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
@@ -25,6 +49,7 @@ services:
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Model'
- '../src/Kernel.php'
App\BusProNet\ApiClient:
@@ -36,19 +61,14 @@ services:
bpn_api_ip: '%env(APP_BPN_IP)%'
bpn_api_port: '%env(APP_BPN_PORT)%'
debug: '%env(bool:APP_BPN_DEBUG)%'
connection_timeout: '%env(int:APP_BPN_CONNECTION_TIMEOUT)%'
stream_timeout: '%env(int:APP_BPN_STREAM_TIMEOUT)%'
total_timeout: '%env(int:APP_BPN_TOTAL_TIMEOUT)%'
App\Twig\AppRuntime:
arguments:
$intlExtension: '@twig.extension.intl'
App\Menu\MenuBuilder:
arguments:
$factory: '@knp_menu.factory'
tags:
- name: knp_menu.menu_builder
method: createMainMenu
alias: main
App\Command\GenerateKeysCommand:
arguments:
$path: '%path_to_keys%'
@@ -62,3 +82,46 @@ services:
$logger: '@monolog.logger.core'
$preferRemote: '%env(bool:APP_TRAVEL_PREFER_REMOTE)%'
$enableFallback: '%env(bool:APP_TRAVEL_ENABLE_FALLBACK)%'
App\Service\VoucherValidationService:
arguments:
$cache: '@cache.app'
$logger: '@monolog.logger.bpn'
# Field Handlers with dependencies
App\Form\Service\ParticipantInsuranceFieldHandler: ~
App\Form\Service\ParticipantBulkInsuranceFieldHandler: ~
App\Form\Service\ParticipantTransportationDiscountReplacementFieldHandler: ~
App\Form\Service\ParticipantPurchaseVoucherFieldHandler: ~
App\Form\Service\ParticipantPromoVoucherFieldHandler: ~
# Participant Field Handler Registry - most handlers instantiate dependencies directly, some use services
App\Form\Service\ParticipantFieldHandlerRegistry:
arguments:
$handlers:
# Simple handlers with no external dependencies - instantiated directly
- 'App\Form\Service\ParticipantDateOfBirthFieldHandler'
- 'App\Form\Service\ParticipantAssignedRoomFieldHandler'
- 'App\Form\Service\ParticipantRemarksRoomFieldHandler'
- 'App\Form\Service\ParticipantAdditionalServicesFieldHandler'
- 'App\Form\Service\ParticipantCoursesFieldHandler'
- 'App\Form\Service\ParticipantBoardFieldHandler'
- 'App\Form\Service\ParticipantRentalsFieldHandler'
- 'App\Form\Service\ParticipantSkiPassFieldHandler'
- 'App\Form\Service\ParticipantTransportationOutboundFieldHandler'
- 'App\Form\Service\ParticipantTransportationInboundFieldHandler'
- 'App\Form\Service\ParticipantPickupFieldHandler'
- 'App\Form\Service\ParticipantParkingFieldHandler'
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
# Complex handlers with dependencies - use service references
- '@App\Form\Service\ParticipantTransportationDiscountReplacementFieldHandler'
- '@App\Form\Service\ParticipantBulkInsuranceFieldHandler'
- '@App\Form\Service\ParticipantInsuranceFieldHandler'
- '@App\Form\Service\ParticipantPurchaseVoucherFieldHandler'
- '@App\Form\Service\ParticipantPromoVoucherFieldHandler'
App\Service\CmsDataService:
arguments:
$httpClient: '@typo3.client'
$apiKey: 'AbcAbc123'# dummy key, not yet implemented
+8
View File
@@ -0,0 +1,8 @@
const {defineConfig} = require("cypress");
module.exports = defineConfig({
e2e: {
baseUrl: 'https://myep-next-booking.ddev.site',
supportFile: false,
},
});
+61
View File
@@ -0,0 +1,61 @@
describe('Booking test', () => {
it('Books a double room on test travel 1', () => {
cy.visit('/bookings/create/11946/157047')
cy.contains('Neue Buchung')
// await form refresh after selecting registration type
cy.intercept('POST', '/bookings/create/refresh').as('htmxRequest')
cy.get('#booking_create_step1_roomSelections_76_quantity').select('1')
cy.wait('@htmxRequest')
cy.intercept('POST', '/bookings/create').as('htmxRequest')
cy.get('[data-qa-btn-submit]').click()
cy.wait('@htmxRequest')
cy.intercept('GET', '/bookings/create/participants/0').as('htmxRequest')
cy.get('[data-qa-btn-edit-participant="0"]').click()
cy.wait('@htmxRequest')
cy.get('#booking_participant_firstName').type('Theo')
cy.get('#booking_participant_lastName').type('Test')
cy.get('#booking_participant_email').type('[email protected]')
cy.get('#booking_participant_mobile').type('1234567890')
cy.get('#booking_participant_address_street').type('Teststraße 1')
cy.get('#booking_participant_address_postCode').type('12345')
cy.get('#booking_participant_address_city').type('Testhausen')
cy.intercept('POST', '/bookings/create/participants/0/refresh').as('htmxRequest')
cy.get('#booking_participant_dateOfBirth').type('1980-11-11')
cy.get('#booking_participant_dateOfBirth').trigger('change')
cy.wait('@htmxRequest')
cy.get('#booking_participant_skiPass_0').check()
cy.intercept('POST', '/bookings/create/participants/0/refresh').as('htmxRequest')
cy.get('#booking_participant_courses_0').check()
cy.wait('@htmxRequest')
cy.intercept('POST', '/bookings/create/participants/0/refresh').as('htmxRequest')
cy.get('#booking_participant_insurance_0').check()
cy.wait('@htmxRequest')
cy.intercept('POST', '/bookings/create/participants/0/refresh').as('htmxRequest')
cy.get('#booking_participant_transportationOutbound_1').check()
cy.wait('@htmxRequest')
cy.intercept('POST', '/bookings/create/participants/0/refresh').as('htmxRequest')
cy.get('#booking_participant_transportationInbound_1').check()
cy.wait('@htmxRequest')
cy.intercept('POST', '/bookings/create/participants/0').as('htmxRequest')
cy.get('[data-qa-btn-submit]').click()
cy.wait('@htmxRequest')
cy.intercept('POST', '/bookings/create/participants').as('htmxRequest')
cy.get('[data-qa-btn-submit]').click()
cy.wait('@htmxRequest')
cy.get('[data-qa-alert-form-errors]').should('be.visible')
cy.get('[data-qa-participant-incomplete="1"]').should('be.visible')
})
})
+14 -3
View File
@@ -11,17 +11,22 @@ add('shared_dirs', [
'var/bpn',
'var/log',
'var/sessions',
'var/jsonexport',
'var/xmlexport',
]);
add('shared_files', [
'public/.htaccess',
'.env.local.php',
'var/data.db',
]);
set('keep_releases', 3);
// Rsync options, mainly files/dirs to exclude
$rsyncOptions = [
'exclude' => [
'.claude',
'.cursor',
'.idea',
'.vscode',
'.DS_Store',
@@ -40,6 +45,7 @@ $rsyncOptions = [
'.php-cs-fixer.dist.php',
'.phpunit.result.cache',
'config/secret/',
'CLAUDE.md',
'deploy.php',
'package.json',
'package-lock.json',
@@ -72,13 +78,13 @@ host('prod')
->setRemoteUser('ssh-53857-root')
->setForwardAgent(true)
->setSshMultiplexing(true)
->setDeployPath('/kunden/tiefschnee.de/myep-next')
->setDeployPath('/kunden/tiefschnee.de/myep')
->set('writable_mode', 'chmod')
->set('http_user', 'ssh-53857-root')
->set('bin/php', '/usr/bin/php82')
->set('rsync_src', __DIR__)
->set('rsync', $rsyncOptions)
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{release_or_current_path}}/public/ --web-url=https://myep.ep-reisen.de')
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{release_or_current_path}}/public/ --web-url=https://my.ep-reisen.de')
;
host('staging')
@@ -100,6 +106,7 @@ task('deploy', [
'deploy:setup',
'deploy:lock',
'deploy:release',
'deploy:assets',
'rsync',
'deploy:shared',
'deploy:writable',
@@ -109,6 +116,10 @@ task('deploy', [
'deploy:stop-workers',
]);
task('deploy:assets', function () {
runLocally('npm i && npm run build');
});
task('deploy:stop-workers', function () {
run('{{bin/console}} messenger:stop-workers');
});
+475
View File
@@ -0,0 +1,475 @@
# Symfony 7.4 Upgrade Plan
**Project:** MyEP Next Booking
**Current Version:** Symfony 6.4.31
**Target Version:** Symfony 7.4 (LTS)
**Date:** 2026-01-05
**Status:** Ready for Upgrade
---
## Executive Summary
**Estimated Effort: LOW**
**Risk Level: LOW**
**Status: READY FOR UPGRADE**
The MyEP Next Booking application is well-positioned for a Symfony 7.4 upgrade. The codebase follows modern Symfony best practices, uses PHP 8 features extensively, and avoids most deprecated patterns. All pre-upgrade fixes have been completed.
**Key Facts:**
- Symfony 7.4 was released on November 27, 2025
- Requires PHP 8.2.0+ (current: PHP 8.2.29)
- LTS version with bug fixes until November 2028, security fixes until November 2029
- Symfony 8.0 = Symfony 7.4 minus deprecations (same features, PHP 8.4 required)
**Pre-Upgrade Status:**
- PHP version: 8.2.29 (compatible)
- OAuth2 Server Bundle: 1.1.0 (compatible)
- Stimulus Bundle: 2.32.0 (compatible)
- KnpMenuBundle: Removed (no longer in use)
- Doctrine ORM: 3.6.0 (compatible)
- All 312 tests passing
- Email validation mode: Configured (html5)
- 18 test-only deprecation warnings (cosmetic, do not affect runtime)
---
## Previous Plan Status
The previous upgrade plan (dated 2025-12-06) has been updated. All pre-upgrade tasks are now complete.
### Completed Items
| Item | Previous Status | Current Status |
|------|----------------|----------------|
| OAuth2 Server Bundle | 1.0.0 | **1.1.0 installed** |
| Stimulus Bundle | 2.31.0 | **2.32.0 installed** |
| PHP 8.2 deprecations (ParticipantDto::$touched) | Planned | **Completed** |
| Doctrine config (controller_resolver) | Planned | **Completed** |
| SecurityController null safety | Needs verification | **Completed** (uses `?->` and `??`) |
| Email validator "loose" mode deprecation | Needs fix | **Completed** (validator.yaml configured) |
| Failing test in BookingDataProcessorTest | Unrelated to upgrade | **Resolved** (test now passes) |
### Items No Longer Relevant
| Item | Reason |
|------|--------|
| KnpMenuBundle upgrade | **Bundle removed from project** |
---
## Current State Analysis
### PHP Version Compatibility
- **Current Runtime:** PHP 8.2.29
- **Composer Platform:** PHP 8.2.28
- **Symfony 7.4 Required:** PHP 8.2.0+
- **Status:** Compatible
### Installed Dependencies
#### Core Symfony Packages (6.4.x)
All 33 Symfony packages currently at 6.4.x need version constraint updates to 7.4.*
#### Third-Party Bundles - Compatible
| Package | Installed | Symfony 7.4 Compatible |
|---------|-----------|------------------------|
| `doctrine/orm` | 3.6.0 | Yes |
| `doctrine/doctrine-bundle` | 2.18.1 | Yes |
| `doctrine/doctrine-migrations-bundle` | 3.7.0 | Yes |
| `league/oauth2-server-bundle` | 1.1.0 | Yes |
| `symfony/stimulus-bundle` | 2.32.0 | Yes |
| `symfony/webpack-encore-bundle` | 2.4.0 | Yes |
| `symfony/monolog-bundle` | 3.11.0 | Yes |
| `zenstruck/schedule-bundle` | 1.9.0 | Yes |
### Code Quality Assessment
The codebase implements modern Symfony/PHP patterns:
- All controllers use PHP 8 attributes (`#[Route]`)
- Constructor property promotion used throughout
- Strict types enforced on all files
- Typed properties and return type declarations
- Entities use PHP 8 attributes for ORM mapping
- Modern security system (AbstractLoginFormAuthenticator)
- No deprecated annotation usage
---
## Pre-Upgrade Fixes - COMPLETED
### 1. Email Validator "loose" Mode Deprecation - DONE
**Issue:** The default mode for `#[Assert\Email]` is "loose", which is deprecated in Symfony 6.2+ and removed in Symfony 7.0.
**Resolution:** Framework default configured in `config/packages/validator.yaml`:
```yaml
framework:
validation:
email_validation_mode: html5
```
All Email constraints in the codebase also explicitly use `mode: 'strict'`:
- `src/Form/Model/ParticipantDto.php` - Has `mode: 'strict'`
- `src/Form/Model/RegistrationDto.php` - Has `mode: 'strict'`
- `src/BusProNet/Model/Communication.php` - Has `mode: 'strict'`
- `src/Controller/ResetPasswordController.php` - Has `mode: 'strict'`
### 2. Test Deprecation Warnings - COSMETIC ONLY
**Issue:** 18 deprecation warnings appear during test runs.
**Cause:** `ParticipantEditDtoTest` creates a standalone validator using `Validation::createValidatorBuilder()` which bypasses the framework's `validator.yaml` configuration.
**Impact:** These warnings are cosmetic and do not affect:
- Runtime behavior (all Email constraints use explicit `mode: 'strict'`)
- Symfony 7.4 compatibility (no code changes needed)
- Test results (all 312 tests pass)
**Note:** Warnings can be silenced with `SYMFONY_DEPRECATIONS_HELPER=disabled` if desired.
---
## Required Changes
### 1. Update Composer Version Constraints
**File:** `composer.json`
#### Symfony Core Packages
Update all packages from `6.4.*` to `7.4.*`:
```json
{
"require": {
"symfony/asset": "7.4.*",
"symfony/console": "7.4.*",
"symfony/doctrine-messenger": "7.4.*",
"symfony/dom-crawler": "7.4.*",
"symfony/dotenv": "7.4.*",
"symfony/expression-language": "7.4.*",
"symfony/form": "7.4.*",
"symfony/framework-bundle": "7.4.*",
"symfony/html-sanitizer": "7.4.*",
"symfony/http-client": "7.4.*",
"symfony/intl": "7.4.*",
"symfony/lock": "7.4.*",
"symfony/mailer": "7.4.*",
"symfony/mime": "7.4.*",
"symfony/notifier": "7.4.*",
"symfony/process": "7.4.*",
"symfony/property-access": "7.4.*",
"symfony/property-info": "7.4.*",
"symfony/runtime": "7.4.*",
"symfony/security-bundle": "7.4.*",
"symfony/serializer": "7.4.*",
"symfony/string": "7.4.*",
"symfony/translation": "7.4.*",
"symfony/twig-bundle": "7.4.*",
"symfony/uid": "7.4.*",
"symfony/validator": "7.4.*",
"symfony/web-link": "7.4.*",
"symfony/yaml": "7.4.*"
}
}
```
#### Development Packages
```json
{
"require-dev": {
"symfony/browser-kit": "7.4.*",
"symfony/css-selector": "7.4.*",
"symfony/debug-bundle": "7.4.*",
"symfony/stopwatch": "7.4.*",
"symfony/web-profiler-bundle": "7.4.*"
}
}
```
### 2. Update Symfony Flex Configuration
**File:** `composer.json`
```json
{
"extra": {
"symfony": {
"allow-contrib": true,
"require": "7.4.*",
"docker": true
}
}
}
```
---
## Potential Breaking Changes
### 1. Symfony Core Changes (Symfony 7.0 -> 7.4)
Symfony 7.0 removed all deprecated features from 6.4. Our codebase is fully compliant:
- **Native Return Types:** Already implemented
- **Attribute-based Configuration:** Already implemented
- **Security System:** Modern authenticator pattern in use
- **Form System:** Modern approach in use
- **No Annotation Usage:** All using PHP 8 attributes
### 2. Email Validation Mode
**Breaking Change:** The default `mode` for `Email` constraint changes from `loose` to `html5` in Symfony 7.0.
**Status:** Already handled - all constraints use explicit `mode: 'strict'` and framework default is set to `html5`.
### 3. Deprecation Cleanup
Deprecations accumulated in Symfony 7.1-7.4 will need review:
- Check Symfony Profiler after upgrade for any new deprecation warnings
- Review [UPGRADE-7.0.md](https://github.com/symfony/symfony/blob/7.0/UPGRADE-7.0.md)
- Review [UPGRADE-7.4.md](https://github.com/symfony/symfony/blob/7.4/UPGRADE-7.4.md)
---
## Upgrade Procedure
### Phase 1: Preparation (15 minutes)
1. **Create Feature Branch**
```bash
git checkout -b feature/symfony-7.4-upgrade
```
2. **Backup Current State**
```bash
git tag before-symfony-7.4-upgrade
```
### Phase 2: Dependency Updates (30 minutes - 1 hour)
1. **Update composer.json**
- Update all Symfony package constraints to `7.4.*`
- Update `extra.symfony.require` to `"7.4.*"`
2. **Run Composer Update**
```bash
ddev composer update
```
3. **Review Recipe Updates**
```bash
ddev composer recipes
```
Review any recipe updates from Symfony Flex and apply as needed.
4. **Clear All Caches**
```bash
ddev exec bin/console cache:clear
ddev exec bin/console cache:warmup
ddev exec "php -r 'opcache_reset();'"
```
### Phase 3: Testing & Validation (1-2 hours)
1. **Run PHPUnit Test Suite**
```bash
ddev exec ./vendor/bin/phpunit
```
Check for:
- Test failures
- Deprecation warnings
- Fatal errors
2. **Run PHP-CS-Fixer**
```bash
ddev php vendor/bin/php-cs-fixer fix --dry-run --diff
```
3. **Manual Testing - Critical Paths**
- [ ] User authentication (BPN API login)
- [ ] OAuth2 token generation (`/token` endpoint)
- [ ] OAuth2 authorization flow (`/authorize` endpoint)
- [ ] API endpoints (all `/api/*` routes)
- [ ] Booking creation flow (Steps 1-4)
- [ ] Participant form rendering and submission
- [ ] Dynamic field updates (HTMX)
- [ ] Real-time pricing calculations
- [ ] Insurance selection and filtering
- [ ] Transportation service selection
- [ ] Payment method selection
- [ ] Final booking submission to BPN API
- [ ] Booking edit functionality
- [ ] PDF/XML download functionality
- [ ] User registration
- [ ] Password reset
- [ ] Personal data management
4. **Check Symfony Profiler**
- Review deprecation warnings (should be minimal)
- Check performance metrics
- Verify no errors in logs
5. **Test Frontend Assets**
```bash
npm run build
```
- Verify Stimulus controllers load correctly
- Test HTMX interactions
- Verify Toastify notifications
- Check TailwindCSS compilation
### Phase 4: Bug Fixes (0-2 hours)
Address any issues discovered during testing:
1. **Review Error Logs**
```bash
ddev logs
```
2. **Fix Any Remaining Deprecations**
- Use Symfony Profiler to identify deprecations
- Update code to use modern alternatives
### Phase 5: Deployment (1 hour)
1. **Deploy to Staging**
```bash
ddev exec vendor/bin/dep deploy staging
```
2. **Staging Validation**
- Full regression testing
- Performance testing
3. **Production Deployment**
- Schedule maintenance window
- Deploy during low-traffic period
- Monitor error logs closely
---
## Rollback Plan
### Immediate Rollback
If critical issues are discovered:
1. **Git Revert**
```bash
git checkout master
git reset --hard before-symfony-7.4-upgrade
```
2. **Restore Dependencies**
```bash
ddev composer install
```
3. **Clear Caches**
```bash
ddev exec bin/console cache:clear
```
### Production Rollback
If issues occur in production:
```bash
ddev exec vendor/bin/dep rollback production
```
---
## Testing Checklist
### Unit Tests
- [ ] All PHPUnit tests pass (312 tests)
- [ ] No new deprecation warnings
- [ ] Code coverage maintained
### Integration Tests
- [ ] BusProNet API integration
- [ ] XML parsing and data processing
- [ ] Booking price calculations
- [ ] Insurance assignment logic
- [ ] Field handler system
### Functional Tests
- [ ] User authentication flow
- [ ] OAuth2 token generation
- [ ] API endpoints respond correctly
- [ ] Booking creation (all steps)
- [ ] Form validation
- [ ] HTMX partial updates
- [ ] Dynamic field state changes
- [ ] Real-time pricing updates
- [ ] Toast notifications
### Browser Testing
- [ ] Chrome (latest)
- [ ] Firefox (latest)
- [ ] Safari (latest)
- [ ] Mobile browsers
---
## Timeline Estimate
| Phase | Estimated Time | Notes |
|-------|---------------|-------|
| **Phase 1:** Preparation | 15 min | Branch creation |
| **Phase 2:** Dependency Updates | 30 min - 1 hour | composer.json updates, composer update |
| **Phase 3:** Testing & Validation | 1-2 hours | Automated + manual testing |
| **Phase 4:** Bug Fixes | 0-2 hours | Address any issues |
| **Phase 5:** Deployment | 1 hour | Staging deployment |
| **Total** | **2.5-6 hours** | Average: **3.5 hours** |
---
## Future Considerations: Symfony 8.0
Symfony 8.0 was released alongside 7.4 (November 2025) with identical features but:
- Requires PHP 8.4
- No deprecation layers (all deprecated code removed)
- Shorter support window (8 months)
**Recommendation:** Stay on Symfony 7.4 LTS for stability. Plan PHP 8.4 upgrade separately, then consider Symfony 8.x path.
---
## Success Criteria
- [ ] All PHPUnit tests pass (312 tests)
- [ ] No deprecation warnings in Symfony Profiler
- [ ] All critical user flows functional
- [ ] OAuth2 authentication working
- [ ] BPN API integration operational
- [ ] Booking creation flow complete
- [ ] Frontend assets compile and load
- [ ] Performance metrics comparable or better
- [ ] No errors in production logs after 24 hours
---
## Additional Resources
### Official Symfony Documentation
- [Upgrade Major Version Guide](https://symfony.com/doc/current/setup/upgrade_major.html)
- [UPGRADE-7.0.md](https://github.com/symfony/symfony/blob/7.0/UPGRADE-7.0.md)
- [UPGRADE-7.4.md](https://github.com/symfony/symfony/blob/7.4/UPGRADE-7.4.md)
- [Symfony 7.4 Release Notes](https://symfony.com/releases/7.4)
- [Preparing for Symfony 7.4 and 8.0](https://symfony.com/blog/preparing-for-symfony-7-4-and-symfony-8-0)
### Bundle-Specific Documentation
- [OAuth2 Server Bundle](https://github.com/thephpleague/oauth2-server-bundle)
- [Doctrine ORM 3.x Documentation](https://www.doctrine-project.org/projects/orm.html)
---
**Document Version:** 5.0
**Previous Version:** 4.0 (dated 2025-12-06)
**Last Updated:** 2026-01-05
**Status:** Ready for Symfony 7.4 Upgrade
+6 -6
View File
@@ -7,8 +7,8 @@
"Grant Type": "Authorization Code",
"Client ID": "{{oauth2_client_id}}",
"Client Secret": "{{oauth2_client_secret}}",
"Auth URL": "https://myep-next.ddev.site/authorize",
"Token URL": "https://myep-next.ddev.site/token",
"Auth URL": "https://myep-next-booking.ddev.site/authorize",
"Token URL": "https://myep-next-booking.ddev.site/token",
"Redirect URL": "https://myep-team.ddev.site/auth/check",
"Scope": "email"
},
@@ -17,8 +17,8 @@
"Grant Type": "Authorization Code",
"Client ID": "{{oauth2_client_id}}",
"Client Secret": "{{oauth2_client_secret}}",
"Auth URL": "https://myep-next.ddev.site/authorize",
"Token URL": "https://myep-next.ddev.site/token",
"Auth URL": "https://myep-next-booking.ddev.site/authorize",
"Token URL": "https://myep-next-booking.ddev.site/token",
"Redirect URL": "https://myep-team.ddev.site/auth/check",
"Scope": "email profile"
},
@@ -27,8 +27,8 @@
"Grant Type": "Client Credentials",
"Client ID": "{{oauth2_api_client_id}}",
"Client Secret": "{{oauth2_api_client_secret}}",
"Auth URL": "https://myep-next.ddev.site/authorize",
"Token URL": "https://myep-next.ddev.site/token",
"Auth URL": "https://myep-next-booking.ddev.site/authorize",
"Token URL": "https://myep-next-booking.ddev.site/token",
"Scope": "api"
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251212162836 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user ADD hotel_codes JSON NOT NULL COMMENT \'(DC2Type:json)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE user DROP hotel_codes');
}
}
+9276 -6985
View File
File diff suppressed because it is too large Load Diff
+33 -33
View File
@@ -1,35 +1,35 @@
{
"name": "myep-next",
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@hotwired/stimulus": "^3.0.0",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/webpack-encore": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.20",
"core-js": "^3.38.0",
"file-loader": "^6.2.0",
"htmx.org": "^1.9.12",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"regenerator-runtime": "^0.13.9",
"tailwindcss": "^3.4.15",
"webpack": "^5.74.0",
"webpack-cli": "^5.1.0",
"webpack-notifier": "^1.15.0"
},
"license": "WTFPL",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress"
},
"dependencies": {
"@iframe-resizer/child": "^5.3.2",
"tippy.js": "^6.3.7"
}
"name": "myep",
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/preset-env": "^7.16.0",
"@hotwired/stimulus": "^3.0.0",
"@symfony/stimulus-bridge": "^3.2.0",
"@symfony/webpack-encore": "^5.0.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"autoprefixer": "^10.4.20",
"core-js": "^3.38.0",
"cypress": "^15.5.0",
"file-loader": "^6.2.0",
"htmx.org": "^1.9.12",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"regenerator-runtime": "^0.13.9",
"tailwindcss": "^3.4.15",
"tippy.js": "^6.3.7",
"toastify-js": "^1.12.0",
"webpack": "^5.74.0",
"webpack-cli": "^5.1.0",
"webpack-notifier": "^1.15.0"
},
"license": "WTFPL",
"private": true,
"scripts": {
"dev-server": "encore dev-server",
"dev": "encore dev",
"watch": "encore dev --watch",
"build": "encore production --progress",
"cy:open": "cypress open"
}
}
+4
View File
@@ -0,0 +1,4 @@
memory_limit=512M
max_input_vars=10000
post_max_size=128M
+8
View File
@@ -0,0 +1,8 @@
<?php
return [
'host' => 'host.orb.internal',
'port' => 23517,
'remote_path' => '/var/www/html',
'local_path' => '/Users/fromme/Development/Symfony/myep-next-booking',
];
+250 -8
View File
@@ -7,14 +7,19 @@ use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Exception\ResponseParserException;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\ContactFormResponse;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\PromoVoucher;
use App\BusProNet\Model\PurchaseVoucher;
use App\BusProNet\Model\ServiceAvailabilityResponse;
use App\BusProNet\Model\Travel;
use App\BusProNet\Traits\ApiClientTrait;
use App\BusProNet\XmlParser\ApiResponseParser;
use App\Form\Model\BookingEditDto;
use App\Form\Model\BookingDto;
use App\Form\Model\RegistrationDto;
use League\Flysystem\FilesystemException;
use League\Flysystem\FilesystemOperator;
@@ -34,8 +39,12 @@ class ApiClient
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
public const TYPE_AVAILABILITY_HOTEL = 'VERFUEGBARKEITHOTEL';
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
public const TYPE_BOOKING = 'BUCHUNG';
public const TYPE_PRODUCTS = 'PRODUKTE';
public const TYPE_PRODUCT_DATA = 'PRODUKTDATEN';
public const TYPE_AGENCIES = 'AGENTUREN';
public const TYPE_PURCHASE_VOUCHER = 'GUTSCHEINPRUEFUNGEINLOESUNG';
public const TYPE_PROMO_VOUCHER = 'AKTIONSGUTSCHEIN';
private array $config;
@@ -44,6 +53,7 @@ class ApiClient
private readonly ApiResponseParser $responseParser,
private readonly FilesystemOperator $xmlDump,
private readonly LoggerInterface $logger,
private readonly BookingDataProcessor $bookingDataProcessor,
array $options,
) {
$this->config = $this->resolveOptions($options);
@@ -128,6 +138,25 @@ class ApiClient
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data, [], $debug);
}
/**
* @throws ApiClientException
*/
public function createAddress(
PersonalData $personalData,
bool $debug = false,
): ContactFormResponse|Notification {
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_CUSTOMER_DATA),
'satz' => ['@typ' => static::TYPE_CUSTOMER_DATA],
'art' => 'Adresse_Neu',
'adressdaten' => $personalData->toPayload(),
'ohnemailversand' => 'True',
];
return $this->sendRequest(static::TYPE_CUSTOMER_DATA, $data, [], $debug);
}
/**
* @throws ApiClientException
*/
@@ -188,21 +217,75 @@ class ApiClient
/**
* @throws ApiClientException
*/
public function updateBooking(BookingEditDto $formData, bool $debug = false): Notification|BookingUpdate
public function updateBooking(BookingDto $formData, bool $debug = false): Notification|BookingUpdate
{
$payload = (new BookingDataProcessor())->createUpdateRequestPayload($formData);
$payload = $this->bookingDataProcessor->createUpdateRequestPayload($formData);
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING_UPDATE),
'satz' => ['@typ' => static::TYPE_BOOKING_UPDATE],
'buchungsart' => 'Buchung',
'buchungsart' => Constants::BOOKING_TYPE_BOOKING,
...$payload,
];
return $this->sendRequest(static::TYPE_BOOKING_UPDATE, $data, [], $debug);
}
/**
* Submits a booking inquiry for validation.
*
* First phase of the two-phase booking process. Validates all booking data
* and returns pricing information without creating an actual booking.
*
* @param BookingDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
*
* @return Notification|BookingResponse Notification on error, BookingResponse on success
*
* @throws ApiClientException If the API request fails
*/
public function createBookingInquiry(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, Constants::BOOKING_TYPE_INQUIRY);
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING),
'satz' => ['@typ' => static::TYPE_BOOKING],
...$payload,
];
return $this->sendRequest(static::TYPE_BOOKING, $data, [], $debug);
}
/**
* Submits the final booking request.
*
* Second phase of the two-phase booking process. Creates the actual booking
* after successful inquiry validation.
*
* @param BookingDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
*
* @return Notification|BookingResponse Notification on error, BookingResponse with booking number on success
*
* @throws ApiClientException If the API request fails
*/
public function createBooking(BookingDto $bookingDto, bool $debug = false): Notification|BookingResponse
{
$payload = $this->bookingDataProcessor->createBookingRequestPayload($bookingDto, Constants::BOOKING_TYPE_BOOKING);
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING),
'satz' => ['@typ' => static::TYPE_BOOKING],
...$payload,
];
return $this->sendRequest(static::TYPE_BOOKING, $data, [], $debug);
}
/**
* @throws ApiClientException
*/
@@ -221,7 +304,7 @@ class ApiClient
/**
* @throws ApiClientException
*/
public function getAvailabilities(int $dateId): Notification|BaseData
public function getAvailabilities(int $dateId): Notification|ServiceAvailabilityResponse
{
$data = [
'user' => $this->config['bpn_username'],
@@ -328,6 +411,159 @@ class ApiClient
return $this->sendRequest(static::TYPE_PRODUCTS, $data);
}
/**
* Fetches all available agencies from the BusProNet API.
*
* Returns a list of all agencies with their contact information.
* This data is typically cached for long periods as it changes infrequently.
*
* @return Agency[]|Notification Array of Agency objects on success, Notification on error
*
* @throws ApiClientException If the API request fails
*/
public function getAgencies(): array|Notification
{
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_AGENCIES),
'satz' => ['@typ' => static::TYPE_AGENCIES],
];
return $this->sendRequest(static::TYPE_AGENCIES, $data);
}
/**
* Validates a purchase voucher by redemption code.
*
* Purchase vouchers (Gutscheine) have a remaining balance that reduces with each redemption.
* They can be regular purchased vouchers or goodwill vouchers (Kulanz).
* Returns the voucher with remaining balance, or a Notification if not found or invalid.
*
* @param string $redemptionCode The voucher redemption code (einloesecode)
*
* @return Notification|PurchaseVoucher Notification on error, PurchaseVoucher on success
*
* @throws ApiClientException If the API request fails
*/
public function validatePurchaseVoucher(string $redemptionCode): Notification|PurchaseVoucher
{
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_PURCHASE_VOUCHER),
'satz' => ['@typ' => static::TYPE_PURCHASE_VOUCHER],
'einloesecode' => $redemptionCode,
];
return $this->sendRequest(static::TYPE_PURCHASE_VOUCHER, $data);
}
/**
* Validates a promo voucher for a specific travel and participant price.
*
* Promo vouchers (Aktionsgutscheine) provide absolute discounts per person or per booking.
* Applicability is indicated by the pro_buchung_person field: "P" = per person, "B" = per booking.
*
* @param string $promoCode The promo voucher code
* @param int $travelId The travel ID this voucher applies to
* @param float $participantPrice Current participant price for validation
*
* @return Notification|PromoVoucher Notification on error, PromoVoucher on success
*
* @throws ApiClientException If the API request fails
*/
public function validatePromoVoucher(string $promoCode, int $travelId, float $participantPrice): Notification|PromoVoucher
{
// Format price to German format (comma decimal separator)
$priceFormatted = number_format($participantPrice, 2, ',', '');
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_PROMO_VOUCHER),
'satz' => ['@typ' => static::TYPE_PROMO_VOUCHER],
'aktionsgutschein' => $promoCode,
'idreise' => $travelId,
'preis' => $priceFormatted,
];
return $this->sendRequest(static::TYPE_PROMO_VOUCHER, $data);
}
/**
* Sends raw XML to the BPN API with key regeneration.
*
* Parses the XML to extract the request type, regenerates the authentication key
* with the current date, and sends the request. Returns the raw XML response.
*
* @param string $xml The raw XML request body
* @param bool $debug Enable debug mode (XML dumps)
*
* @return string The raw XML response
*
* @throws ApiClientException If the request fails or XML is invalid
*/
public function sendRawXml(string $xml, bool $debug = false): string
{
$requestId = date(DATE_ATOM).uniqid();
$doc = new \DOMDocument();
if (false === @$doc->loadXML($xml)) {
throw new ApiClientException('Invalid XML provided');
}
$satzNode = $doc->getElementsByTagName('satz')->item(0);
if (null === $satzNode) {
throw new ApiClientException('Missing <satz> element in XML');
}
$type = $satzNode->getAttribute('typ');
if ('' === $type) {
throw new ApiClientException('Missing typ attribute on <satz> element');
}
$keyNode = $doc->getElementsByTagName('key')->item(0);
if (null === $keyNode) {
throw new ApiClientException('Missing <key> element in XML');
}
$newKey = $this->createKey(
$this->config['bpn_username'],
$this->config['bpn_password'],
$type
);
$keyNode->nodeValue = $newKey;
$body = $doc->saveXML();
$this->logger->info('Sending raw XML request to BPN API', [
'requestId' => $requestId,
'type' => $type,
]);
if (true === $debug || true === $this->config['debug']) {
$this->dumpXmlToFile('request', $requestId, $body);
}
$socket = $this->connect(
$this->config['bpn_api_ip'],
$this->config['bpn_api_port'],
$this->config['max_retries'],
$this->config['connection_timeout'],
$this->config['stream_timeout'],
$this->config['total_timeout']
);
$this->send($socket, $body, $this->config['total_timeout']);
$response = $this->receive($socket, $this->config['total_timeout']);
$this->disconnect($socket);
$responseXml = substr($response, 10);
if (true === $debug || true === $this->config['debug']) {
$this->dumpXmlToFile('response', $requestId, $responseXml);
}
return $responseXml;
}
/**
* @throws ApiClientException
*/
@@ -355,10 +591,13 @@ class ApiClient
$socket = $this->connect(
$this->config['bpn_api_ip'],
$this->config['bpn_api_port'],
$this->config['max_retries']
$this->config['max_retries'],
$this->config['connection_timeout'],
$this->config['stream_timeout'],
$this->config['total_timeout']
);
$this->send($socket, $body);
$response = $this->receive($socket);
$this->send($socket, $body, $this->config['total_timeout']);
$response = $this->receive($socket, $this->config['total_timeout']);
$this->disconnect($socket);
// message length (10 bytes) is prepended to actual message
@@ -408,6 +647,9 @@ class ApiClient
$optionsResolver->setDefaults([
'max_retries' => 25,
'debug' => false,
'connection_timeout' => 5,
'stream_timeout' => 30,
'total_timeout' => 45,
]);
return $optionsResolver->resolve($options);
+49
View File
@@ -15,8 +15,57 @@ final class Constants
public const TOKEN_ADDITIONAL = 'SON';
public const TOKEN_BOARD = 'VPF';
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
public const TOKEN_RENTAL_INSURANCE = 'LVS';
public const TOKEN_INSURANCES = ['RRV', 'PAK', 'OHN', 'PKG'];
public const TOKEN_PARKING = 'PAR';
// Insurance subtype labels for display
public const INSURANCE_LABELS = [
'RRV' => 'Reiserücktrittsversicherung',
'PAK' => 'Reiseschutz',
'OHN' => 'Selbstbehalt',
];
// Normalized service group keys for pricing display
public const GROUP_TRANSPORTATION = 'transportation';
public const GROUP_RENTALS = 'rentals';
public const GROUP_INSURANCE = 'insurance';
// Service type display labels (German) - used in pricing calculator
public const SERVICE_LABELS = [
self::TOKEN_COURSES => 'Kurse',
self::TOKEN_SKI_PASS => 'Skipässe',
self::TOKEN_ADDITIONAL => 'Zusatzleistungen',
self::TOKEN_BOARD => 'Verpflegung',
self::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung',
self::TOKEN_PARKING => 'Parkplatz',
self::GROUP_TRANSPORTATION => 'Beförderung',
self::GROUP_RENTALS => 'Leihmaterial',
self::GROUP_INSURANCE => 'Reiseversicherungen',
];
public const STATUS_AVAILABLE = 'Frei';
public const STATUS_BLOCKED = 'Buchungsstop';
public const STATUS_ON_REQUEST = 'Anfrage';
// Booking types for API requests
public const BOOKING_TYPE_INQUIRY = 'Anfrage';
public const BOOKING_TYPE_BOOKING = 'Buchung';
// Booking status codes from buchungstatusmoeglich attribute
public const BOOKING_STATUS_FREE = 'F';
public const BOOKING_STATUS_INQUIRY = 'A';
public const BOOKING_STATUS_OPEN = 'O'; // Optionsbuchung - not relevant for current implementation
// Payment methods
public const PAYMENT_METHOD_TRANSFER = 'transfer';
public const PAYMENT_METHOD_DEBIT = 'debit';
// Payment type IDs for API
public const PAYMENT_TYPE_ID_TRANSFER = 2;
public const PAYMENT_TYPE_ID_DEBIT = 5;
// Baby room constraints
public const BABY_MAX_AGE = 2;
public const BABY_ROOM_CODE = 'Baby';
}
@@ -4,20 +4,235 @@ declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Model\Communication;
use App\Form\Model\BookingEditDto;
use App\BusProNet\Constants;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Travel;
use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BankAccountDto;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\BookingPriceCalculatorService;
use App\Service\InsuranceService;
/**
* Processes booking form data and converts it into BusProNet API payload format.
*
* This processor handles the complex transformation of booking edit form data into the structured
* array format required by the BusProNet API. It manages service mappings between participants
* and various booking components like additional services, transportation, and accommodations.
* The processor ensures data consistency by resetting and rebuilding participant-to-service
* mappings based on form selections, while maintaining proper API payload structure.
* This processor orchestrates the transformation of booking data between DTO and API formats.
* It delegates to specialized classes for service mapping, participant processing,
* payload building, and personal data synchronization.
*/
class BookingDataProcessor
{
public function __construct(
private readonly InsuranceService $insuranceService,
private readonly BookingPriceCalculatorService $priceCalculatorService,
private readonly ServiceMappingCollector $mappingCollector,
private readonly ParticipantServiceProcessor $serviceProcessor,
private readonly BookingPayloadBuilder $payloadBuilder,
private readonly PersonalDataSynchronizer $personalDataSynchronizer,
) {
}
/**
* Creates a BookingDto from an existing Booking entity (for edit mode).
*
* Extracts all service selections from the booking entity and assigns them
* to participant DTOs, creating a unified data structure identical to create mode.
*
* IMPORTANT: Address handling for edit mode
* - ParticipantDto::fromPersonalData() clones address objects to prevent shared references
* - First participant's address is NOT auto-populated from applicant
* - Template uses placeholders to show applicant's address as hints
* - This prevents address data from being inadvertently modified during form binding
*/
public function createBookingDtoFromBooking(Booking $booking, Travel $travel): BookingDto
{
$dto = new BookingDto($travel, $booking->hotelId);
$dto->booking = $booking;
$dto->agencyId = $booking->agencyId;
// Map payment ID from API to form payment method
$dto->paymentMethod = match ($booking->paymentId) {
(string) Constants::PAYMENT_TYPE_ID_DEBIT => Constants::PAYMENT_METHOD_DEBIT,
(string) Constants::PAYMENT_TYPE_ID_TRANSFER => Constants::PAYMENT_METHOD_TRANSFER,
default => Constants::PAYMENT_METHOD_TRANSFER,
};
if (null !== $booking->bankAccount) {
$dto->bankAccount = BankAccountDto::fromBankAccount($booking->bankAccount);
}
foreach ($booking->participants as $index => $participant) {
$participantData = ParticipantDto::fromPersonalData($participant);
$participantData->index = $index;
// First participant (applicant): copy data from applicant if participant data is empty
// BPN API may return full data only in <anmelder> but minimal/empty data in <teilnehmer id="1">
if (0 === $index) {
// Copy address if first participant has no street (indicating empty/incomplete address)
// This allows applicant and first participant to be different people with different addresses
if (null !== $booking->applicant->address) {
$isEmpty = null === $participantData->address
|| null === $participantData->address->street
|| '' === trim($participantData->address->street);
if ($isEmpty) {
$participantData->address = clone $booking->applicant->address;
}
}
// Copy body dimensions from applicant if not present in participant
if (null === $participantData->height && null !== $booking->applicant->height) {
$participantData->height = $booking->applicant->height;
}
if (null === $participantData->weight && null !== $booking->applicant->weight) {
$participantData->weight = $booking->applicant->weight;
}
if (null === $participantData->shoeSize && null !== $booking->applicant->shoeSize) {
$participantData->shoeSize = $booking->applicant->shoeSize;
}
}
// Extract service selections from booking and assign to participant DTO
$participantData->courses = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_COURSES);
$participantData->skiPass = $booking->getSkiPassForParticipant($index);
$participantData->additionalServices = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_ADDITIONAL);
$participantData->board = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_BOARD);
$participantData->rentals = $booking
->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_RENTALS);
// Rental insurance (single service, not array)
$participantData->rentalInsurance = $booking->getRentalInsuranceForParticipant($index);
$participantData->rentalInsuranceSelected = null !== $participantData->rentalInsurance;
// Transportation services
$participantData->transportationOutbound = $booking
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::OUTBOUND_BOOKING);
$participantData->transportationInbound = $booking
->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::INBOUND_BOOKING);
// Pickup location
$participantData->pickup = $booking->getPickupForParticipant($index);
// Parking (stored in form data, not in booking entity - needs special handling)
// For now, leave as false - may need to extract from transportation services
$participantData->parking = false;
// License plate (not stored in booking entity)
$participantData->licensePlate = null;
// Insurance
$participantData->insurance = $booking->getInsuranceForParticipant($index);
// Room assignment
$room = $booking->getRoomForParticipant($index);
$participantData->assignedRoomId = $room?->id;
// Enrich services with data from travel (especially prices)
$this->enrichParticipantServicesFromTravel($participantData, $travel);
$dto->participants[$index] = $participantData;
}
// Rebuild roomSelections array from booking room data
// This is needed for conditions like SingleRoomTypeCondition that rely on roomSelections
// The booking already contains rooms with actual quantities (anzahl from XML)
$availableRooms = $travel->getAvailableRooms();
foreach ($booking->rooms as $bookingRoom) {
$room = $availableRooms[$bookingRoom->id] ?? null;
if (null !== $room && null !== $bookingRoom->totalCount && $bookingRoom->totalCount > 0) {
$roomSelection = new RoomSelectionDto();
$roomSelection->id = $bookingRoom->id;
$roomSelection->label = $room->label;
$roomSelection->price = $room->price;
$roomSelection->quantity = $bookingRoom->totalCount; // Actual quantity from booking XML (anzahl)
$dto->roomSelections[] = $roomSelection;
}
}
return $dto;
}
/**
* Enriches participant service selections with data from travel model.
*
* Services extracted from booking API responses might not include all necessary data
* (especially prices). This method looks up each service in the travel data and copies
* over missing properties to ensure proper pricing calculations.
*
* @param ParticipantDto $participant The participant with service selections
* @param Travel $travel The travel data containing full service information
*/
private function enrichParticipantServicesFromTravel(ParticipantDto $participant, Travel $travel): void
{
// Enrich courses
foreach ($participant->courses as $key => $course) {
if (isset($travel->additionalServices[$course->id])) {
$participant->courses[$key] = $travel->additionalServices[$course->id];
}
}
// Enrich ski pass
if (null !== $participant->skiPass && isset($travel->additionalServices[$participant->skiPass->id])) {
$participant->skiPass = $travel->additionalServices[$participant->skiPass->id];
}
// Enrich additional services
foreach ($participant->additionalServices as $key => $service) {
if (isset($travel->additionalServices[$service->id])) {
$participant->additionalServices[$key] = $travel->additionalServices[$service->id];
}
}
// Enrich board
foreach ($participant->board as $key => $board) {
if (isset($travel->additionalServices[$board->id])) {
$participant->board[$key] = $travel->additionalServices[$board->id];
}
}
// Enrich rentals
foreach ($participant->rentals as $key => $rental) {
if (isset($travel->additionalServices[$rental->id])) {
$participant->rentals[$key] = $travel->additionalServices[$rental->id];
}
}
// Enrich rental insurance
if (null !== $participant->rentalInsurance && isset($travel->additionalServices[$participant->rentalInsurance->id])) {
$participant->rentalInsurance = $travel->additionalServices[$participant->rentalInsurance->id];
}
// Enrich transportation services
if (null !== $participant->transportationOutbound && isset($travel->transportationServices[$participant->transportationOutbound->id])) {
$participant->transportationOutbound = $travel->transportationServices[$participant->transportationOutbound->id];
}
if (null !== $participant->transportationInbound && isset($travel->transportationServices[$participant->transportationInbound->id])) {
$participant->transportationInbound = $travel->transportationServices[$participant->transportationInbound->id];
}
// Enrich pickup
if (null !== $participant->pickup && isset($travel->pickupsOutbound[$participant->pickup->id])) {
$participant->pickup = $travel->pickupsOutbound[$participant->pickup->id];
}
// Enrich insurance
if (null !== $participant->insurance && isset($travel->insurances[$participant->insurance->id])) {
$participant->insurance = $travel->insurances[$participant->insurance->id];
}
}
/**
* Creates an update request payload for the BusProNet API from booking form data.
*
@@ -26,382 +241,133 @@ class BookingDataProcessor
* updates, and generates the complete payload structure including booking metadata,
* participant information, services, transportation, and accommodation details.
*
* The process involves:
* - Resetting existing service-to-participant mappings
* - Rebuilding mappings based on current form selections
* - Adding new services from travel data when participants select them
* - Removing services with no participant mappings
* - Updating participant personal data from form input
* - Synchronizing applicant data with first participant details
* - Building the final API payload structure
*
* @param BookingEditDto|null $formData The booking edit form data containing updated participant and service selections
* @param BookingDto|null $formData The booking edit form data containing updated participant and service selections
*
* @return array The structured payload array ready for BusProNet API submission
*/
public function createUpdateRequestPayload(?BookingEditDto $formData): array
public function createUpdateRequestPayload(?BookingDto $formData): array
{
// Apply bulk insurance if enabled (modifies DTO in place)
$this->applyBulkInsuranceIfActive($formData);
$bookingData = $formData->booking;
$travelData = $formData->travel;
$this->resetServiceMappings($bookingData);
// CRITICAL: The applicant address may have been lost during form binding because ParticipantDto::fromPersonalData()
// stores a reference to the address object, and Symfony's form binding can modify it in place.
// Since we can't easily restore it here without the original booking, we rely on the EditController
// to preserve the original booking's applicant data by fetching fresh data before calling updateBooking().
// The applicant data should NEVER be modified in edit mode.
$this->serviceProcessor->resetServiceMappings($bookingData);
foreach ($formData->participants as $participant) {
$this->processParticipantServices($participant, $bookingData, $travelData);
$this->serviceProcessor->processParticipantServices($participant, $bookingData, $travelData);
}
$this->removeUnusedServices($bookingData);
$this->updateParticipantPersonalData($formData->participants, $bookingData);
$this->syncApplicantData($bookingData);
$this->serviceProcessor->removeUnusedServices($bookingData);
$this->personalDataSynchronizer->updateParticipantPersonalData($formData->participants, $bookingData);
$payload = $this->buildBasePayload($bookingData);
$this->addBankAccountToPayload($payload, $bookingData);
$this->buildParticipantPayload($payload, $bookingData);
$this->buildServicesPayload($payload, $bookingData);
$this->buildPickupPayload($payload, $bookingData);
$payload = $this->payloadBuilder->buildBasePayload($bookingData);
$this->payloadBuilder->addBankAccountToPayload($payload, $bookingData);
$this->payloadBuilder->buildParticipantPayload($payload, $bookingData, $formData->participants);
$this->payloadBuilder->buildServicesPayload($payload, $bookingData);
$this->payloadBuilder->buildPickupPayload($payload, $bookingData);
// Add purchase vouchers if any exist
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($formData);
if (false === empty($purchaseVouchers)) {
$payload['gutscheine']['gutschein'] = [];
foreach ($purchaseVouchers as $code) {
$payload['gutscheine']['gutschein'][] = [
'@einloesecode' => $code,
];
}
}
return $payload;
}
/**
* Resets all existing participant-to-service mappings to start with a clean slate.
* Creates a booking request payload for new bookings (inquiry or final booking).
*
* This ensures that service assignments are rebuilt from scratch based on current form selections.
* Generates the array payload structure for creating new bookings through the BusProNet API.
* This includes all participant data, room selections, services (including insurance), and
* payment information. The booking type determines whether this is an inquiry validation
* ('Anfrage') or a final booking commit ('Buchung').
*
* @param object $bookingData The booking data object containing services to reset
* @param BookingDto $bookingDto The booking creation form data
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
*
* @return array The structured payload array for BusProNet API submission
*/
private function resetServiceMappings(object $bookingData): void
public function createBookingRequestPayload(BookingDto $bookingDto, string $bookingType): array
{
$servicesToReset = [
...$bookingData->additionalServices,
...$bookingData->transportationServices,
...$bookingData->pickupsTo,
...$bookingData->pickupsFro,
];
// Apply bulk insurance if enabled (modifies DTO in place)
$this->applyBulkInsuranceIfActive($bookingDto);
foreach ($servicesToReset as $service) {
$service->mapping = [];
}
return $this->payloadBuilder->buildCreatePayload($bookingDto, $bookingType);
}
/**
* Processes all services for a single participant.
* Applies bulk insurance assignment if the applicant has enabled it.
*
* This orchestrator method handles the complete service assignment workflow for one participant,
* including additional services, transportation services, and pickup locations.
* When bulk insurance is active (applicant's bulkInsuranceBooking = true), this method
* assigns the applicant's insurance TYPE to all dependent participants with automatic
* price tier adjustment based on each participant's total cost.
*
* @param object $participant The participant data from the form
* @param object $bookingData The booking data object to update
* @param object $travelData The travel data containing available services
* IMPORTANT: In edit mode, bulk insurance only applies to participants who:
* 1. Currently have NO insurance assigned (insurance === null)
* 2. OR whose insurance was already assigned via previous bulk operation
*
* This prevents overriding individually selected insurances that are locked.
*
* @param BookingDto $bookingDto The booking DTO (create or edit flow)
*/
private function processParticipantServices(object $participant, object $bookingData, object $travelData): void
private function applyBulkInsuranceIfActive(BookingDto $bookingDto): void
{
if (true === $participant->isCanceled()) {
$applicant = $bookingDto->getParticipant(0);
// Check if bulk insurance is enabled and applicant has selected an insurance
if (null === $applicant || false === $applicant->bulkInsuranceBooking || null === $applicant->insurance) {
return;
}
$this->processAdditionalServices($participant, $bookingData, $travelData);
$this->processTransportationServices($participant, $bookingData, $travelData);
$this->processPickupLocations($participant, $bookingData);
}
// Get selectable (non-complementary) insurances with caching
$selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel);
/**
* Processes additional services for a participant.
*
* Maps additional services (courses, ski passes, board options, rentals) to the participant.
* Adds new services to the booking if they don't already exist and sets individual pricing.
*
* @param object $participant The participant data from the form
* @param object $bookingData The booking data object to update
* @param object $travelData The travel data containing available services
*/
private function processAdditionalServices(object $participant, object $bookingData, object $travelData): void
{
$servicesToMap = [
...$participant->courses,
...$participant->additionalServices,
...$participant->skiPass,
...$participant->board,
...$participant->rentals,
];
foreach ($servicesToMap as $service) {
if (false === isset($bookingData->additionalServices[$service->id])) {
$serviceToAdd = $travelData->additionalServices[$service->id] ?? null;
if (null !== $serviceToAdd) {
$bookingData->additionalServices[$service->id] = $serviceToAdd;
$bookingData->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
}
}
$bookingData->additionalServices[$service->id]->mapping[] = $participant->index;
}
}
/**
* Processes transportation services for a participant.
*
* Maps transportation services (both directions: to and from destination) to the participant.
* Adds new transportation services to the booking if they don't already exist.
*
* @param object $participant The participant data from the form
* @param object $bookingData The booking data object to update
* @param object $travelData The travel data containing available services
*/
private function processTransportationServices(object $participant, object $bookingData, object $travelData): void
{
foreach ([$participant->transportationServiceTo, $participant->transportationServiceFro] as $service) {
if (false === isset($bookingData->transportationServices[$service->id])) {
$serviceToAdd = $travelData->transportationServices[$service->id] ?? null;
if (null !== $serviceToAdd) {
$bookingData->transportationServices[$service->id] = $serviceToAdd;
$bookingData->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
}
}
$bookingData->transportationServices[$service->id]->mapping[] = $participant->index;
}
}
/**
* Processes pickup locations for participants using bus transportation.
*
* Only processes pickup locations for bus transportation services and maps the participant
* to their selected pickup location.
*
* @param object $participant The participant data from the form
* @param object $bookingData The booking data object to update
*/
private function processPickupLocations(object $participant, object $bookingData): void
{
if ('BUS' === $participant->transportationServiceTo->subType && null !== $selectedPickup = $participant->pickup) {
if (false === isset($bookingData->pickupsTo[$selectedPickup->id])) {
$bookingData->pickupsTo[$selectedPickup->id] = $selectedPickup;
}
$bookingData->pickupsTo[$selectedPickup->id]->mapping[] = $participant->index;
}
}
/**
* Removes services and pickups with no participant mappings.
*
* Cleans up unused services to prevent empty services from being sent to the API.
* This includes additional services, transportation services, and pickup locations.
*
* @param object $bookingData The booking data object to clean up
*/
private function removeUnusedServices(object $bookingData): void
{
foreach ($bookingData->additionalServices as $service) {
if (0 === count($service->mapping)) {
unset($bookingData->additionalServices[$service->id]);
}
// Calculate travel prices for all participants (excluding insurance)
$participantPrices = [];
foreach ($bookingDto->getParticipants() as $index => $participant) {
$participantPrices[$index] = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index);
}
foreach ($bookingData->transportationServices as $service) {
if (0 === count($service->mapping)) {
unset($bookingData->transportationServices[$service->id]);
}
}
// Use InsuranceService for proper type-based assignment with price tier matching
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
$selectableInsurances,
$applicant->insurance,
$bookingDto,
$participantPrices
);
foreach ($bookingData->pickupsTo as $pickup) {
if (0 === count($pickup->mapping)) {
unset($bookingData->pickupsTo[$pickup->id]);
// Apply assignments to dependent participants (skip applicant at index 0)
foreach ($assignments as $index => $insurance) {
if (0 === $index) {
continue; // Skip applicant
}
}
foreach ($bookingData->pickupsFro as $pickup) {
if (0 === count($pickup->mapping)) {
unset($bookingData->pickupsFro[$pickup->id]);
}
}
}
/**
* Updates participant personal data from form input.
*
* Only processes participants with status 'F' (active/confirmed participants).
* Updates all personal data fields and communication information.
*
* @param array $participants The participants array from the form
* @param object $bookingData The booking data object to update
*/
private function updateParticipantPersonalData(array $participants, object $bookingData): void
{
foreach ($participants as $participant) {
if ('F' !== $participant->status) {
$participant = $bookingDto->getParticipant($index);
if (null === $participant) {
continue;
}
$bookingData->participants[$participant->index]->firstName = $participant->firstName;
$bookingData->participants[$participant->index]->name = $participant->lastName;
$bookingData->participants[$participant->index]->dateOfBirth = $participant->dateOfBirth;
$bookingData->participants[$participant->index]->gender = $participant->gender;
$bookingData->participants[$participant->index]->nationality = $participant->nationality;
$bookingData->participants[$participant->index]->height = $participant->height;
$bookingData->participants[$participant->index]->weight = $participant->weight;
$bookingData->participants[$participant->index]->shoeSize = $participant->shoeSize;
if ($participant->email || $participant->mobile) {
if (null === $bookingData->participants[$participant->index]->communication) {
$bookingData->participants[$participant->index]->communication = new Communication();
}
$bookingData->participants[$participant->index]->communication->email = $participant->email;
$bookingData->participants[$participant->index]->communication->mobile = $participant->mobile;
// In edit mode: only apply bulk insurance if participant has no insurance
// This respects the rule that once assigned, insurance cannot be changed
if (BookingDto::MODE_EDIT === $bookingDto->getMode() && null !== $participant->insurance) {
continue; // Skip participants with existing insurance assignment
}
}
}
/**
* Synchronizes applicant data with first participant's physical characteristics.
*
* The applicant (booking holder) inherits physical data from the first participant.
*
* @param object $bookingData The booking data object to update
*/
private function syncApplicantData(object $bookingData): void
{
if (false !== $firstParticipant = reset($bookingData->participants)) {
$bookingData->applicant->height = $firstParticipant->height;
$bookingData->applicant->weight = $firstParticipant->weight;
$bookingData->applicant->shoeSize = $firstParticipant->shoeSize;
}
}
/**
* Builds the base API payload structure with booking information.
*
* Creates the main structure that will be populated with detailed data sections.
*
* @param object $bookingData The booking data object
*
* @return array The base payload structure
*/
private function buildBasePayload(object $bookingData): array
{
return [
'idbuchung' => $bookingData->id,
'status' => $bookingData->status,
'idagentur' => $bookingData->agencyId,
'idreise' => $bookingData->dateId,
'idpartner' => $bookingData->hotelId,
'anmelder' => $bookingData->applicant->toPayload(),
'zahlung' => [
'@idzahlungsart' => $bookingData->paymentId,
'@bezeichnung' => $bookingData->paymentLabel,
'@art' => $bookingData->paymentType,
],
'teilnehmerliste' => [
'teilnehmer' => [],
],
'zusatzleistungen' => [
'zusatzleistung' => [],
],
'beförderungen' => [
'beförderung' => [],
],
'ferienzielunterbringungen' => [
'ferienzielunterbringung' => [],
],
];
}
/**
* Adds bank account information to the payload if present.
*
* Bank account information is required for direct debit payments.
*
* @param array $payload The payload array to modify
* @param object $bookingData The booking data object
*/
private function addBankAccountToPayload(array &$payload, object $bookingData): void
{
if (null !== $bookingData->bankAccount) {
$payload['zahlung']['bankverbindung'] = [
'@kreditinstitut' => $bookingData->bankAccount->bankName,
'@iban' => $bookingData->bankAccount->iban,
'@bic' => $bookingData->bankAccount->bic,
'@kontoinhaber' => $bookingData->bankAccount->holder,
];
}
}
/**
* Builds the participant list section of the payload.
*
* Includes status and personal data for each participant.
*
* @param array $payload The payload array to modify
* @param object $bookingData The booking data object
*/
private function buildParticipantPayload(array &$payload, object $bookingData): void
{
foreach ($bookingData->participants as $index => $participant) {
$payload['teilnehmerliste']['teilnehmer'][] = [
'@id' => $index + 1,
'status' => $bookingData->participantsStatus[$index],
...$participant->toPayload(),
];
}
}
/**
* Builds the services sections of the payload.
*
* Includes additional services, transportation services, and accommodation details
* with participant mappings and quantities.
*
* @param array $payload The payload array to modify
* @param object $bookingData The booking data object
*/
private function buildServicesPayload(array &$payload, object $bookingData): void
{
foreach ($bookingData->additionalServices as $service) {
$payload['zusatzleistungen']['zusatzleistung'][] = [
'@idleistung' => $service->id,
'@anzahl' => count($service->mapping),
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $service->mapping)),
];
}
foreach ($bookingData->transportationServices as $service) {
$payload['beförderungen']['beförderung'][] = [
'@idleistung' => $service->id,
'@anzahl' => count($service->mapping),
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $service->mapping)),
];
}
foreach ($bookingData->rooms as $room) {
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
'@idzimmer' => $room->id,
'@kategorie' => $room->category,
'@idverpflegung' => $room->boardId,
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
'@anzahl' => $room->totalCount,
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $room->mapping)),
];
}
}
/**
* Builds the pickup locations section of the payload.
*
* Only included in payload if there are actual pickup assignments for bus transportation.
*
* @param array $payload The payload array to modify
* @param object $bookingData The booking data object
*/
private function buildPickupPayload(array &$payload, object $bookingData): void
{
if (0 < count($bookingData->pickupsTo)) {
$payload['zustiege']['zustieg'] = [];
foreach ($bookingData->pickupsTo as $pickup) {
$payload['zustiege']['zustieg'][] = [
'@idzustieg' => $pickup->id,
'@anzahl' => count($pickup->mapping),
'@zuordnung' => implode(',', array_map(fn($index) => $index + 1, $pickup->mapping)),
];
}
$participant->insurance = $insurance;
}
}
}
@@ -0,0 +1,496 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Constants;
use App\BusProNet\Model\Booking;
use App\Form\Model\BookingDto;
/**
* Builds API payload structures for BusProNet booking requests.
*
* Transforms booking data into the structured array format expected by the
* BusProNet XML API for both create and update operations.
*/
class BookingPayloadBuilder
{
public function __construct(
private readonly ServiceMappingCollector $mappingCollector,
) {
}
/**
* Builds the base API payload structure with booking information.
*
* Creates the main structure that will be populated with detailed data sections.
*
* @param Booking $bookingData The booking data object
*
* @return array The base payload structure
*/
public function buildBasePayload(Booking $bookingData): array
{
return [
'idbuchung' => $bookingData->id,
'status' => $bookingData->status,
'idagentur' => $bookingData->agencyId,
'idreise' => $bookingData->dateId,
'idpartner' => $bookingData->hotelId,
'anmelder' => $bookingData->applicant->toPayload(),
'zahlung' => [
'@idzahlungsart' => $bookingData->paymentId,
'@bezeichnung' => $bookingData->paymentLabel,
'@art' => $bookingData->paymentType,
],
'teilnehmerliste' => [
'teilnehmer' => [],
],
'zusatzleistungen' => [
'zusatzleistung' => [],
],
'beförderungen' => [
'beförderung' => [],
],
'ferienzielunterbringungen' => [
'ferienzielunterbringung' => [],
],
];
}
/**
* Adds bank account information to the payload if present.
*
* Bank account information is required for direct debit payments.
*
* @param array $payload The payload array to modify
* @param Booking $bookingData The booking data object
*/
public function addBankAccountToPayload(array &$payload, Booking $bookingData): void
{
if (null !== $bookingData->bankAccount) {
$payload['zahlung']['bankverbindung'] = [
'@kreditinstitut' => $bookingData->bankAccount->bankName,
'@iban' => $bookingData->bankAccount->iban,
'@bic' => $bookingData->bankAccount->bic,
'@kontoinhaber' => $bookingData->bankAccount->holder,
];
}
}
/**
* Builds the participant list section of the payload.
*
* Includes status, personal data, and wishes (room remarks, license plate) for each participant.
*
* @param array $payload The payload array to modify
* @param Booking $bookingData The booking data object
* @param array $participantDtos The participant DTOs from the form (for wishes data)
*/
public function buildParticipantPayload(array &$payload, Booking $bookingData, array $participantDtos): void
{
foreach ($bookingData->participants as $index => $participant) {
$participantPayload = [
'@id' => $index + 1,
'status' => $bookingData->participantsStatus[$index],
...$participant->toPayload(),
];
// Add wishes (room remarks, license plate) from form DTO
if (isset($participantDtos[$index])) {
$dto = $participantDtos[$index];
if (null !== $dto->remarksRoom || null !== $dto->licensePlate) {
$wishes = [];
if (null !== $dto->remarksRoom && '' !== trim($dto->remarksRoom)) {
$wishes['unterbringungswunsch'] = $dto->remarksRoom;
}
if (null !== $dto->licensePlate && '' !== trim($dto->licensePlate)) {
$wishes['beförderungswunsch'] = $dto->licensePlate;
}
if (false === empty($wishes)) {
$participantPayload['wünsche'] = $wishes;
}
}
// Add promotional voucher as promotionalCode
$promotionalCode = $this->mappingCollector->getPromoVoucherCodeForParticipant($dto);
if (null !== $promotionalCode) {
$participantPayload['aktionscode'] = $promotionalCode;
}
// Add goodwill voucher as participant-level einloesecode
$goodwillCode = $this->mappingCollector->getGoodwillVoucherCodeForParticipant($dto);
if (null !== $goodwillCode) {
$participantPayload['einloesecode'] = $goodwillCode;
}
}
$payload['teilnehmerliste']['teilnehmer'][] = $participantPayload;
}
}
/**
* Builds the services sections of the payload.
*
* Includes additional services, transportation services, and accommodation details
* with participant mappings and quantities.
*
* @param array $payload The payload array to modify
* @param Booking $bookingData The booking data object
*/
public function buildServicesPayload(array &$payload, Booking $bookingData): void
{
foreach ($bookingData->additionalServices as $service) {
$payload['zusatzleistungen']['zusatzleistung'][] = [
'@idleistung' => $service->id,
'@anzahl' => count($service->mapping),
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
];
}
foreach ($bookingData->transportationServices as $service) {
$payload['beförderungen']['beförderung'][] = [
'@idleistung' => $service->id,
'@anzahl' => count($service->mapping),
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $service->mapping)),
];
}
foreach ($bookingData->rooms as $room) {
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
'@idzimmer' => $room->id,
'@kategorie' => $room->category,
'@idverpflegung' => $room->boardId,
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
'@anzahl' => $room->totalCount,
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $room->mapping)),
];
}
// Add insurances with participant mappings
if (false === empty($bookingData->insurances)) {
$payload['versicherungen']['versicherung'] = [];
foreach ($bookingData->insurances as $insurance) {
if (count($insurance->mapping) > 0) {
$payload['versicherungen']['versicherung'][] = [
'@idversicherung' => $insurance->id,
'@anzahl' => count($insurance->mapping),
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $insurance->mapping)),
];
}
}
}
}
/**
* Builds the pickup locations section of the payload.
*
* Only included in payload if there are actual pickup assignments for bus transportation.
*
* @param array $payload The payload array to modify
* @param Booking $bookingData The booking data object
*/
public function buildPickupPayload(array &$payload, Booking $bookingData): void
{
if (0 < count($bookingData->pickupsOutbound)) {
$payload['zustiege']['zustieg'] = [];
foreach ($bookingData->pickupsOutbound as $pickup) {
$payload['zustiege']['zustieg'][] = [
'@idzustieg' => $pickup->id,
'@anzahl' => count($pickup->mapping),
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $pickup->mapping)),
];
}
}
}
/**
* Builds a create booking request payload.
*
* Generates the array payload structure for creating new bookings through the BusProNet API.
*
* @param BookingDto $bookingDto The booking creation form data
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
*
* @return array The structured payload array for BusProNet API submission
*/
public function buildCreatePayload(BookingDto $bookingDto, string $bookingType): array
{
$firstParticipant = $bookingDto->participants[0];
$payload = [
'buchungsart' => $bookingType,
'status' => $bookingDto->bookingStatus,
'idreise' => $bookingDto->travel->id,
'idpartner' => $bookingDto->travel->hotelId,
'idagentur' => $bookingDto->agencyId,
];
// Add applicant (first participant data)
$payload['anmelder'] = [
'name' => $firstParticipant->lastName,
'vorname' => $firstParticipant->firstName,
'geschlecht' => $firstParticipant->gender ?? '',
'nationalitaet' => $firstParticipant->nationality ?? '',
];
// DO NOT include personId or addressId in create mode
// BPN will automatically match existing customers by exact personal data (name, DOB, address)
// Including IDs would prevent automatic matching and could cause data inconsistencies
if (null !== $firstParticipant->dateOfBirth) {
$payload['anmelder']['geburtsdatum'] = $firstParticipant->dateOfBirth->format('d.m.Y');
}
// Add address for applicant
if (null !== $firstParticipant->address) {
$payload['anmelder']['anschrift'] = $firstParticipant->address->toPayload();
}
if (null !== $firstParticipant->email || null !== $firstParticipant->mobile) {
$payload['anmelder']['kommunikation'] = [];
if (null !== $firstParticipant->email) {
$payload['anmelder']['kommunikation']['email'] = $firstParticipant->email;
}
if (null !== $firstParticipant->mobile) {
$payload['anmelder']['kommunikation']['telefonmobil'] = $firstParticipant->mobile;
}
}
// Determine if this is an inquiry booking for voucher handling
$isInquiryBooking = 'A' === $bookingDto->bookingStatus;
// Add participants
$payload['teilnehmerliste']['teilnehmer'] = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantData = [
'@id' => $index + 1,
'name' => $participant->lastName,
'vorname' => $participant->firstName,
'geschlecht' => $participant->gender ?? '',
'nationalitaet' => $participant->nationality ?? '',
];
// DO NOT include personId or addressId in create mode
// BPN will automatically match existing customers by exact personal data (name, DOB, address)
// Including IDs would prevent automatic matching and could cause data inconsistencies
if (null !== $participant->dateOfBirth) {
$participantData['geburtsdatum'] = $participant->dateOfBirth->format('d.m.Y');
}
// Add address (always include structure, even if empty)
$participantData['anschrift'] = $participant->address?->toPayload() ?? [
'strasse' => null,
'plz' => null,
'ort' => null,
'ortsteil' => null,
'land' => null,
];
// Add contact info for all participants
if (null !== $participant->email || null !== $participant->mobile) {
$participantData['kommunikation'] = [];
if (null !== $participant->email) {
$participantData['kommunikation']['email'] = $participant->email;
}
if (null !== $participant->mobile) {
$participantData['kommunikation']['telefonmobil'] = $participant->mobile;
}
}
// Add body dimensions
if (null !== $participant->height) {
$participantData['sonstiges1'] = $participant->height;
}
if (null !== $participant->weight) {
$participantData['sonstiges2'] = $participant->weight;
}
if (null !== $participant->shoeSize) {
$participantData['sonstiges3'] = $participant->shoeSize;
}
// Add wishes (room remarks and license plate)
if (null !== $participant->remarksRoom || null !== $participant->licensePlate) {
$participantData['wünsche'] = [];
if (null !== $participant->remarksRoom && '' !== trim($participant->remarksRoom)) {
$participantData['wünsche']['unterbringungswunsch'] = $participant->remarksRoom;
}
if (null !== $participant->licensePlate && '' !== trim($participant->licensePlate)) {
$participantData['wünsche']['beförderungswunsch'] = $participant->licensePlate;
}
}
// Add promotional voucher as aktionscode (excluded from inquiry bookings)
if (false === $isInquiryBooking) {
$promotionalCode = $this->mappingCollector->getPromoVoucherCodeForParticipant($participant);
if (null !== $promotionalCode) {
$participantData['aktionscode'] = $promotionalCode;
}
}
// Add goodwill voucher as participant-level einloesecode
// Goodwill vouchers are excluded from inquiry bookings (status='A') but included in final bookings (status='F')
// They can be sent in both validation requests (buchungsart=Anfrage) and commit requests (buchungsart=Buchung)
if (false === $isInquiryBooking) {
$goodwillCode = $this->mappingCollector->getGoodwillVoucherCodeForParticipant($participant);
if (null !== $goodwillCode) {
$participantData['einloesecode'] = $goodwillCode;
}
}
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
}
// Collect and group all services by ID with participant mappings
$serviceMap = $this->mappingCollector->collectServiceMappings($bookingDto);
$transportationMap = $this->mappingCollector->collectTransportationMappings($bookingDto);
$roomMap = $this->mappingCollector->collectRoomMappings($bookingDto);
$pickupMap = $this->mappingCollector->collectPickupMappings($bookingDto);
$insuranceMap = $this->mappingCollector->collectInsuranceMappings($bookingDto);
// Add services using reusable helper methods
$this->addServicesFromMap($payload, 'beförderungen', 'beförderung', '@idleistung', $transportationMap);
$this->addRoomMappingsToPayload($payload, $roomMap, $bookingDto);
$this->addServicesFromMap($payload, 'zusatzleistungen', 'zusatzleistung', '@idleistung', $serviceMap);
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
// Add purchase vouchers (regular purchase vouchers, excluding goodwill vouchers)
// All vouchers (purchase, promotional, goodwill) are excluded from inquiry bookings
if (false === $isInquiryBooking) {
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($bookingDto);
if (false === empty($purchaseVouchers)) {
$payload['gutscheine']['gutschein'] = [];
foreach ($purchaseVouchers as $code) {
$payload['gutscheine']['gutschein'][] = [
'@einloesecode' => $code,
];
}
}
}
// Add payment information
$payload['zahlung'] = [
'@idzahlungsart' => Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod
? Constants::PAYMENT_TYPE_ID_DEBIT
: Constants::PAYMENT_TYPE_ID_TRANSFER,
'@art' => Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod ? 'EINZUG' : 'UEBERWEISUNG',
];
if (Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod && null !== $bookingDto->bankAccount) {
$payload['zahlung']['bankverbindung'] = [
'@iban' => $bookingDto->bankAccount->iban,
'@kontoinhaber' => $bookingDto->bankAccount->accountHolder,
];
}
return $payload;
}
/**
* Adds services from a mapping to the payload.
*
* Generic helper method that converts service ID => participant IDs mappings
* into XML payload structure.
*
* @param array $payload The payload array to modify
* @param string $sectionKey The section key (e.g., 'beförderungen', 'versicherungen')
* @param string $itemKey The item key (e.g., 'beförderung', 'versicherung')
* @param string $idAttributeName The ID attribute name (e.g., '@idleistung', '@idversicherung')
* @param array $serviceMap Map of service ID to participant IDs
*/
public function addServicesFromMap(
array &$payload,
string $sectionKey,
string $itemKey,
string $idAttributeName,
array $serviceMap,
): void {
if (false === empty($serviceMap)) {
$payload[$sectionKey][$itemKey] = [];
foreach ($serviceMap as $serviceId => $participantIds) {
$payload[$sectionKey][$itemKey][] = [
$idAttributeName => $serviceId,
'@anzahl' => count($participantIds),
'@zuordnung' => implode(',', $participantIds),
];
}
}
}
/**
* Adds room mappings with detailed attributes to the payload.
*
* Rooms require special attributes beyond simple service mapping:
* - kategorie (room category code)
* - idverpflegung (board type ID)
* - anreise (arrival date)
* - abreise (departure date)
* - anzahl (number of rooms of this type booked)
*
* @param array $payload The payload array to modify
* @param array $roomMap Map of room ID to participant IDs
* @param BookingDto $bookingDto The booking data for accessing room details and quantities
*/
public function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingDto $bookingDto): void
{
if (empty($roomMap)) {
return;
}
$availableRooms = $bookingDto->travel->getAvailableRooms();
$payload['ferienzielunterbringungen']['ferienzielunterbringung'] = [];
// Build room selection quantity lookup
$roomQuantities = [];
foreach ($bookingDto->roomSelections as $selection) {
if ($selection->quantity > 0) {
$roomQuantities[$selection->id] = $selection->quantity;
}
}
foreach ($roomMap as $roomId => $participantIds) {
$room = $availableRooms[$roomId] ?? null;
if (null === $room) {
continue;
}
$quantity = $roomQuantities[$roomId] ?? 1;
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
'@idzimmer' => $room->id,
'@kategorie' => $room->category,
'@idverpflegung' => $room->boardId,
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
'@anzahl' => $quantity,
'@zuordnung' => implode(',', $participantIds),
];
}
}
/**
* Adds purchase vouchers to the payload.
*
* @param array $payload The payload array to modify
* @param BookingDto $bookingDto The booking data
*/
public function addPurchaseVouchersToPayload(array &$payload, BookingDto $bookingDto): void
{
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($bookingDto);
if (false === empty($purchaseVouchers)) {
$payload['gutscheine']['gutschein'] = [];
foreach ($purchaseVouchers as $code) {
$payload['gutscheine']['gutschein'][] = [
'@einloesecode' => $code,
];
}
}
}
}
@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Travel;
use App\Form\Model\ParticipantDto;
/**
* Processes participant service selections for booking updates.
*
* Handles the assignment and mapping of services (additional, transportation,
* pickup, room, insurance) from participant DTOs to the booking model during
* the update request workflow.
*/
class ParticipantServiceProcessor
{
/**
* Resets all existing participant-to-service mappings to start with a clean slate.
*
* This ensures that service assignments are rebuilt from scratch based on current form selections.
* Includes resetting room mappings to allow room reassignments during edit.
*
* @param Booking $bookingData The booking data object containing services to reset
*/
public function resetServiceMappings(Booking $bookingData): void
{
$servicesToReset = [
...$bookingData->additionalServices,
...$bookingData->transportationServices,
...$bookingData->pickupsOutbound,
...$bookingData->pickupsInbound,
...$bookingData->rooms,
...$bookingData->insurances,
];
foreach ($servicesToReset as $service) {
$service->mapping = [];
}
}
/**
* Processes all services for a single participant.
*
* This orchestrator method handles the complete service assignment workflow for one participant,
* including additional services, transportation services, pickup locations, and room assignments.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
* @param Travel $travelData The travel data containing available services
*/
public function processParticipantServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
{
if (true === $participant->isCanceled()) {
return;
}
$this->processAdditionalServices($participant, $bookingData, $travelData);
$this->processTransportationServices($participant, $bookingData, $travelData);
$this->processPickupLocations($participant, $bookingData);
$this->processRoomAssignment($participant, $bookingData);
$this->processInsurance($participant, $bookingData, $travelData);
}
/**
* Removes services and pickups with no participant mappings.
*
* Cleans up unused services to prevent empty services from being sent to the API.
* This includes additional services, transportation services, pickup locations, and insurances.
*
* @param Booking $bookingData The booking data object to clean up
*/
public function removeUnusedServices(Booking $bookingData): void
{
foreach ($bookingData->additionalServices as $service) {
if (0 === count($service->mapping)) {
unset($bookingData->additionalServices[$service->id]);
}
}
foreach ($bookingData->transportationServices as $service) {
if (0 === count($service->mapping)) {
unset($bookingData->transportationServices[$service->id]);
}
}
foreach ($bookingData->pickupsOutbound as $pickup) {
if (0 === count($pickup->mapping)) {
unset($bookingData->pickupsOutbound[$pickup->id]);
}
}
foreach ($bookingData->pickupsInbound as $pickup) {
if (0 === count($pickup->mapping)) {
unset($bookingData->pickupsInbound[$pickup->id]);
}
}
foreach ($bookingData->insurances as $insurance) {
if (0 === count($insurance->mapping)) {
unset($bookingData->insurances[$insurance->id]);
}
}
}
/**
* Collects additional services from a participant for mapping.
*
* Extracts all additional services (courses, board, rentals, ski pass, rental insurance,
* additional services) from a participant into a flat array of Service objects.
*
* @param ParticipantDto $participant The participant data
*
* @return array<\App\BusProNet\Model\Service> Array of services selected by this participant
*/
private function collectParticipantAdditionalServices(ParticipantDto $participant): array
{
$services = [
...$participant->courses,
...$participant->additionalServices,
...$participant->board,
...$participant->rentals,
];
// Add ski pass if selected (single service, not an array)
if (null !== $participant->skiPass) {
$services[] = $participant->skiPass;
}
// Add rental insurance if selected (single service, not an array)
if (null !== $participant->rentalInsurance) {
$services[] = $participant->rentalInsurance;
}
return $services;
}
/**
* Processes additional services for a participant.
*
* Maps additional services (courses, ski passes, board options, rentals) to the participant.
* Adds new services to the booking if they don't already exist and sets individual pricing.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
* @param Travel $travelData The travel data containing available services
*/
private function processAdditionalServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
{
$servicesToMap = $this->collectParticipantAdditionalServices($participant);
foreach ($servicesToMap as $service) {
if (false === isset($bookingData->additionalServices[$service->id])) {
$serviceToAdd = $travelData->additionalServices[$service->id] ?? null;
if (null !== $serviceToAdd) {
$bookingData->additionalServices[$service->id] = $serviceToAdd;
$bookingData->additionalServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
}
}
$bookingData->additionalServices[$service->id]->mapping[] = $participant->index;
}
}
/**
* Processes transportation services for a participant.
*
* Maps transportation services (both directions: to and from destination) to the participant.
* Adds new transportation services to the booking if they don't already exist.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
* @param Travel $travelData The travel data containing available services
*/
private function processTransportationServices(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
{
foreach ([$participant->transportationOutbound, $participant->transportationInbound] as $service) {
if (false === isset($bookingData->transportationServices[$service->id])) {
$serviceToAdd = $travelData->transportationServices[$service->id] ?? null;
if (null !== $serviceToAdd) {
$bookingData->transportationServices[$service->id] = $serviceToAdd;
$bookingData->transportationServices[$service->id]->individualPrice[$participant->index] = $serviceToAdd->price;
}
}
$bookingData->transportationServices[$service->id]->mapping[] = $participant->index;
}
}
/**
* Processes pickup locations for participants using bus transportation.
*
* Only processes pickup locations for bus transportation services and maps the participant
* to their selected pickup location.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
*/
private function processPickupLocations(ParticipantDto $participant, Booking $bookingData): void
{
// Check if either transportation direction is BUS and pickup is selected
$hasOutboundBus = null !== $participant->transportationOutbound && 'BUS' === $participant->transportationOutbound->subType;
$hasInboundBus = null !== $participant->transportationInbound && 'BUS' === $participant->transportationInbound->subType;
if (($hasOutboundBus || $hasInboundBus) && null !== $selectedPickup = $participant->pickup) {
if (false === isset($bookingData->pickupsOutbound[$selectedPickup->id])) {
$bookingData->pickupsOutbound[$selectedPickup->id] = $selectedPickup;
}
$bookingData->pickupsOutbound[$selectedPickup->id]->mapping[] = $participant->index;
}
}
/**
* Processes room assignment for a participant.
*
* Maps the participant to their assigned room. Allows room reassignment during
* booking edits while maintaining the constraint that participants can only be
* assigned to room types that have already been booked.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
*/
private function processRoomAssignment(ParticipantDto $participant, Booking $bookingData): void
{
if (null === $participant->assignedRoomId) {
return;
}
// Find the room in the existing booking by ID
foreach ($bookingData->rooms as $room) {
if ($room->id === $participant->assignedRoomId) {
$room->mapping[] = $participant->index;
break;
}
}
}
/**
* Processes travel insurance for a participant.
*
* Maps insurance to the participant. Adds new insurances to the booking if they don't exist.
* Insurance can be either individual or package-based, with automatic price-tier adjustment.
*
* IMPORTANT: Excludes synthetic "no insurance" option from BPN transmission.
*
* @param ParticipantDto $participant The participant data from the form
* @param Booking $bookingData The booking data object to update
* @param Travel $travelData The travel data containing available insurances
*/
private function processInsurance(ParticipantDto $participant, Booking $bookingData, Travel $travelData): void
{
// Skip if no insurance or if participant selected "keine Versicherung gewünscht"
if (null === $participant->insurance || $participant->insurance->isNoInsurance()) {
return;
}
$insurance = $participant->insurance;
if (false === isset($bookingData->insurances[$insurance->id])) {
$insuranceToAdd = $travelData->insurances[$insurance->id] ?? null;
if (null !== $insuranceToAdd) {
$bookingData->insurances[$insurance->id] = $insuranceToAdd;
$bookingData->insurances[$insurance->id]->individualPrice[$participant->index] = $insuranceToAdd->price;
}
}
// Only add mapping if insurance exists in booking data
// This can legitimately be false if the insurance is no longer available in current travel data
if (isset($bookingData->insurances[$insurance->id])) {
$bookingData->insurances[$insurance->id]->mapping[] = $participant->index;
}
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Model\Address;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Communication;
use App\Form\Model\ParticipantDto;
/**
* Synchronizes personal data between form DTOs and booking models.
*
* Handles the update of participant personal data, addresses, and communication
* information from form submissions to the booking model during update operations.
*/
class PersonalDataSynchronizer
{
/**
* Updates participant personal data from form input.
*
* Processes all active participants (status 'F' or 'A'). Skips canceled participants (status 'S').
* Updates all personal data fields and communication information.
*
* IMPORTANT: The applicant's address must never be modified. This method updates
* participant addresses independently to ensure applicant data remains intact.
*
* @param array<ParticipantDto> $participants The participants array from the form
* @param Booking $bookingData The booking data object to update
*/
public function updateParticipantPersonalData(array $participants, Booking $bookingData): void
{
foreach ($participants as $participant) {
// Skip canceled participants (status 'S')
if ('S' === $participant->status) {
continue;
}
$bookingData->participants[$participant->index]->firstName = $participant->firstName;
$bookingData->participants[$participant->index]->name = $participant->lastName;
$bookingData->participants[$participant->index]->dateOfBirth = $participant->dateOfBirth;
$bookingData->participants[$participant->index]->gender = $participant->gender;
$bookingData->participants[$participant->index]->nationality = $participant->nationality;
$bookingData->participants[$participant->index]->height = $participant->height;
$bookingData->participants[$participant->index]->weight = $participant->weight;
$bookingData->participants[$participant->index]->shoeSize = $participant->shoeSize;
// Update address if provided
// Create new Address instance to avoid modifying any shared object references
if (null !== $participant->address) {
$newAddress = new Address();
$newAddress->street = $participant->address->street;
$newAddress->postCode = $participant->address->postCode;
$newAddress->city = $participant->address->city;
$newAddress->district = $participant->address->district;
$newAddress->country = $participant->address->country;
$bookingData->participants[$participant->index]->address = $newAddress;
}
if ($participant->email || $participant->mobile) {
if (null === $bookingData->participants[$participant->index]->communication) {
$bookingData->participants[$participant->index]->communication = new Communication();
}
$bookingData->participants[$participant->index]->communication->email = $participant->email;
$bookingData->participants[$participant->index]->communication->mobile = $participant->mobile;
}
}
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
/**
* Transforms raw pickup planning data into structured travel-indexed pickup information.
*
* Processes JSON payload from the CMS pickup planning endpoint, filtering for outbound
* trips (HIN direction) and grouping pickups by travel code. Each pickup includes city,
* location, and datetime, sorted chronologically per travel code.
*/
class PickupPlanningTransformer
{
/**
* @param array<int, array<string, mixed>> $payload
*
* @return array<string, array<int, array<string, string>>>
*/
public function transform(array $payload): array
{
$result = [];
foreach ($payload as $item) {
if ('HIN' !== ($item['PlanungsArt'] ?? null)) {
continue;
}
$travelCodes = $item['Reisecodes'] ?? [];
if (0 === count($travelCodes)) {
continue;
}
$dateTime = \DateTimeImmutable::createFromFormat('d.m.Y H:i', $item['Zeit'] ?? '');
foreach ($travelCodes as $code) {
$pickup = [
'city' => $item['Zustieg/Ausstieg Ort'] ?? '',
'location' => $item['Zustieg/Ausstieg Strasse'] ?? '',
'datetime' => false !== $dateTime ? $dateTime->format('Y-m-d H:i') : '',
];
$key = sprintf('%s|%s|%s', $pickup['city'], $pickup['location'], $pickup['datetime']);
if (false === isset($result[$code][$key])) {
$result[$code][$key] = $pickup;
}
}
}
foreach ($result as $code => $pickups) {
$pickups = array_values($pickups);
usort($pickups, static fn (array $a, array $b): int => $a['datetime'] <=> $b['datetime']);
$result[$code] = $pickups;
}
return $result;
}
}
@@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
/**
* Collects service-to-participant mappings for BusProNet API payloads.
*
* Extracts and groups participant service selections into structured maps
* keyed by service ID with arrays of participant IDs (1-based for API).
*/
class ServiceMappingCollector
{
/**
* Collects room mappings.
*
* Groups participants by their assigned room ID.
*
* @return array<string, array<int>> Map of room ID to participant IDs
*/
public function collectRoomMappings(BookingDto $bookingDto): array
{
$roomMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
if (null !== $participant->assignedRoomId) {
$roomMap[$participant->assignedRoomId][] = $participantId;
}
}
return $roomMap;
}
/**
* Collects service mappings for the booking request.
*
* Groups board, ski passes, rentals, rental insurance, courses, parking, and additional services
* by service ID with their participant assignments (1-based).
*
* @return array<string, array<int>> Map of service ID to participant IDs
*/
public function collectServiceMappings(BookingDto $bookingDto): array
{
$serviceMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
// Board services
foreach ($participant->board as $board) {
$serviceMap[$board->id][] = $participantId;
}
// Ski pass
if (null !== $participant->skiPass) {
$serviceMap[$participant->skiPass->id][] = $participantId;
}
// Rentals
foreach ($participant->rentals as $rental) {
$serviceMap[$rental->id][] = $participantId;
}
// Rental insurance
if (null !== $participant->rentalInsurance) {
$serviceMap[$participant->rentalInsurance->id][] = $participantId;
}
// Courses
foreach ($participant->courses as $course) {
$serviceMap[$course->id][] = $participantId;
}
// Parking service (for self-organized transportation)
if (true === $participant->parking && null !== $participant->parkingService) {
$serviceMap[$participant->parkingService->id][] = $participantId;
}
// Additional services
foreach ($participant->additionalServices as $service) {
$serviceMap[$service->id][] = $participantId;
}
}
return $serviceMap;
}
/**
* Collects transportation service mappings.
*
* @return array<string, array<int>> Map of transportation service ID to participant IDs
*/
public function collectTransportationMappings(BookingDto $bookingDto): array
{
$transportationMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
if (null !== $participant->transportationOutbound) {
$transportationMap[$participant->transportationOutbound->id][] = $participantId;
}
if (null !== $participant->transportationInbound) {
$transportationMap[$participant->transportationInbound->id][] = $participantId;
}
}
return $transportationMap;
}
/**
* Collects pickup location mappings.
*
* Collects the unified pickup selection which applies to both directions.
* The API receives this as outbound pickup data.
*
* @return array<string, array<int>> Map of pickup ID to participant IDs
*/
public function collectPickupMappings(BookingDto $bookingDto): array
{
$pickupMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
if (null !== $participant->pickup) {
$pickupMap[$participant->pickup->id][] = $participantId;
}
}
return $pickupMap;
}
/**
* Collects insurance mappings.
*
* CRITICAL: Insurance data is only included in CREATE flow, not in UPDATE flow.
* IMPORTANT: Excludes synthetic "no insurance" option from BPN transmission.
*
* @return array<string, array<int>> Map of insurance ID to participant IDs
*/
public function collectInsuranceMappings(BookingDto $bookingDto): array
{
$insuranceMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
// Exclude synthetic "keine Versicherung gewünscht" option from BPN XML
if (null !== $participant->insurance && false === $participant->insurance->isNoInsurance()) {
$insuranceMap[$participant->insurance->id][] = $participantId;
}
}
return $insuranceMap;
}
/**
* Collects all purchase vouchers from participants.
*
* Removes duplicates and empty values, returning unique redemption codes
* for aggregation into the <gutscheine> collection in the booking payload.
*
* IMPORTANT: Goodwill vouchers (Kulanz) are excluded from this collection.
* They are added per participant as <aktionscode> instead.
*
* @return string[] Array of unique redemption codes (excluding Kulanz)
*/
public function collectPurchaseVouchers(BookingDto $bookingDto): array
{
$vouchers = [];
foreach ($bookingDto->participants as $participant) {
if (null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
// Exclude goodwill vouchers
if (false === $participant->hasGoodwillVoucher()) {
$vouchers[] = trim($participant->purchaseVoucherCode);
}
}
}
// Remove duplicates (in case multiple participants enter same code)
return array_unique($vouchers);
}
/**
* Gets the aktionscode (promotional voucher code) for a participant.
*
* Returns the promotional voucher code if present.
* Goodwill vouchers are handled separately via getGoodwillVoucherCodeForParticipant().
*
* @return string|null The aktionscode to use, or null if none
*/
public function getPromoVoucherCodeForParticipant(ParticipantDto $participant): ?string
{
// Only return promotional voucher code (goodwill vouchers are handled separately)
if (null !== $participant->promoVoucherCode && '' !== trim($participant->promoVoucherCode)) {
return trim($participant->promoVoucherCode);
}
return null;
}
/**
* Extracts goodwill voucher code for a participant.
*
* Goodwill vouchers are sent as <einloesecode> per participant (not in aggregate),
* allowing them to coexist with promotional codes.
*/
public function getGoodwillVoucherCodeForParticipant(ParticipantDto $participant): ?string
{
if ($participant->hasGoodwillVoucher() && null !== $participant->purchaseVoucherCode && '' !== trim($participant->purchaseVoucherCode)) {
return trim($participant->purchaseVoucherCode);
}
return null;
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Exception;
/**
* Thrown when an API operation exceeds its timeout limit.
*/
class TimeoutException extends ApiClientException
{
}
-6
View File
@@ -4,8 +4,6 @@ declare(strict_types=1);
namespace App\BusProNet\Model;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Represents a physical address with street, postal code, city, and country information.
*
@@ -15,18 +13,14 @@ use Symfony\Component\Validator\Constraints as Assert;
*/
class Address
{
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
public ?string $street = null;
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
public ?string $postCode = null;
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
public ?string $city = null;
public ?string $district = null;
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
public ?string $country = null;
/**
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Represents the result of parsing age constraint data from XML.
*
* This DTO holds the parsed age constraint information including both absolute age
* constraints and birth year constraints, along with metadata for extensibility.
*/
class AgeConstraintResult
{
public function __construct(
public readonly string $type,
public readonly ?int $ageFrom = null,
public readonly ?int $ageTo = null,
public readonly ?int $birthYearFrom = null,
public readonly ?int $birthYearTo = null,
public readonly array $metadata = [],
public readonly ?string $rawData = null,
) {
}
public function hasAgeConstraints(): bool
{
return null !== $this->ageFrom || null !== $this->ageTo;
}
public function hasBirthYearConstraints(): bool
{
return null !== $this->birthYearFrom || null !== $this->birthYearTo;
}
public function isEmpty(): bool
{
return false === $this->hasAgeConstraints() && false === $this->hasBirthYearConstraints();
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
final readonly class Agency
{
public function __construct(
public int $id,
public string $name,
public string $code,
public ?string $street = null,
public ?string $postCode = null,
public ?string $city = null,
public ?string $phone = null,
) {
}
}
+67 -7
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\BusProNet\Model;
use App\BusProNet\Constants;
/**
* Represents a booking with participants, services, and pricing information.
*
@@ -39,9 +41,10 @@ class Booking
public array $transportationServices = [];
public array $additionalServices = [];
public array $rooms = [];
public array $pickupsTo = [];
public array $pickupsFro = [];
public array $pickupsOutbound = [];
public array $pickupsInbound = [];
public array $surcharges = [];
public array $insurances = [];
public ?int $invoiceNumber = null;
public ?float $totalPrice = null;
public ?string $travelInfoUrl = null;
@@ -106,7 +109,7 @@ class Booking
* Retrieves transportation service for a specific participant and direction.
*
* Finds the transportation service that matches the participant index
* and travel direction (e.g., 'H' for outbound, 'R' for return).
* and travel direction (e.g., 'H' for outbound, 'R' for inbound).
*
* @param int $participantIndex The participant index to search for
* @param string $direction The travel direction ('H' or 'R')
@@ -141,7 +144,7 @@ class Booking
*/
public function getPickupForParticipant(int $participantIndex): ?Pickup
{
foreach ($this->pickupsTo as $pickup) {
foreach ($this->pickupsOutbound as $pickup) {
if (in_array($participantIndex, $pickup->mapping)) {
return $pickup;
}
@@ -150,6 +153,63 @@ class Booking
return null;
}
/**
* Gets the insurance assigned to a specific participant.
*
* @param int $participantIndex The participant index (0-based)
*
* @return Insurance|null The assigned insurance or null if none assigned
*/
public function getInsuranceForParticipant(int $participantIndex): ?Insurance
{
foreach ($this->insurances as $insurance) {
if (in_array($participantIndex, $insurance->mapping ?? [], true)) {
return $insurance;
}
}
return null;
}
/**
* Retrieves ski pass for a specific participant.
*
* Since each participant can only have one ski pass, this method returns
* the single ski pass assigned to the participant or null if none is assigned.
*
* @param int $participantIndex The participant index to search for
*
* @return Service|null The participant's ski pass or null if not found
*/
public function getSkiPassForParticipant(int $participantIndex): ?Service
{
$skiPasses = $this->getAdditionalServicesByGroup('SPA');
foreach ($skiPasses as $service) {
if (in_array($participantIndex, $service->mapping)) {
return $service;
}
}
return null;
}
/**
* Retrieves rental insurance for a specific participant.
*/
public function getRentalInsuranceForParticipant(int $participantIndex): ?Service
{
$rentalInsurances = $this->getAdditionalServicesByGroup(Constants::TOKEN_RENTAL_INSURANCE);
foreach ($rentalInsurances as $service) {
if (in_array($participantIndex, $service->mapping)) {
return $service;
}
}
return null;
}
/**
* Retrieves room assignment for a specific participant.
*
@@ -189,7 +249,7 @@ class Booking
foreach ([...$this->transportationServices, ...$this->additionalServices] as $service) {
if ((in_array($participantIndex, $service->mapping) || true === $service->mandatory)
&& isset($service->individualPrice[$participantIndex])) {
$price += $service->individualPrice[$participantIndex];
$price += $service->individualPrice[$participantIndex] ?? 0;
}
}
@@ -197,13 +257,13 @@ class Booking
foreach ($this->surcharges as $surcharge) {
if (in_array($participantIndex, $surcharge->mapping)
&& isset($surcharge->individualPrice[$participantIndex])) {
$price += $surcharge->individualPrice[$participantIndex];
$price += $surcharge->individualPrice[$participantIndex] ?? 0;
}
}
$room = $this->getRoomForParticipant($participantIndex);
if (null !== $room && isset($room->individualPrice[$participantIndex])) {
$price += $room->individualPrice[$participantIndex];
$price += $room->individualPrice[$participantIndex] ?? 0;
}
return $price;
+105
View File
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Represents a successful API response from a booking request.
*
* Response structure for successful requests:
* - <buchung>möglich</buchung> = inquiry validation successful
* - <buchung>erfolgt</buchung> = booking creation successful
*
* Error responses return Notification objects instead (typ="HINWEIS").
*/
class BookingResponse
{
/**
* @param string $status Booking status (möglich|erfolgt)
* @param string|null $transactionNumber Transaction number (vorgang)
* @param array<int, PriceItem> $priceItems Individual price items from response
* @param float|null $totalPrice Total price (gesamtpreis)
* @param PaymentTerms|null $paymentTerms Payment terms (anzahlung/restzahlung)
*/
public function __construct(
public readonly string $status,
public readonly ?string $transactionNumber = null,
public readonly array $priceItems = [],
public readonly ?float $totalPrice = null,
public readonly ?PaymentTerms $paymentTerms = null,
public readonly ?string $message = null,
) {
}
/**
* Returns true if the inquiry validation was successful.
*/
public function isInquiryValid(): bool
{
return 'möglich' === $this->status;
}
/**
* Returns true if the booking was successfully created.
*/
public function isBookingSuccessful(): bool
{
return 'erfolgt' === $this->status;
}
/**
* Gets voucher discounts from price items (AKTION and KULANZGUTSCHEIN types).
*
* Promotional vouchers appear as negative price items with art="AKTION".
* Goodwill vouchers appear as negative price items with art="KULANZGUTSCHEIN".
*
* @return array<PriceItem> Price items representing voucher discounts
*/
public function getVoucherDiscountsFromPrices(): array
{
return array_filter(
$this->priceItems,
fn (PriceItem $item) => \in_array($item->type, ['AKTION', 'KULANZGUTSCHEIN'], true)
);
}
/**
* Gets total discount from price-based vouchers (promotional and goodwill).
*
* Returns the absolute sum of negative price items with AKTION or KULANZGUTSCHEIN types.
*/
public function getVoucherDiscountFromPrices(): float
{
$discount = 0.0;
foreach ($this->getVoucherDiscountsFromPrices() as $item) {
// Price items for vouchers are negative, so we take absolute value
$discount += abs($item->totalPrice);
}
return round($discount, 2);
}
/**
* Gets total discount from purchase vouchers in payment terms.
*/
public function getPurchaseVoucherDiscount(): float
{
return $this->paymentTerms?->getPurchaseVoucherDiscount() ?? 0.0;
}
/**
* Gets total voucher discount from all sources.
*
* Combines discounts from:
* - Price items (art="AKTION" for promotional, art="KULANZGUTSCHEIN" for goodwill)
* - Payment terms (kaufgutschein elements for purchase vouchers)
*/
public function getTotalVoucherDiscount(): float
{
return round(
$this->getVoucherDiscountFromPrices() + $this->getPurchaseVoucherDiscount(),
2
);
}
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
class ContactFormResponse
{
public function __construct(
public readonly int $addressId,
public readonly int $personId,
public readonly bool $isNewRecord,
) {
}
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
class ContactFormSubmission
{
public ?string $lastName = null;
public ?string $firstName = null;
public ?string $gender = null;
public ?string $street = null;
public ?string $zipCode = null;
public ?string $city = null;
public ?string $email = null;
public ?string $phone = null;
}
+1 -5
View File
@@ -14,10 +14,6 @@ class CrmAttributes
{
public ?array $selectionGroups = null;
public ?array $crmActions = null;
public bool $admin = false;
public bool $manager = false;
public bool $houseManager = false;
public bool $teamer = false;
public ?string $hotelCode = null;
public array $hotelCodes = [];
public array $roles = [];
}
+205
View File
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
/**
* Represents an insurance product or package with pricing, eligibility, and booking constraints.
*
* This class handles both individual insurance products and insurance packages,
* including pricing tiers, age restrictions, travel date validity, booking windows, and coverage details.
*/
class Insurance
{
/**
* Special ID for the synthetic "no insurance" option.
*
* This option is injected into the insurance selection list to force explicit user choice
* for legal compliance. When selected, it satisfies validation requirements but transmits
* no insurance data to the BPN API.
*/
public const NO_INSURANCE_ID = '0';
#[Groups(['api:single', 'api:list'])]
public ?string $id = null;
#[Groups(['api:single', 'api:list'])]
public ?string $code = null;
#[Groups(['api:single', 'api:list'])]
public ?string $label = null;
#[Groups(['api:single', 'api:list'])]
public ?string $subType = null;
#[Groups(['api:single', 'api:list'])]
public ?float $price = null;
#[Groups(['api:single', 'api:list'])]
public bool $familyInsurance = false;
#[Groups(['api:single', 'api:list'])]
public bool $package = false;
#[Groups(['api:single', 'api:list'])]
public bool $complementary = false;
#[Groups(['api:single', 'api:list'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $travelDateFrom = null;
#[Groups(['api:single', 'api:list'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $travelDateTo = null;
#[Groups(['api:single', 'api:list'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $bookingDateFrom = null;
#[Groups(['api:single', 'api:list'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $bookingDateTo = null;
#[Groups(['api:single', 'api:list'])]
public ?int $ageFrom = null;
#[Groups(['api:single', 'api:list'])]
public ?int $ageTo = null;
#[Groups(['api:single', 'api:list'])]
public ?float $travelPriceFrom = null;
#[Groups(['api:single', 'api:list'])]
public ?float $travelPriceTo = null;
#[Groups(['api:single', 'api:list'])]
public ?int $travelDurationFrom = null;
#[Groups(['api:single', 'api:list'])]
public ?int $travelDurationTo = null;
#[Groups(['api:single', 'api:list'])]
public ?string $urlInfo = null;
#[Groups(['api:single', 'api:list'])]
public ?string $urlProductInfo = null;
#[Groups(['api:single', 'api:list'])]
public ?string $urlTerms = null;
/**
* @var array<Insurance> Insurance products contained in this package (only for packages)
*/
#[Groups(['api:single', 'api:list'])]
public array $containedInsurances = [];
/**
* @var array<string|int> IDs of insurance products contained in this package (only for packages)
*/
#[Groups(['api:single', 'api:list'])]
public array $containedInsuranceIds = [];
/**
* @var array<int> Participant indices assigned to this insurance (booking data only)
*/
public array $mapping = [];
/**
* @var array<int, float> Individual prices per participant index (booking data only)
*/
public array $individualPrice = [];
/**
* Returns the subType, with virtual 'PKG' subType for insurance packages.
*
* @return string|null The subType (RRV, PAK, OHN, PKG) or null
*/
public function getSubType(): ?string
{
if (true === $this->package) {
return 'PKG'; // Virtual subType for all insurance packages
}
return $this->subType;
}
/**
* Returns all info URLs from this insurance or its contained insurances (for packages).
*
* @return array<string> Array of info URLs
*/
public function getAllUrlsInfo(): array
{
if (true === $this->package) {
return $this->collectUrlsFromContainedInsurances('urlInfo');
}
return null !== $this->urlInfo ? [$this->urlInfo] : [];
}
/**
* Returns all product info URLs from this insurance or its contained insurances (for packages).
*
* @return array<string> Array of product info URLs
*/
public function getAllUrlsProductInfo(): array
{
if (true === $this->package) {
return $this->collectUrlsFromContainedInsurances('urlProductInfo');
}
return null !== $this->urlProductInfo ? [$this->urlProductInfo] : [];
}
/**
* Returns all terms URLs from this insurance or its contained insurances (for packages).
*
* @return array<string> Array of terms URLs
*/
public function getAllUrlsTerms(): array
{
if (true === $this->package) {
return $this->collectUrlsFromContainedInsurances('urlTerms');
}
return null !== $this->urlTerms ? [$this->urlTerms] : [];
}
/**
* Collects URLs from contained insurances for a specific URL property.
*
* @param string $urlProperty The URL property name to collect (urlInfo, urlProductInfo, urlTerms)
*
* @return array<string> Array of non-null URLs from contained insurances
*/
private function collectUrlsFromContainedInsurances(string $urlProperty): array
{
$urls = [];
foreach ($this->containedInsurances as $containedInsurance) {
if (null !== $containedInsurance->{$urlProperty}) {
$urls[] = $containedInsurance->{$urlProperty};
}
}
return array_values(array_unique($urls));
}
/**
* Checks if this is the synthetic "no insurance" option.
*
* The "no insurance" option is a UI construct used to force explicit user choice.
* It should be excluded from BPN API transmission.
*
* @return bool True if this is the "no insurance" option
*/
public function isNoInsurance(): bool
{
return self::NO_INSURANCE_ID === $this->id;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Represents payment terms from the booking response.
*
* Contains deposit/final payment details and any applied purchase vouchers.
*/
class PaymentTerms
{
/**
* @param array<array{nummer: string, betrag: float}> $appliedPurchaseVouchers
*/
public function __construct(
public readonly ?float $depositAmount = null,
public readonly ?string $depositDate = null,
public readonly ?float $finalPaymentAmount = null,
public readonly ?string $finalPaymentDate = null,
public readonly array $appliedPurchaseVouchers = [],
) {
}
/**
* Gets the total discount from purchase vouchers.
*/
public function getPurchaseVoucherDiscount(): float
{
$total = 0.0;
foreach ($this->appliedPurchaseVouchers as $voucher) {
$total += $voucher['betrag'];
}
return round($total, 2);
}
}
+37 -2
View File
@@ -27,7 +27,7 @@ class PersonalData
public ?string $name = null;
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['Default', 'personal_data'])]
public string $firstName = '';
public ?string $firstName = '';
public ?string $salutation = null;
public ?string $title = null;
@@ -39,18 +39,30 @@ class PersonalData
public ?\DateTimeImmutable $dateOfBirth = null;
public ?string $remarks = null;
// Wishes (unterbringungswunsch, beförderungswunsch)
public ?string $remarksRoom = null;
public ?string $licensePlate = null;
#[Assert\Valid(groups: ['personal_data'])]
public Address $address;
#[Assert\Valid(groups: ['personal_data'])]
public Communication $communication;
public array $roles = [];
public array $hotelCodes = [];
public function __construct()
{
$this->address = new Address();
$this->communication = new Communication();
}
public function getFullName(): string
{
return sprintf('%s %s', $this->firstName, $this->name);
}
/**
* Converts the personal data to API payload format.
*
@@ -85,6 +97,25 @@ class PersonalData
];
}
public static function fromContactFormSubmission(ContactFormSubmission $dto): self
{
$gender = null !== $dto->gender ? strtoupper($dto->gender) : null;
if ('F' === $gender) {
$gender = 'W';
}
$instance = new self();
$instance->name = $dto->lastName;
$instance->firstName = $dto->firstName;
$instance->gender = $gender;
$instance->address->street = $dto->street;
$instance->address->postCode = $dto->zipCode;
$instance->address->city = $dto->city;
$instance->communication->email = $dto->email;
$instance->communication->phone = $dto->phone;
return $instance;
}
/**
* Extracts user claims for authentication and profile information.
*
@@ -96,19 +127,22 @@ class PersonalData
public function getClaims(): array
{
return [
'id' => $this->personId,
'email' => $this->communication->email,
'roles' => $this->roles,
'profile' => [
'first_name' => $this->firstName,
'last_name' => $this->name,
'title' => $this->title,
'salutation' => $this->salutation,
'gender' => $this->gender,
'date_of_birth' => $this->dateOfBirth?->format('Y-m-d'),
'nationality' => $this->nationality,
'address' => [
'street' => $this->address->street,
'postcode' => $this->address->postCode,
'city' => $this->address->city,
'disctrict' => $this->address->district,
'district' => $this->address->district,
'country' => $this->address->country,
],
'communication' => [
@@ -117,6 +151,7 @@ class PersonalData
'phone' => $this->communication->phone,
'newsletter' => $this->communication->newsletter,
],
'hotel_codes' => $this->hotelCodes,
'height' => $this->height,
'weight' => $this->weight,
'shoe_size' => $this->shoeSize,
+49
View File
@@ -41,4 +41,53 @@ class Pickup
#[Groups(['api:booking'])]
public array $mapping = [];
/**
* Gets the formatted label for the pickup location without pricing.
*
* Creates user-friendly labels following the pattern:
* - Primary format: "City (Street)" if street is available
* - Fallback format: "City" if no street information
*
* @return string The formatted pickup location label
*/
public function getLabel(): string
{
$label = $this->city ?? '';
if (null !== $this->street && '' !== trim($this->street)) {
$label .= ' ('.$this->street.')';
}
return $label;
}
/**
* Gets the formatted label with pricing information.
*
* Formats the pickup label with price display:
* - Zero/null price: Just the label without price suffix
* - Positive price: "Label (€X,XX)" as surcharge
* - Negative price: "Label (-X,XX€ Rabatt)" as discount
*
* @return string The formatted pickup label with pricing
*/
public function getLabelWithPrice(): string
{
$label = $this->getLabel();
if (null === $this->price) {
return $label;
}
if (0.0 === $this->price) {
return sprintf('%s (inkl.)', $label);
}
if ($this->price < 0) {
return sprintf('%s (-%s€ Rabatt)', $label, number_format(abs($this->price), 2, ',', '.'));
}
return sprintf('%s (€%s)', $label, number_format($this->price, 2, ',', '.'));
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Represents a single price item from the booking response.
*/
class PriceItem
{
public function __construct(
public readonly int $position,
public readonly string $type,
public readonly ?string $subType,
public readonly string $label,
public readonly ?string $dateFrom,
public readonly ?string $dateTo,
public readonly int $quantity,
public readonly ?string $assignment,
public readonly float $unitPrice,
public readonly float $totalPrice,
public readonly ?string $id,
) {
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Promo voucher validation response from BPN API.
*
* Represents a validated promotional voucher (Aktionsgutschein) applied per participant.
* These vouchers provide discounts either as fixed amounts or percentages.
*/
final readonly class PromoVoucher
{
public const APPLICABILITY_PER_PERSON = 'P';
public const APPLICABILITY_PER_BOOKING = 'B';
public function __construct(
public string $code,
public string $description,
public string $applicability,
public float $discountAmount,
public float $discountPercentage,
public float $minimumPrice,
) {
}
public function isPerPerson(): bool
{
return self::APPLICABILITY_PER_PERSON === $this->applicability;
}
public function isPerBooking(): bool
{
return self::APPLICABILITY_PER_BOOKING === $this->applicability;
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
final readonly class PromoVoucherValidationResult
{
public function __construct(
public bool $isValid,
public ?PromoVoucher $voucher = null,
public ?string $errorMessage = null,
public ?int $errorCode = null,
) {
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Purchase voucher validation response from BPN API.
*
* Represents a validated purchase voucher (Gutschein) with redemption code.
* These vouchers have a remaining balance that can be redeemed against bookings.
* The voucher reduces the booking price by its balance until depleted.
*
* Purchase vouchers can be regular purchased vouchers or goodwill vouchers (Kulanz)
* issued to customers for complaints or other reasons.
*/
final readonly class PurchaseVoucher
{
public const TYPE_PURCHASE = 'Kauf';
public const TYPE_GOODWILL = 'Kulanz';
public function __construct(
public string $voucherNumber,
public string $redemptionCode,
public int $voucherId,
public float $remainingBalance,
public string $type,
) {
}
/**
* Checks if this voucher is a purchase voucher.
*
* Purchase vouchers (Kauf) are aggregated into <gutscheine>.
*/
public function isPurchase(): bool
{
return self::TYPE_PURCHASE === $this->type;
}
/**
* Checks if this voucher is a goodwill voucher.
*
* Goodwill vouchers (Kulanz) must be treated as promo vouchers and added
* per participant as <aktionscode>, not aggregated into <gutscheine>.
*/
public function isGoodwill(): bool
{
return self::TYPE_GOODWILL === $this->type;
}
}
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
final readonly class PurchaseVoucherValidationResult
{
public function __construct(
public bool $isValid,
public ?PurchaseVoucher $voucher = null,
public ?string $errorMessage = null,
public ?int $errorCode = null,
) {
}
}
+17
View File
@@ -17,6 +17,9 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
*/
class Room
{
public const SELECTION_TYPE_BY_PAX = 'by_pax';
public const SELECTION_TYPE_BY_ROOM = 'by_room';
#[Groups(['api:list', 'api:single'])]
public ?int $id = null;
@@ -72,4 +75,18 @@ class Room
#[Groups(['booking'])]
public array $individualPrice = [];
/**
* Determines the selection type of the room based on its label.
*
* @return string Either self::SELECTION_TYPE_BY_PAX or self::SELECTION_TYPE_BY_ROOM
*/
public function getSelectionType(): string
{
if (1 === preg_match('/bett/i', $this->label)) {
return self::SELECTION_TYPE_BY_PAX;
}
return self::SELECTION_TYPE_BY_ROOM;
}
}
+34
View File
@@ -72,4 +72,38 @@ class Service
#[Groups(['api:single', 'api:list'])]
public ?string $direction = null;
#[Groups(['api:single', 'api:list'])]
public ?int $ageFrom = null;
#[Groups(['api:single', 'api:list'])]
public ?int $ageTo = null;
#[Groups(['api:single', 'api:list'])]
public ?int $birthYearFrom = null;
#[Groups(['api:single', 'api:list'])]
public ?int $birthYearTo = null;
#[Groups(['api:single', 'api:list'])]
public ?string $ageConstraintType = null;
#[Groups(['api:single'])]
public ?array $ageConstraintMetadata = null;
#[Groups(['api:single'])]
public ?string $rawAgeConstraintData = null;
#[Groups(['api:single', 'api:list'])]
public ?string $description = null;
/**
* Indicates whether this service should be included in insurance eligibility price calculation.
*
* Maps to XML attribute 'versicherungsberechnung' (J=true, N=false).
* When false, this service's price is excluded when determining which insurance
* tier is appropriate for a participant.
*/
#[Groups(['api:single', 'api:list'])]
public bool $includeInInsuranceCalculation = true;
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Response model for service availability API calls.
*
* Contains both service-level availability data and travel-level
* booking status information from the VERFUEGBARKEIT endpoint.
*/
class ServiceAvailabilityResponse
{
/**
* @param array<int, Availability> $services Service availability data indexed by service ID
* @param array<string> $allowedBookingStatus Allowed booking status codes (e.g., ['F', 'O', 'A'])
* @param string|null $travelStatus Travel status from API (e.g., 'frei', 'anfrage')
*/
public function __construct(
private readonly array $services,
public readonly array $allowedBookingStatus = [],
public readonly ?string $travelStatus = null,
) {
}
/**
* @return array<int, Availability>
*/
public function getServices(): array
{
return $this->services;
}
}
+161 -45
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\BusProNet\Model;
use App\BusProNet\Constants;
use App\BusProNet\Traits\SortByPriceTrait;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
@@ -18,12 +19,16 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
*/
class Travel
{
use SortByPriceTrait;
#[Groups(['api:list', 'api:single'])]
public ?int $id = null;
#[Groups(['api:list', 'api:single'])]
public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
public ?string $productCode = null;
#[Groups(['api:list', 'api:single'])]
public ?string $label = null;
@@ -44,6 +49,22 @@ class Travel
#[Groups(['api:list', 'api:single'])]
public ?string $type = null;
/**
* Travel booking status from BusProNet API.
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'.
*/
#[Groups(['api:single'])]
public ?string $status = null;
/**
* Allowed booking status codes from buchungstatusmoeglich attribute.
* Array of status codes (F, A, O) indicating which booking types are permitted.
*
* @var array<string>
*/
#[Groups(['api:single'])]
public array $allowedBookingStatus = [];
#[Groups(['api:list', 'api:single'])]
public ?float $priceFrom = null;
@@ -63,10 +84,10 @@ class Travel
public bool $transportationServicesMutable = true;
#[Groups(['api:single'])]
public array $pickupsTo = [];
public array $pickupsOutbound = [];
#[Groups(['api:single'])]
public array $pickupsFro = [];
public array $pickupsInbound = [];
#[Groups(['api:single'])]
public bool $pickupsMutable = true;
@@ -87,53 +108,70 @@ class Travel
public ?Guide $guide = null;
/**
* Retrieves additional services filtered by group and availability.
*
* Filters additional services based on the provided group(s) and optionally
* by availability. Services are sorted alphabetically by label.
*
* @param mixed $group The service group(s) to filter by
* @param bool $availableOnly Whether to include only available services
*
* @return array<Service> The filtered and sorted services array
* @var array<Insurance> Available insurances for this travel package
*/
public function getAdditionalServicesByGroup(mixed $group, bool $availableOnly = true): array
#[Groups(['api:single'])]
public array $insurances = [];
/**
* Retrieves additional services filtered by subtype and optionally by travel date range.
*
* Filters additional services based on the provided subtype(s) and optionally whether
* their date range overlaps with the travel dates. Services with null dates are
* considered always available when date filtering is enabled.
* Services are sorted by price in ascending order.
*
* Availability filtering is NOT applied here - it happens at the form/UI level where
* unavailable services are shown as readonly with appropriate tooltips.
*
* @param mixed $subTypes The service subtype(s) to filter by
* @param bool $filterByTravelDateRange Whether to filter by travel date range overlap
*
* @return array<int, Service> The filtered and sorted services array
*/
public function getAdditionalServicesBySubTypes(mixed $subTypes, bool $filterByTravelDateRange = false): array
{
$group = (array) $group;
$subTypes = (array) $subTypes;
$services = array_filter($this->additionalServices, function (Service $service) use ($group, $availableOnly) {
return true === in_array($service->subType, $group) && (false === $availableOnly || 0 < $service->available);
$services = array_filter($this->additionalServices, function (Service $service) use ($subTypes, $filterByTravelDateRange) {
// Check subtype
if (false === in_array($service->subType, $subTypes, true)) {
return false;
}
// Check date range overlap if filtering by travel dates is enabled
if (true === $filterByTravelDateRange) {
if (null !== $service->dateFrom && null !== $service->dateTo
&& null !== $this->dateFrom && null !== $this->dateTo) {
// Service is available if its date range overlaps with travel dates:
// service.dateFrom <= travel.dateTo AND service.dateTo >= travel.dateFrom
return $service->dateFrom <= $this->dateTo && $service->dateTo >= $this->dateFrom;
}
// Service is available if it has no date constraints or travel has no dates
}
return true;
});
usort($services, function (Service $a, Service $b) {
return $a->label <=> $b->label;
});
return $services;
return $this->sortByPrice($services);
}
/**
* Retrieves transportation services filtered by direction and availability.
* Retrieves transportation services filtered by direction.
*
* Filters transportation services based on travel direction and optionally
* by availability. Services are sorted by subtype.
* Filters transportation services based on travel direction. Services are sorted by subtype.
* Availability filtering is NOT applied here - it happens at the form/UI level where
* unavailable services are shown as readonly with appropriate tooltips.
*
* @param string $direction The travel direction to filter by
* @param bool $availableOnly Whether to include only available services
* @param string $direction The travel direction to filter by
*
* @return array<Service> The filtered and sorted transportation services
* @return array<int, Service> The filtered and sorted transportation services
*/
public function getTransportationServicesByDirection(string $direction, bool $availableOnly = true): array
public function getTransportationServicesByDirection(string $direction): array
{
$services = array_filter($this->transportationServices, function (Service $service) use ($direction, $availableOnly) {
return $direction === $service->direction
&& $service->price >= 0
&& (false === $availableOnly || $service->available > 0);
});
$services = array_filter($this->transportationServices, fn (Service $service) => $direction === $service->direction);
usort($services, function (Service $a, Service $b) {
return $a->subType <=> $b->subType;
});
usort($services, fn (Service $a, Service $b) => $a->subType <=> $b->subType);
return $services;
}
@@ -144,7 +182,7 @@ class Travel
* Maps selection group IDs to their corresponding services based on
* the predefined included services mapping.
*
* @return array The included services from selection groups
* @return array<int, Service> The included services from selection groups
*/
public function getIncludedServices(): array
{
@@ -168,7 +206,7 @@ class Travel
*
* @param array<int> $ids The array of room IDs to filter by
*
* @return array<Room> The filtered rooms array
* @return array<int, Room> The filtered rooms array
*/
public function getRoomsByIds(array $ids): array
{
@@ -182,18 +220,96 @@ class Travel
}
/**
* Retrieves all available rooms for booking.
* Retrieves all bookable rooms (both regular and inquiry).
*
* Filters the rooms collection to return only rooms that have availability
* greater than zero and have an available status. This ensures only
* bookable rooms are returned for selection.
* Returns rooms that have availability greater than zero and a bookable status
* (either 'Frei' for regular booking or 'Anfrage' for inquiry booking).
*
* @return array<Room> The filtered array of available rooms
* @return array<int, Room> The filtered array of bookable rooms, indexed by room ID
*/
public function getAvailableRooms(): array
{
return array_filter($this->rooms, function (Room $room) {
return $room->available > 0 && Constants::STATUS_AVAILABLE === $room->status;
});
$result = [];
foreach ($this->rooms as $room) {
if ($room->available > 0 && $this->isBookableRoomStatus($room->status)) {
$result[$room->id] = $room;
}
}
return $result;
}
/**
* Checks if the travel has any bookable rooms.
*
* @return bool True if at least one room is bookable
*/
public function hasBookableRooms(): bool
{
return [] !== $this->getAvailableRooms();
}
/**
* Checks if all bookable rooms require inquiry booking.
*
* Returns true if there are bookable rooms but none with 'Frei' status.
* This indicates the booking must be submitted as an inquiry.
*
* @return bool True if only inquiry rooms are available
*/
public function requiresInquiryBooking(): bool
{
$bookableRooms = $this->getAvailableRooms();
if ([] === $bookableRooms) {
return false;
}
foreach ($bookableRooms as $room) {
if (Constants::STATUS_AVAILABLE === $room->status) {
return false;
}
}
return true;
}
/**
* Checks if a room status allows booking.
*/
private function isBookableRoomStatus(?string $status): bool
{
return Constants::STATUS_AVAILABLE === $status
|| Constants::STATUS_ON_REQUEST === $status;
}
/**
* Retrieves a room by its ID.
*
* @param int|null $roomId The room ID to search for
*
* @return Room|null The room with the matching ID, or null if not found
*/
public function getRoomById(?int $roomId): ?Room
{
if (null === $roomId) {
return null;
}
$rooms = $this->getRoomsByIds([$roomId]);
return false === empty($rooms) ? reset($rooms) : null;
}
/**
* Checks if a specific booking status code is allowed for this travel.
*
* @param string $statusCode The booking status code to check (F, A, or O)
*
* @return bool True if the status is allowed, false otherwise
*/
public function isBookingStatusAllowed(string $statusCode): bool
{
return in_array($statusCode, $this->allowedBookingStatus, true);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
final readonly class XmlExportInfo
{
private const INFO_FILENAME = 'uebertragung.info';
private const TIMESTAMP_FORMAT = 'd.m.Y H:i:s';
public function __construct(
public \DateTimeImmutable $lastTransfer,
public int $fileCount,
) {
}
public static function getFilename(): string
{
return self::INFO_FILENAME;
}
public static function fromString(string $content): self
{
$content = mb_convert_encoding($content, 'UTF-8', 'Windows-1252');
$lines = array_filter(array_map('trim', explode("\n", $content)));
if (count($lines) < 3) {
throw new \InvalidArgumentException('Invalid uebertragung.info format: expected at least 3 lines');
}
$timestamp = \DateTimeImmutable::createFromFormat(self::TIMESTAMP_FORMAT, $lines[0]);
if (false === $timestamp) {
throw new \InvalidArgumentException(sprintf('Invalid timestamp format in uebertragung.info: expected "%s", got "%s"', self::TIMESTAMP_FORMAT, $lines[0]));
}
if (1 !== preg_match('/(\d+)/', $lines[2], $matches)) {
throw new \InvalidArgumentException(sprintf('Could not extract file count from: "%s"', $lines[2]));
}
return new self($timestamp, (int) $matches[1]);
}
public function isNewerThan(self $other): bool
{
return $this->lastTransfer > $other->lastTransfer;
}
}
+65 -7
View File
@@ -3,14 +3,20 @@
namespace App\BusProNet\Traits;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Exception\TimeoutException;
trait ApiClientTrait
{
private float $operationStartTime;
/**
* @throws ApiClientException
* @throws TimeoutException
*/
private function connect(string $host, int $port, int $maxRetries = 25)
private function connect(string $host, int $port, int $maxRetries = 25, int $connectionTimeout = 5, int $streamTimeout = 30, int $totalTimeout = 45)
{
$this->operationStartTime = microtime(true);
$tries = 1;
$errNo = $errStr = '';
$errorCodesForRetry = [
@@ -18,22 +24,32 @@ trait ApiClientTrait
SOCKET_EBADF,
];
$openSocket = function (&$errNo, &$errStr) use ($host, $port) {
$openSocket = function (&$errNo, &$errStr) use ($host, $port, $connectionTimeout) {
return @fsockopen(
$host,
$port,
$errNo,
$errStr,
10
$connectionTimeout
);
};
$socket = $openSocket($errNo, $errStr);
while (false === $socket && true === in_array($errNo, $errorCodesForRetry) && $maxRetries > $tries) {
// Check if we've exceeded total timeout during retries
if (microtime(true) - $this->operationStartTime > $totalTimeout) {
$this->logger->error('Connection retry timeout exceeded', [
'elapsed_time' => microtime(true) - $this->operationStartTime,
'total_timeout' => $totalTimeout,
]);
throw new TimeoutException('Connection timeout exceeded during retries');
}
$this->logger->warning('Could not connect to socket, retrying', [
'error_message' => $errStr,
'error_number' => $errNo,
'attempt' => $tries,
]);
++$tries;
sleep(1);
@@ -41,11 +57,12 @@ trait ApiClientTrait
}
if (false !== $socket) {
stream_set_timeout($socket, 60);
stream_set_timeout($socket, $streamTimeout);
} else {
$this->logger->error('Unable to open socket', [
'error_message' => $errStr,
'error_number' => $errNo,
'elapsed_time' => microtime(true) - $this->operationStartTime,
]);
throw new ApiClientException('Unable to open socket');
}
@@ -53,19 +70,60 @@ trait ApiClientTrait
return $socket;
}
private function send($socket, string $data): void
/**
* @throws TimeoutException
*/
private function send($socket, string $data, int $totalTimeout): void
{
// Check total timeout before sending
if (microtime(true) - $this->operationStartTime > $totalTimeout) {
$this->logger->error('Total timeout exceeded before send', [
'elapsed_time' => microtime(true) - $this->operationStartTime,
]);
throw new TimeoutException('Total operation timeout exceeded before send');
}
// message length is prepended to actual message
$send = sprintf('%010s', strlen($data)).$data;
fwrite($socket, $send);
}
private function receive($socket): string
/**
* @throws TimeoutException
*/
private function receive($socket, int $totalTimeout): string
{
$response = '';
$readAttempts = 0;
while (false === feof($socket)) {
$response .= fread($socket, 4096);
// Check total timeout before each read
$elapsedTime = microtime(true) - $this->operationStartTime;
if ($elapsedTime > $totalTimeout) {
$this->logger->error('Total timeout exceeded during receive', [
'elapsed_time' => $elapsedTime,
'total_timeout' => $totalTimeout,
'bytes_received' => strlen($response),
'read_attempts' => $readAttempts,
]);
throw new TimeoutException('Total operation timeout exceeded while receiving data');
}
$chunk = fread($socket, 4096);
++$readAttempts;
// Check if stream timed out on this specific read
$metadata = stream_get_meta_data($socket);
if (true === $metadata['timed_out']) {
$this->logger->error('Stream read timeout detected', [
'elapsed_time' => $elapsedTime,
'bytes_received' => strlen($response),
'read_attempts' => $readAttempts,
]);
throw new TimeoutException('Stream timeout while reading from socket');
}
$response .= $chunk;
}
return $response;

Some files were not shown because too many files have changed in this diff Show More