Compare commits
19
Commits
9265e0d1c1
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a8013e8a | ||
|
|
518f29b160 | ||
|
|
5b37b37a78 | ||
|
|
ab2b840dc5 | ||
|
|
4cefbce87e | ||
|
|
2bd663a9bf | ||
|
|
bf9afd143d | ||
|
|
5d41660a9a | ||
|
|
c63cbdd3a6 | ||
|
|
e9bda426f2 | ||
|
|
63e3ed4a78 | ||
|
|
9aabff6b5b | ||
|
|
863dd9de1a | ||
|
|
26a7d5f78a | ||
|
|
8f667e92ee | ||
|
|
0ac58c1033 | ||
|
|
903292d84c | ||
|
|
170e14dae5 | ||
|
|
b428ce9086 |
No files matched your search
@@ -0,0 +1,44 @@
|
||||
-- Removes personal data from a production dump. Run by `ddev anonymize-db`, which ddev also
|
||||
-- runs after every `ddev import-db`. Safe to run more than once.
|
||||
-- @devPasswordHash is set by the command before this file runs.
|
||||
|
||||
-- Tables renamed by the database analyzer in production keep their data until dropped there
|
||||
SET @zzzTables = (
|
||||
SELECT GROUP_CONCAT('`', table_name, '`')
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name LIKE 'zzz\_deleted\_%'
|
||||
);
|
||||
SET @dropZzzTables = IF(@zzzTables IS NULL, 'DO 0', CONCAT('DROP TABLE ', @zzzTables));
|
||||
PREPARE dropZzzTables FROM @dropZzzTables;
|
||||
EXECUTE dropZzzTables;
|
||||
DEALLOCATE PREPARE dropZzzTables;
|
||||
|
||||
-- Usernames stay, so everybody logs in locally as their own user (with the dev password)
|
||||
UPDATE be_users SET
|
||||
realName = IF('' = realName, '', CONCAT('BE User ', uid)),
|
||||
email = IF('' = email, '', CONCAT('be-user-', uid, '@example.invalid')),
|
||||
password = @devPasswordHash,
|
||||
password_reset_token = '',
|
||||
mfa = NULL;
|
||||
|
||||
UPDATE fe_users SET
|
||||
username = CONCAT('fe-user-', uid),
|
||||
password = @devPasswordHash,
|
||||
name = IF('' = name, '', CONCAT('FE User ', uid)),
|
||||
first_name = IF('' = first_name, '', 'Vorname'),
|
||||
middle_name = '',
|
||||
last_name = IF('' = last_name, '', CONCAT('Nachname ', uid)),
|
||||
address = IF('' = address, '', 'Musterstraße 1'),
|
||||
telephone = IF('' = telephone, '', '0000 000000'),
|
||||
email = IF('' = email, '', CONCAT('fe-user-', uid, '@example.invalid')),
|
||||
zip = IF('' = zip, '', '00000'),
|
||||
city = IF('' = city, '', 'Musterstadt');
|
||||
|
||||
-- Sessions, locks, reports and logs: IP addresses, user agents, user data
|
||||
TRUNCATE be_sessions;
|
||||
TRUNCATE fe_sessions;
|
||||
TRUNCATE sys_lockedrecords;
|
||||
TRUNCATE sys_http_report;
|
||||
TRUNCATE sys_log;
|
||||
TRUNCATE tx_epproducts_log;
|
||||
TRUNCATE tx_epevents_log;
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
## Description: Remove personal data from the imported production database
|
||||
## Usage: anonymize-db
|
||||
## Example: "ddev anonymize-db"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DEV_PASSWORD=password
|
||||
|
||||
# TYPO3 verifies argon2i hashes with plain password_verify()
|
||||
hash=$(php -r 'echo password_hash($argv[1], PASSWORD_ARGON2I);' "$DEV_PASSWORD")
|
||||
|
||||
{
|
||||
echo "SET @devPasswordHash = '${hash}';"
|
||||
cat /var/www/html/.ddev/anonymize-db.sql
|
||||
} | mysql --host=db --user=db --password=db db
|
||||
|
||||
echo "Database anonymized. Backend users log in with their username and the password '${DEV_PASSWORD}'."
|
||||
@@ -313,3 +313,7 @@ corepack_enable: false
|
||||
#hooks:
|
||||
# post-start:
|
||||
# - exec: composer install -d /var/www/html
|
||||
|
||||
hooks:
|
||||
post-import-db:
|
||||
- exec: .ddev/commands/web/anonymize-db
|
||||
@@ -1,65 +1,26 @@
|
||||
# TYPO3 CMS Base Distribution
|
||||
# E&P Reisen websites (TYPO3 13)
|
||||
|
||||
Get going quickly with TYPO3 CMS.
|
||||
TYPO3 13 LTS for www.ep-reisen.de and its subsites (ski-boarderweek, snowzone, semesterende-skireisen,
|
||||
ski-wochenenden, …). Products, hotels, regions and dates live in `packages/ep_products`, templates
|
||||
and assets in `packages/ep_theme`. Dates, prices and availability come from **MyE&P** (`EP/myep`),
|
||||
which in turn reads bpn-connect.
|
||||
|
||||
## Prerequisites
|
||||
## Local setup
|
||||
|
||||
* PHP 8.2
|
||||
* [Composer](https://getcomposer.org/download/)
|
||||
DDEV project: `ddev start`, then `ddev composer install` and `ddev npm install && ddev npm run build`.
|
||||
Rebuild the assets after template changes: Tailwind purges classes it does not find in the
|
||||
templates.
|
||||
|
||||
## Quickstart
|
||||
## Documentation
|
||||
|
||||
* `composer create-project typo3/cms-base-distribution project-name ^13`
|
||||
* `cd project-name`
|
||||
- [docs/cms-api.md](docs/cms-api.md): the API TYPO3 serves to MyE&P and the chatbot
|
||||
(`/api/hotel`, `/api/product`, `/api/calendar-hotels`, `/api/content/*`).
|
||||
- [docs/calendars.md](docs/calendars.md): price calendar, Belegungskalender and iCal feed: where
|
||||
the data comes from, caching, and what to check when a calendar does not load.
|
||||
|
||||
Note that this distribution installs most, but not all of the TYPO3 CMS core extensions.
|
||||
Depending on your need you might also want to install other TYPO3 extensions from
|
||||
[packagist.org](https://packagist.org/?type=typo3-cms-framework).
|
||||
## Deployment
|
||||
|
||||
### Setup
|
||||
|
||||
To start an interactive installation, you can do so by executing the following
|
||||
command and then follow the wizard:
|
||||
|
||||
```bash
|
||||
composer exec typo3 setup
|
||||
```
|
||||
|
||||
### Setup unattended (optional)
|
||||
|
||||
If you're a more advanced user, you might want to leverage the unattended installation.
|
||||
To do this, you need to execute the following command and substitute the arguments
|
||||
with your own environment configuration.
|
||||
|
||||
```bash
|
||||
export TYPO3_SETUP_ADMIN_PASSWORD=$(tr -dc "_A-Za-z0-9#=$()/" < /dev/urandom | head -c24)
|
||||
composer exec -- typo3 setup \
|
||||
--no-interaction \
|
||||
--server-type=other \
|
||||
--driver=sqlite \
|
||||
--admin-username=admin \
|
||||
--admin-email="[email protected]" \
|
||||
--project-name="My TYPO3 Project" \
|
||||
--create-site="http://localhost:8000/"
|
||||
echo "Admin password: ${TYPO3_SETUP_ADMIN_PASSWORD}"
|
||||
```
|
||||
|
||||
### Development server
|
||||
|
||||
While it's advised to use a more sophisticated web server such as
|
||||
Apache 2 or Nginx, you can instantly run the project by using PHPs` built-in
|
||||
[web server](https://secure.php.net/manual/en/features.commandline.webserver.php).
|
||||
|
||||
* `TYPO3_CONTEXT=Development php -S localhost:8000 -t public`
|
||||
* open your browser at "http://localhost:8000"
|
||||
|
||||
Please be aware that the built-in web server is single threaded and only meant
|
||||
to be used for development.
|
||||
|
||||
## Next steps
|
||||
|
||||
* [Getting Started with TYPO3](https://docs.typo3.org/permalink/t3start:start)
|
||||
* [Create a Site Package](https://docs.typo3.org/permalink/t3sitepackage:start)
|
||||
`deploy.php` (Deployer). A deploy runs `extension:setup`, resets OPcache and flushes all caches.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+1
-1
@@ -20,6 +20,7 @@
|
||||
"ep/ep-theme": "@dev",
|
||||
"fluidtypo3/vhs": "^8.0",
|
||||
"georgringer/news": "^14.1",
|
||||
"helhum/typo3-console": "^8.2",
|
||||
"lochmueller/staticfilecache": "^15.2",
|
||||
"ssch/typo3-encore": "^7.0",
|
||||
"typo3/cms-backend": "^13.4",
|
||||
@@ -29,7 +30,6 @@
|
||||
"typo3/cms-dashboard": "^13.4",
|
||||
"typo3/cms-extbase": "^13.4",
|
||||
"typo3/cms-extensionmanager": "^13.4",
|
||||
"typo3/cms-felogin": "^13.4",
|
||||
"typo3/cms-filelist": "^13.4",
|
||||
"typo3/cms-fluid": "^13.4",
|
||||
"typo3/cms-fluid-styled-content": "^13.4",
|
||||
|
||||
Generated
+850
-486
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
/*/settings.yaml
|
||||
@@ -20,3 +20,5 @@ languages:
|
||||
languageId: 0
|
||||
rootPageId: 2458
|
||||
websiteTitle: ''
|
||||
dependencies:
|
||||
- ep/theme
|
||||
@@ -20,3 +20,5 @@ languages:
|
||||
languageId: 0
|
||||
rootPageId: 2457
|
||||
websiteTitle: ''
|
||||
dependencies:
|
||||
- ep/theme
|
||||
@@ -294,3 +294,5 @@ routes:
|
||||
type: staticText
|
||||
content: "User-agent: *\r\nDisallow: /typo3/\r\nDisallow: /typo3_src/\r\nDisallow: /ajax/\r\nAllow: /typo3/sysext/frontend/Resources/Public/*\r\n"
|
||||
websiteTitle: ''
|
||||
dependencies:
|
||||
- ep/theme
|
||||
@@ -92,3 +92,5 @@ routes:
|
||||
type: staticText
|
||||
content: "User-agent: *\r\nDisallow: /typo3/\r\nDisallow: /typo3_src/\r\nAllow: /typo3/sysext/frontend/Resources/Public/*\r\n"
|
||||
websiteTitle: 'Semsterende Skireisen'
|
||||
dependencies:
|
||||
- ep/theme
|
||||
@@ -75,3 +75,5 @@ routes:
|
||||
type: staticText
|
||||
content: "User-agent: *\r\nDisallow: /typo3/\r\nDisallow: /typo3_src/\r\nAllow: /typo3/sysext/frontend/Resources/Public/*\r\n"
|
||||
websiteTitle: ''
|
||||
dependencies:
|
||||
- ep/theme
|
||||
@@ -59,3 +59,5 @@ routes:
|
||||
type: staticText
|
||||
content: "User-agent: *\r\nDisallow: /typo3/\r\nDisallow: /typo3_src/\r\nAllow: /typo3/sysext/frontend/Resources/Public/*\r\n"
|
||||
websiteTitle: 'Ski-Wochenenden mit E&P Reisen und Events'
|
||||
dependencies:
|
||||
- ep/theme
|
||||
@@ -58,3 +58,5 @@ routes:
|
||||
route: robots.txt
|
||||
type: staticText
|
||||
content: "User-agent: *\r\nDisallow: /typo3/\r\nDisallow: /typo3_src/\r\nAllow: /typo3/sysext/frontend/Resources/Public/*\r\n"
|
||||
dependencies:
|
||||
- ep/theme
|
||||
@@ -58,3 +58,5 @@ routes:
|
||||
route: robots.txt
|
||||
type: staticText
|
||||
content: "User-agent: *\r\nDisallow: /typo3/\r\nDisallow: /typo3_src/\r\nAllow: /typo3/sysext/frontend/Resources/Public/*\r\n"
|
||||
dependencies:
|
||||
- ep/theme
|
||||
+18
-1
@@ -21,10 +21,16 @@ set('shared_dirs', [
|
||||
// Baseline of the MyE&P import: without it every deploy would trigger a full re-import
|
||||
'var/transient/ep_products_import',
|
||||
]);
|
||||
// Not in git, production is leading (encryptionKey, install tool, backend changes)
|
||||
// Not in git, production is leading (encryptionKey, install tool, backend changes; the site
|
||||
// settings are edited in the backend's Settings module, one settings.yaml per site)
|
||||
set('site_settings_files', array_map(
|
||||
static fn(string $config): string => 'config/sites/' . basename(dirname($config)) . '/settings.yaml',
|
||||
glob(__DIR__ . '/config/sites/*/config.yaml')
|
||||
));
|
||||
set('shared_files', [
|
||||
'config/system/secrets.php',
|
||||
'config/system/settings.php',
|
||||
...get('site_settings_files'),
|
||||
]);
|
||||
set('writable_dirs', [
|
||||
'{{typo3/public_dir}}/typo3temp/assets',
|
||||
@@ -96,6 +102,16 @@ task('deploy:check:fileadmin', function () {
|
||||
}
|
||||
});
|
||||
|
||||
// The same for a site's settings.yaml: an empty one silently resets the site to the default settings
|
||||
// (navigation, storage folders, ...). A new site's file is put in place by hand first.
|
||||
task('deploy:check:site-settings', function () {
|
||||
foreach (get('site_settings_files') as $file) {
|
||||
if (!test("[ -s {{deploy_path}}/shared/$file ]")) {
|
||||
throw new \RuntimeException(parse("{{deploy_path}}/shared/$file is missing or empty"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// current_path instead of the recipe's release_path, so it also works after a rollback
|
||||
task('typo3:cache:flush', function () {
|
||||
run('{{bin/php}} {{current_path}}/{{bin/typo3}} cache:flush');
|
||||
@@ -104,6 +120,7 @@ task('typo3:cache:flush', function () {
|
||||
task('deploy', [
|
||||
'deploy:info',
|
||||
'deploy:check:fileadmin',
|
||||
'deploy:check:site-settings',
|
||||
// Build locally first, so a failing build aborts before the server is touched at all.
|
||||
'deploy:build',
|
||||
'deploy:setup',
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Availability and prices on product pages
|
||||
|
||||
TYPO3 shows three things built from contingent data. None of them talks to bpn-connect directly:
|
||||
all go through **MyE&P**, which keeps a local snapshot of bpn-connect's availability and holds the
|
||||
group house prices.
|
||||
|
||||
| What | Where | Shown for | MyE&P endpoint |
|
||||
|---|---|---|---|
|
||||
| **Price calendar** + price tables | product detail, tab "Termine/Preise" | products with a calendar hotel and season **summer** (`s`) | `contingents/calendar`, `contingents/prices` |
|
||||
| **Belegungskalender** (availability calendar) | product detail, sidebar | products with a calendar hotel and season **not summer** | `contingents/availability` (+ `contingents/calendar` for the months to leave out) |
|
||||
| **iCal feed** for booking portals | `https://www.ep-reisen.de/contingents/<hotel>.ical` | every hotel | `contingents/availability` |
|
||||
|
||||
So every product with a calendar hotel shows exactly one calendar. The MyE&P side is documented in
|
||||
its `docs/api-consumer-guide.md` (`EP/myep`).
|
||||
|
||||
## The calendar hotel field
|
||||
|
||||
A product's **calendar hotel** (`calendar_hotel`) decides whose availability and prices it shows.
|
||||
TYPO3 sends that hotel's **code** to MyE&P, which only understands **BusPro calendar codes** (the
|
||||
codes bpn-connect knows, e.g. `DGS`, `LPJ`, `DKS`).
|
||||
|
||||
Most hotel records carry the BusPro code. Group houses also have **content records** of their own
|
||||
(`DKSGRP`, `LPJGRP`, `SERASCSO`, …) whose code is the **CMS code** MyE&P uses to fetch content via
|
||||
`/api/hotel`. These must **not** be used as calendar hotel: MyE&P and bpn-connect do not know
|
||||
them, and the calendar shows its error message. This broke three example trips until 2026-10-29.
|
||||
|
||||
| Use as calendar hotel | Not |
|
||||
|---|---|
|
||||
| `DKS` Sportclub Schweizerhaus (uid 193) | `DKSGRP` Davos Klosters - Gruppenhaus Schweizerhaus |
|
||||
| `LPJ` Sportclub Jenatsch (uid 37) | `LPJGRP` Lenzerheide - Gruppenhaus Jenatsch |
|
||||
|
||||
`/api/calendar-hotels` lists all calendar hotels in use; every code in it should be a BusPro code.
|
||||
A hidden hotel record counts as no calendar hotel: the product shows no calendar and the code is
|
||||
not in the list (e.g. `ASB` Sportclub Bader).
|
||||
|
||||
## Price calendar (summer)
|
||||
|
||||
- `Product/Detail.html`, section `Dates`, partial `Product/PriceCalendar`;
|
||||
`AjaxCalendarController::priceCalendarAction` → `contingents/calendar` (status and price per
|
||||
day), `priceTableAction` → `contingents/prices` for the current and the next year (loaded when
|
||||
scrolled into view).
|
||||
- Only works for hotels that are an **accommodation with prices** in MyE&P (summer group houses).
|
||||
|
||||
## Belegungskalender (winter)
|
||||
|
||||
- `Product/Detail.html`, section `AvailabilityCalendar`, loaded by htmx when it scrolls into view
|
||||
(`intersect once`, so the hidden copy of the sidebar on the other breakpoint does not load);
|
||||
`AjaxCalendarController::availabilityAction`, template `AjaxCalendar/Availability.html`,
|
||||
month select `calendar_controller.js`.
|
||||
- All months in one response; the select only switches between them. 12 months from the current
|
||||
one, extended to the last month MyE&P has data for.
|
||||
- **Months with prices are left out**: they are summer, covered by the price calendar. Only summer
|
||||
months have prices today; that is the whole distinction. If `contingents/calendar` fails, all
|
||||
months are shown (not cached).
|
||||
- Colours as on TYPO3 10: green available (`OK`, and days without data), yellow on request
|
||||
(`ON_REQUEST`), red not available (`BLOCKED`), past days grey; a status change from the previous
|
||||
day is drawn as a diagonal. The `availability-*` classes are built in PHP, so they are safelisted
|
||||
in `tailwind.config.js`; without that Tailwind purges them.
|
||||
- Works for every hotel MyE&P syncs, with or without an accommodation (see below).
|
||||
|
||||
## iCal feed
|
||||
|
||||
- `IcalController::contingentsAction`, `IcalService`; route `/contingents/{hotel}.ical` on the
|
||||
`ep-reisen` site (`config/sites/ep-reisen/config.yaml`, page 9, type 1710), `{hotel}` is the
|
||||
hotel's path segment.
|
||||
- One `Unavailable` event per run of `BLOCKED` or `ON_REQUEST` days with the same status, as on
|
||||
TYPO3 10.
|
||||
- Never cached. When MyE&P has no availability it answers `503` with `Retry-After: 900` instead of
|
||||
an empty calendar, which the portals would read as "every day free"; they keep their last import.
|
||||
|
||||
## Which hotels MyE&P has availability for
|
||||
|
||||
MyE&P syncs its snapshot from bpn-connect every 15 minutes during the day and hourly at night, for
|
||||
the calendar codes of its accommodations **plus** every code in TYPO3's `/api/calendar-hotels`
|
||||
(see [cms-api.md](cms-api.md)). So a hotel offered only in winter needs nothing in MyE&P: setting
|
||||
it as calendar hotel of a visible product is enough; it is synced with the next run.
|
||||
|
||||
`contingents/availability` answers:
|
||||
- `400` for a code that is not synced (not an accommodation, not in TYPO3's list),
|
||||
- `502` when the hotel's snapshot is missing or older than 6 hours (sync failing),
|
||||
- `200` with one entry per known day otherwise.
|
||||
|
||||
`contingents/calendar` and `contingents/prices` additionally need an accommodation with prices.
|
||||
|
||||
MyE&P's `GET /api/contingents/status` lists every synced code with its source, prices, last sync
|
||||
and last error: the first place to look when a calendar does not load.
|
||||
|
||||
## Caching
|
||||
|
||||
- The AJAX responses (price calendar, price tables, Belegungskalender) are page-cached on the AJAX
|
||||
page for at most **1 hour** (`cache_period = 3600`) and never past midnight
|
||||
(`cache_clearAtMidnight`, past days must turn grey). Browsers do not cache them
|
||||
(`sendCacheHeaders = 0`).
|
||||
- **Errors are not cached** (`disablePageCache()`): the next visitor triggers a new attempt.
|
||||
- Every AJAX URL carries a cHash (required for `tx_epproducts_ajax`), so each hotel and year has its
|
||||
own cache entry.
|
||||
- Entries are tagged `ep_hotel_<code>`. Nothing flushes them automatically; a deploy flushes all
|
||||
caches. To refresh by hand: `vendor/bin/typo3 cache:flush --group pages`, or flush the tag via
|
||||
the `CacheManager` for a single hotel.
|
||||
- On top of that comes MyE&P's sync interval, so a calendar is at most about 1¼ hours behind
|
||||
bpn-connect.
|
||||
|
||||
## When a calendar does not load
|
||||
|
||||
The message is "Der Kalender konnte aktuell nicht geladen werden."; the iCal feed answers `503`.
|
||||
|
||||
| Cause | How to tell | Fix |
|
||||
|---|---|---|
|
||||
| Calendar hotel has a CMS code (`…GRP`, `…SO`) | code not BusPro; MyE&P `400`, bpn-connect `404` | set the hotel record with the BusPro code as calendar hotel |
|
||||
| Code not synced by MyE&P | not in `contingents/status`; MyE&P `400` | check that the product and its calendar hotel are visible (then it is in `/api/calendar-hotels`) |
|
||||
| Sync failing for the hotel | `contingents/status`: `available: false`, `lastError` | MyE&P / bpn-connect |
|
||||
| MyE&P unreachable or its OAuth failing | all hotels fail; TYPO3 log `EP.EpProducts.MyEP.ApiClient` | MyE&P |
|
||||
| Price calendar only: no accommodation or no prices | MyE&P `400` / empty | prices are maintained in MyE&P's Groups admin |
|
||||
|
||||
TYPO3 logs every failed MyE&P call in `var/log/typo3_*.log` (component
|
||||
`EP.EpProducts.MyEP.ApiClient`, for the iCal feed also `EP.EpProducts.Service.IcalService`).
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# CMS API
|
||||
|
||||
Endpoints TYPO3 serves to other systems: **MyE&P** (hotel and product content, calendar hotels)
|
||||
and the **chatbot** (content feed). They are answered by `EP\EpProducts\Middleware\ApiMiddleware`
|
||||
before the page resolver, so they do not depend on a page and are never page-cached.
|
||||
|
||||
The other side of these contracts is documented in MyE&P's `docs/api-consumer-guide.md`
|
||||
(`EP/myep`).
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints need the shared key `cmsApiKey` from the `ep_products` extension configuration
|
||||
(Settings → Extension Configuration → ep_products, tab ContentFeed):
|
||||
|
||||
- header `X-Api-Key: <key>` (preferred, query strings end up in access logs), or
|
||||
- query parameter `?key=<key>`.
|
||||
|
||||
A missing or wrong key answers `403 {"message": "Invalid or missing api key"}`. An empty
|
||||
`cmsApiKey` denies every request. MyE&P sends the key as `APP_CMS_API_KEY`; both sides must be
|
||||
changed together.
|
||||
|
||||
## `GET /api/hotel?hotel=<code>`
|
||||
|
||||
Content of one hotel, for MyE&P's group house pages and booking flow.
|
||||
|
||||
- `hotel`: the TYPO3 hotel code or one of its matchcode aliases. In MyE&P this is the
|
||||
accommodation's **CMS code** (e.g. `DGSGRP`), which can differ from its calendar code.
|
||||
- `400` without `hotel`, `404 {"message": "Hotel not found"}` for an unknown code.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "…", "code": "…",
|
||||
"description": "…", "features": "…", "room_types": "…",
|
||||
"additional_information": "…", "address": "…",
|
||||
"images": {"original": [{"url": "…", "alt": "…", "title": "…"}], "resized": […]},
|
||||
"icons": […],
|
||||
"region": {"name": "…", "latitude": …, "longitude": …, "webcam": "…", "ski_area": "…",
|
||||
"ski_area_extended": "…", "description": "…", "news": "…", "length": …,
|
||||
"altitude": …, "lifts": …, "images": {…}, "region_maps": [{"url": "…", "alt": "…", "title": "…"}]}
|
||||
}
|
||||
```
|
||||
|
||||
## `GET /api/product?product=<code>[&hotel=<code>]`
|
||||
|
||||
Content of one product, optionally with a hotel.
|
||||
|
||||
- `product`: the product code. `400` without it, `404 {"message": "Product not found"}` for an
|
||||
unknown code.
|
||||
- `hotel` (optional): adds `hotel` with the same content as `/api/hotel`.
|
||||
|
||||
```json
|
||||
{
|
||||
"product": {"name": "…", "code": "…", "images": {…}},
|
||||
"country": {"name": "…"},
|
||||
"region": {…},
|
||||
"city": {"name": "…"},
|
||||
"hotel": {…}
|
||||
}
|
||||
```
|
||||
|
||||
## `GET /api/calendar-hotels`
|
||||
|
||||
The calendar hotel of every visible product: the hotel codes whose availability TYPO3 shows
|
||||
(iCal feed, availability calendar, price calendar). MyE&P syncs its contingent snapshot for
|
||||
these codes in addition to its accommodations, so hotels offered only in winter, which have no
|
||||
accommodation in MyE&P, have availability too. See [calendars.md](calendars.md).
|
||||
|
||||
- Only products and hotels that are not hidden or deleted; hotels without a code are left out.
|
||||
- Sorted by code; products by uid. Names have line breaks collapsed.
|
||||
- `season`: `"s"` (summer), `"w"` (winter) or `null`.
|
||||
|
||||
```json
|
||||
[
|
||||
{"code": "DKI", "name": "Sportclub Schwendi", "products": [
|
||||
{"uid": 155, "name": "Sportclub Schwendi", "season": "w"},
|
||||
{"uid": 362, "name": "Gruppenhaus Schwendi", "season": "s"}
|
||||
]}
|
||||
]
|
||||
```
|
||||
|
||||
Every code in this list should be a **BusPro calendar code**. A group house's content record
|
||||
(`…GRP`, `…SO`) set as a product's calendar hotel puts its CMS code in here, which bpn-connect does
|
||||
not know (see [calendars.md](calendars.md#the-calendar-hotel-field)).
|
||||
|
||||
## `GET /api/content/manifest` and `GET /api/content/records?type=<type>&uids=<uids>`
|
||||
|
||||
The content feed for the chatbot (`EP\EpProducts\ContentFeed\ContentFeedApi`).
|
||||
|
||||
- `manifest`: everything the feed publishes (pages of the configured sites and records), with a
|
||||
modification timestamp per item. Cached for 10 minutes.
|
||||
- `records`: up to 100 records of one type per request. `type` is one of `product`, `hotel`,
|
||||
`region`, `country`, `city`, `concept`, `news`; `uids` comma-separated. Uids that are no longer
|
||||
eligible are left out, not answered with an error (the consumer reads them as deleted).
|
||||
- `400` for an unknown type, no uids or more than 100 uids; `404` for other paths.
|
||||
- `503` with `Retry-After: 3600` when the feed is switched off (`contentFeedEnabled = 0`, the kill
|
||||
switch for when the host is struggling), `503` when `contentFeedSites` is empty. The consumer
|
||||
keeps what it has on a 503; nothing is deleted on the chatbot side.
|
||||
|
||||
Configuration (extension configuration `ep_products`, tab ContentFeed): `contentFeedEnabled`,
|
||||
`contentFeedSites`, `contentFeedLanguages`, `contentFeedExcludedPages`,
|
||||
`contentFeedNewsDetailPages`.
|
||||
@@ -254,7 +254,7 @@ class ManifestBuilder
|
||||
'pid',
|
||||
'sys_language_uid',
|
||||
'MAX(tstamp) AS tstamp',
|
||||
"SUM(CASE WHEN deleted = 0 AND hidden = 0 AND CType <> 'list' THEN 1 ELSE 0 END) AS text_elements"
|
||||
"SUM(CASE WHEN deleted = 0 AND hidden = 0 AND CType <> 'list' AND CType NOT LIKE 'epproducts\\_%' THEN 1 ELSE 0 END) AS text_elements"
|
||||
)
|
||||
->from('tt_content')->groupBy('pid', 'sys_language_uid')->executeQuery()
|
||||
->fetchAllAssociative()
|
||||
|
||||
@@ -112,6 +112,97 @@ class AjaxCalendarController extends ActionController
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability calendar (Belegungskalender) of the product detail sidebar, for products
|
||||
* without the price calendar, limited to the months without prices (winter). TYPO3 10
|
||||
* fetched it month by month from bpn-connect; MyE&P returns the whole snapshot at once,
|
||||
* so every month is rendered and the month select only switches between them.
|
||||
*/
|
||||
public function availabilityAction(Hotel $hotel, int $months = 12): ResponseInterface
|
||||
{
|
||||
$hotelCode = trim($hotel->getCode() ?? '');
|
||||
$this->tagPageCacheForHotel($hotelCode);
|
||||
|
||||
$period = DateUtility::getDateRange(null, null, $months)['period'];
|
||||
$today = new \DateTimeImmutable('today');
|
||||
|
||||
$statusByDate = [];
|
||||
$error = false;
|
||||
|
||||
if ('' === $hotelCode) {
|
||||
$error = true;
|
||||
$this->disablePageCache();
|
||||
} else {
|
||||
try {
|
||||
foreach ($this->myEpClient->getContingentAvailability($hotelCode) as $item) {
|
||||
if (!is_array($item) || !isset($item['date'])) {
|
||||
continue;
|
||||
}
|
||||
$statusByDate[$item['date']] = $this->normalizeAvailabilityStatus((string)($item['status'] ?? 'OK'));
|
||||
}
|
||||
} catch (ApiException $e) {
|
||||
$error = true;
|
||||
$this->disablePageCache();
|
||||
}
|
||||
}
|
||||
|
||||
if ([] !== $statusByDate) {
|
||||
$lastDataDate = new \DateTimeImmutable(max(array_keys($statusByDate)));
|
||||
$dataEnd = $lastDataDate->modify('first day of next month')->setTime(0, 0, 0);
|
||||
if ($dataEnd > $period->getEndDate()) {
|
||||
$period = new Period($period->getStartDate(), $dataEnd);
|
||||
}
|
||||
}
|
||||
|
||||
// Days without a row are available, as on TYPO3 10. A change of the occupancy level
|
||||
// from the previous day is drawn as a diagonal transition (arrival/departure day).
|
||||
$classByDate = [];
|
||||
$previousStatus = $statusByDate[$period->getStartDate()->modify('-1 day')->format('Y-m-d')] ?? 'OK';
|
||||
for ($day = \DateTimeImmutable::createFromInterface($period->getStartDate()); $day < $period->getEndDate(); $day = $day->modify('+1 day')) {
|
||||
$dateKey = $day->format('Y-m-d');
|
||||
$status = $statusByDate[$dateKey] ?? 'OK';
|
||||
if ($day < $today) {
|
||||
$classByDate[$dateKey] = 'text-zinc-400';
|
||||
} elseif ($previousStatus !== $status) {
|
||||
$classByDate[$dateKey] = 'text-white availability-' . $this->getAvailabilitySuffix($previousStatus) . '-to-' . $this->getAvailabilitySuffix($status);
|
||||
} else {
|
||||
$classByDate[$dateKey] = 'text-white availability-' . $this->getAvailabilitySuffix($status);
|
||||
}
|
||||
$previousStatus = $status;
|
||||
}
|
||||
|
||||
// Only summer months have prices in the price config, and those are covered by the
|
||||
// price calendar, so they are left out here. If the price config is unavailable, all
|
||||
// months are shown, but not cached.
|
||||
$priceMonths = [];
|
||||
if (!$error) {
|
||||
try {
|
||||
foreach ($this->myEpClient->getPriceConfigData($hotelCode) as $item) {
|
||||
if (!is_array($item) || !isset($item['date'])) {
|
||||
continue;
|
||||
}
|
||||
if (null !== ($item['pricePerNight'] ?? null) || null !== ($item['defaultPricePerNight'] ?? null)) {
|
||||
$priceMonths[substr((string)$item['date'], 0, 7)] = true;
|
||||
}
|
||||
}
|
||||
} catch (ApiException $e) {
|
||||
$this->disablePageCache();
|
||||
}
|
||||
}
|
||||
|
||||
$calendarMonths = array_values(array_filter(
|
||||
$this->buildCalendarMonths($period),
|
||||
fn (array $month): bool => !isset($priceMonths[$month['date']->format('Y-m')])
|
||||
));
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'classByDate' => $classByDate,
|
||||
'calendarMonths' => $calendarMonths,
|
||||
'error' => $error,
|
||||
]);
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
|
||||
public function priceTableAction(Hotel $hotel, ?string $year = null): ResponseInterface
|
||||
{
|
||||
$year ??= new \DateTimeImmutable()->format('Y');
|
||||
@@ -167,6 +258,25 @@ class AjaxCalendarController extends ActionController
|
||||
$this->request->getAttribute('frontend.cache.instruction')?->disableCache('Price calendar data could not be retrieved');
|
||||
}
|
||||
|
||||
private function normalizeAvailabilityStatus(string $status): string
|
||||
{
|
||||
$status = strtoupper(trim($status));
|
||||
if ('ONREQUEST' === $status) {
|
||||
return 'ON_REQUEST';
|
||||
}
|
||||
|
||||
return in_array($status, ['OK', 'ON_REQUEST', 'BLOCKED'], true) ? $status : 'OK';
|
||||
}
|
||||
|
||||
private function getAvailabilitySuffix(string $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
'BLOCKED' => 'none',
|
||||
'ON_REQUEST' => 'partial',
|
||||
default => 'full',
|
||||
};
|
||||
}
|
||||
|
||||
private function buildTooltip(string $status, array $item): string
|
||||
{
|
||||
if ($status === 'blocked') {
|
||||
|
||||
@@ -4,14 +4,21 @@ namespace EP\EpProducts\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use EP\EpProducts\Domain\Model\Concept;
|
||||
use EP\EpProducts\Domain\Model\Product;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
class AjaxTeaserPopupController extends ActionController
|
||||
{
|
||||
public function conceptAction(Concept $concept, string $targetUrl): ResponseInterface
|
||||
public function conceptAction(Concept $concept, string $targetUrl, ?Product $product = null): ResponseInterface
|
||||
{
|
||||
// A product can have its own text; title, image and button stay those of the concept
|
||||
$popupText = (string)$product?->getPopupText();
|
||||
if ('' === trim(strip_tags($popupText))) {
|
||||
$popupText = $concept->getPopupText();
|
||||
}
|
||||
$this->view->assignMultiple([
|
||||
'concept' => $concept,
|
||||
'popupText' => $popupText,
|
||||
'targetUrl' => $targetUrl,
|
||||
]);
|
||||
return $this->htmlResponse();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Controller;
|
||||
|
||||
use EP\EpProducts\Domain\Model\Hotel;
|
||||
use EP\EpProducts\Service\IcalService;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Core\Http\PropagateResponseException;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
class IcalController extends ActionController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly IcalService $icalService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The feed is sent as a response of its own, past the page cache: a cached page would lose the
|
||||
* Content-Disposition header, and availability must not be older than the MyE&P snapshot
|
||||
*/
|
||||
public function contingentsAction(Hotel $hotel): ResponseInterface
|
||||
{
|
||||
$calendar = $this->icalService->createContingentsFeed($hotel);
|
||||
|
||||
// An empty calendar would show the portals every day as free; an error keeps their last import
|
||||
if (null === $calendar) {
|
||||
throw new PropagateResponseException(
|
||||
$this->responseFactory->createResponse(503)
|
||||
->withHeader('Content-Type', 'text/plain; charset=utf-8')
|
||||
->withHeader('Cache-Control', 'private, no-store')
|
||||
->withHeader('Retry-After', '900')
|
||||
->withBody($this->streamFactory->createStream('Availability temporarily unavailable')),
|
||||
1761730800
|
||||
);
|
||||
}
|
||||
|
||||
throw new PropagateResponseException(
|
||||
$this->responseFactory->createResponse()
|
||||
->withHeader('Content-Type', 'text/calendar;charset=UTF-8')
|
||||
->withHeader('Content-Disposition', sprintf('attachment; filename=%s.ical', $hotel->getPathSegment()))
|
||||
->withHeader('Cache-Control', 'private, no-store')
|
||||
->withBody($this->streamFactory->createStream($calendar)),
|
||||
1761730801
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,10 +91,6 @@ class ProductController extends ActionController
|
||||
$isProductWithGroupInquiryForm = ($concept !== null) && \in_array($concept->getUid(), $groupInquiryFormConceptUids);
|
||||
$nonBookableInfo = $product->getNonBookableInfo();
|
||||
|
||||
// Assemble backlink
|
||||
$backlinkUrl = $this->request->getQueryParams()['ref'] ?? null;
|
||||
$backLinkLabel = strpos((string)$backlinkUrl, 'suche') !== -1 ? 'zurück zum Suchergebnis' : 'zurück';
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'hotel' => $hotel,
|
||||
'product' => $product,
|
||||
@@ -106,8 +102,6 @@ class ProductController extends ActionController
|
||||
'currentPageUid' => $this->request->getAttribute('frontend.page.information')->getId(),
|
||||
'dateFrom' => $dateFrom,
|
||||
'dateTo' => $dateTo,
|
||||
'referringUrl' => $backlinkUrl,
|
||||
'backlinkLabel' => $backLinkLabel,
|
||||
'nonBookableInfo' => $nonBookableInfo,
|
||||
'pickupsOverview' => $this->pickupOverviewService->getOverview($product, $hotel),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EP\EpProducts\Dashboard;
|
||||
|
||||
use EP\EpProducts\MyEP\ApiClient;
|
||||
use EP\EpProducts\Service\ApiImportSource;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Dashboard\Widgets\RequestAwareWidgetInterface;
|
||||
use TYPO3\CMS\Dashboard\Widgets\WidgetConfigurationInterface;
|
||||
use TYPO3\CMS\Dashboard\Widgets\WidgetInterface;
|
||||
|
||||
/**
|
||||
* When the imports last ran and what data they brought, replacing the TYPO3 10 system information entry
|
||||
* "Letzer Produktdaten-Import"
|
||||
*/
|
||||
final class ImportStatusWidget implements WidgetInterface, RequestAwareWidgetInterface
|
||||
{
|
||||
/**
|
||||
* Command => label; the scheduler tasks are found by the command they run, not by uid
|
||||
*/
|
||||
private const array COMMANDS = [
|
||||
'ep:import-dates' => 'Reisedaten (MyE&P)',
|
||||
'ep:import-snowreport' => 'Schneehöhen',
|
||||
'ep:groups-swiss' => 'groups.swiss',
|
||||
];
|
||||
|
||||
/**
|
||||
* A task whose next run is this far in the past means scheduler:run is not called any more
|
||||
*/
|
||||
private const int OVERDUE_SECONDS = 3600;
|
||||
|
||||
private ServerRequestInterface $request;
|
||||
|
||||
public function __construct(
|
||||
private readonly WidgetConfigurationInterface $configuration,
|
||||
private readonly BackendViewFactory $backendViewFactory,
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
private readonly ApiImportSource $importSource,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly array $options = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public function setRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function renderWidgetContent(): string
|
||||
{
|
||||
$tasks = $this->findTasks();
|
||||
$rows = [];
|
||||
foreach (self::COMMANDS as $command => $label) {
|
||||
$rows[$command] = ['label' => $label, 'dataAt' => null, 'task' => $tasks[$command] ?? null];
|
||||
}
|
||||
$rows['ep:import-dates']['dataAt'] = $this->importSource->getImportedTimestamp()?->getTimestamp();
|
||||
$rows['ep:import-snowreport']['dataAt'] = $this->findLastSnowreportUpdate();
|
||||
|
||||
$myEpLastUpdate = null;
|
||||
$myEpUnreachable = false;
|
||||
try {
|
||||
$myEpLastUpdate = $this->apiClient->getLastUpdateAt()?->getTimestamp();
|
||||
} catch (\Throwable) {
|
||||
$myEpUnreachable = true;
|
||||
}
|
||||
|
||||
$view = $this->backendViewFactory->create($this->request, ['typo3/cms-dashboard', 'ep/ep-products']);
|
||||
$view->assignMultiple([
|
||||
'rows' => $rows,
|
||||
'myEpLastUpdate' => $myEpLastUpdate,
|
||||
'myEpUnreachable' => $myEpUnreachable,
|
||||
'importPending' => null !== $myEpLastUpdate && $myEpLastUpdate > (int)$rows['ep:import-dates']['dataAt'],
|
||||
'configuration' => $this->configuration,
|
||||
]);
|
||||
|
||||
return $view->render('Widget/ImportStatusWidget');
|
||||
}
|
||||
|
||||
public function getOptions(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{uid: int, disabled: bool, lastRun: int, nextRun: int, overdue: bool, failure: ?string}>
|
||||
*/
|
||||
private function findTasks(): array
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_scheduler_task');
|
||||
$result = $queryBuilder
|
||||
->select('uid', 'disable', 'lastexecution_time', 'lastexecution_failure', 'nextexecution', 'serialized_task_object')
|
||||
->from('tx_scheduler_task')
|
||||
->where($queryBuilder->expr()->eq('deleted', 0))
|
||||
->orderBy('uid')
|
||||
->executeQuery();
|
||||
|
||||
$tasks = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
foreach (array_keys(self::COMMANDS) as $command) {
|
||||
// The command identifier appears in the serialized ExecuteSchedulableCommandTask exactly as
|
||||
// serialize() writes the string; matching it avoids unserializing task objects
|
||||
if (!isset($tasks[$command]) && str_contains((string)$row['serialized_task_object'], serialize($command))) {
|
||||
$tasks[$command] = [
|
||||
'uid' => (int)$row['uid'],
|
||||
'disabled' => (bool)$row['disable'],
|
||||
'lastRun' => (int)$row['lastexecution_time'],
|
||||
'nextRun' => (int)$row['nextexecution'],
|
||||
'overdue' => !$row['disable'] && 0 < (int)$row['nextexecution']
|
||||
&& (int)$row['nextexecution'] < time() - self::OVERDUE_SECONDS,
|
||||
'failure' => $this->failureMessage((string)$row['lastexecution_failure']),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $tasks;
|
||||
}
|
||||
|
||||
private function failureMessage(string $failure): ?string
|
||||
{
|
||||
if ('' === $failure) {
|
||||
return null;
|
||||
}
|
||||
// Written by the scheduler as serialize(['code' => …, 'message' => …, …])
|
||||
$data = @unserialize($failure, ['allowed_classes' => false]);
|
||||
|
||||
return is_array($data) && '' !== (string)($data['message'] ?? '') ? (string)$data['message'] : 'Unbekannter Fehler';
|
||||
}
|
||||
|
||||
private function findLastSnowreportUpdate(): ?int
|
||||
{
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_epproducts_domain_model_snowreport');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$tstamp = $queryBuilder
|
||||
->selectLiteral('MAX(tstamp)')
|
||||
->from('tx_epproducts_domain_model_snowreport')
|
||||
->executeQuery()
|
||||
->fetchOne();
|
||||
|
||||
return $tstamp ? (int)$tstamp : null;
|
||||
}
|
||||
}
|
||||
@@ -279,6 +279,11 @@ class Product extends AbstractEntity implements TeaserInterface
|
||||
*/
|
||||
protected $nonBookableInfo;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $popupText = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initStorageObjects();
|
||||
@@ -1146,6 +1151,18 @@ class Product extends AbstractEntity implements TeaserInterface
|
||||
$this->tourThumbnail = $tourThumbnail;
|
||||
}
|
||||
|
||||
public function getPopupText()
|
||||
{
|
||||
return $this->popupText;
|
||||
}
|
||||
|
||||
public function setPopupText($popupText): static
|
||||
{
|
||||
$this->popupText = $popupText;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTourLink()
|
||||
{
|
||||
return $this->tourLink;
|
||||
|
||||
@@ -43,6 +43,10 @@ class ApiMiddleware implements MiddlewareInterface
|
||||
return $this->handleProductRequest($request);
|
||||
}
|
||||
|
||||
if ('/api/calendar-hotels' === $path) {
|
||||
return $this->handleCalendarHotelsRequest($request);
|
||||
}
|
||||
|
||||
if (str_starts_with($path, ContentFeedApi::PATH_PREFIX)) {
|
||||
return $this->contentFeedApi->handle($request);
|
||||
}
|
||||
@@ -97,6 +101,51 @@ class ApiMiddleware implements MiddlewareInterface
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The calendar hotels of all visible products, for the contingent sync of MyE&P: their
|
||||
* availability is needed for the iCal feed and the availability calendar, whether or not
|
||||
* MyE&P has an accommodation with prices for them.
|
||||
*/
|
||||
private function handleCalendarHotelsRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (!$this->authenticator->isAuthorized($request)) {
|
||||
return new JsonResponse(['message' => 'Invalid or missing api key'], 403);
|
||||
}
|
||||
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('tx_epproducts_domain_model_product');
|
||||
|
||||
$rows = $queryBuilder
|
||||
->select('hotel.code', 'hotel.name AS hotelName', 'product.uid', 'product.name', 'product.season')
|
||||
->from('tx_epproducts_domain_model_product', 'product')
|
||||
->join('product', 'tx_epproducts_domain_model_hotel', 'hotel', 'hotel.uid = product.calendar_hotel')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('product.sys_language_uid', 0),
|
||||
$queryBuilder->expr()->neq('hotel.code', $queryBuilder->createNamedParameter('')),
|
||||
)
|
||||
->orderBy('hotel.code')
|
||||
->addOrderBy('product.uid')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$hotels = [];
|
||||
foreach ($rows as $row) {
|
||||
$code = trim((string)$row['code']);
|
||||
$hotels[$code] ??= [
|
||||
'code' => $code,
|
||||
'name' => trim((string)preg_replace('/\s+/', ' ', (string)$row['hotelName'])),
|
||||
'products' => [],
|
||||
];
|
||||
$hotels[$code]['products'][] = [
|
||||
'uid' => (int)$row['uid'],
|
||||
'name' => trim((string)preg_replace('/\s+/', ' ', (string)$row['name'])),
|
||||
'season' => '' !== (string)$row['season'] ? (string)$row['season'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
return new JsonResponse(array_values($hotels));
|
||||
}
|
||||
|
||||
private function fetchProductData(string $productCode): ?array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
@@ -114,9 +163,9 @@ class ApiMiddleware implements MiddlewareInterface
|
||||
->from('tx_epproducts_domain_model_product', 'product')
|
||||
->leftJoin('product', 'tx_epproducts_domain_model_country', 'country', 'product.country = country.uid')
|
||||
->leftJoin('product', 'tx_epproducts_domain_model_city', 'city', 'product.city = city.uid')
|
||||
->leftJoin('product', 'tx_epproducts_domain_model_matchcode', 'matchcode', 'product.uid = matchcode.entity AND matchcode.entity_type="hotel"')
|
||||
->where($queryBuilder->expr()->eq('product.code', $queryBuilder->createNamedParameter($productCode)))
|
||||
->orWhere($queryBuilder->expr()->eq('matchcode.code', $queryBuilder->createNamedParameter($productCode)))->setMaxResults(1)->executeQuery()
|
||||
->orderBy('product.uid')
|
||||
->setMaxResults(1)->executeQuery()
|
||||
->fetchAssociative()
|
||||
;
|
||||
|
||||
|
||||
@@ -207,6 +207,21 @@ class ApiClient implements LoggerAwareInterface
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* One entry per day the snapshot has ('date', 'status': OK, BLOCKED or ON_REQUEST), today until the
|
||||
* end of the snapshot; unlike contingents/calendar, days without a price keep their status
|
||||
*
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function getContingentAvailability(string $hotelCode): array
|
||||
{
|
||||
return $this->requestJson('GET', 'contingents/availability', [
|
||||
'query' => [
|
||||
'hotelCode' => $hotelCode,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ApiException
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace EP\EpProducts\Service;
|
||||
|
||||
use EP\EpProducts\Domain\Model\Hotel;
|
||||
use EP\EpProducts\MyEP\ApiClient;
|
||||
use EP\EpProducts\MyEP\ApiException;
|
||||
use Kigkonsult\Icalcreator\Vcalendar;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* Contingent feed of a hotel for the booking portals, with the events of TYPO3 10
|
||||
*
|
||||
* The availability comes from MyE&P (contingents/availability, the bpn-connect snapshot without the
|
||||
* price rule of contingents/calendar); TYPO3 10 read it from bpn-connect directly. Every run of
|
||||
* BLOCKED or ON_REQUEST days with one status becomes one "Unavailable" event, as bpn-connect's
|
||||
* ranges did.
|
||||
*/
|
||||
class IcalService implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private const string SUMMARY_UNAVAILABLE = 'Unavailable';
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null the calendar, null when the availability is unknown
|
||||
*/
|
||||
public function createContingentsFeed(Hotel $hotel): ?string
|
||||
{
|
||||
$calendarRows = [];
|
||||
$hotelCode = trim((string)$hotel->getCode());
|
||||
|
||||
if ('' !== $hotelCode) {
|
||||
try {
|
||||
$calendarRows = $this->apiClient->getContingentAvailability($hotelCode);
|
||||
} catch (ApiException $e) {
|
||||
$this->logger?->error(sprintf('iCal: no availability for hotel %s', $hotelCode), ['exception' => $e]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$vcalendar = Vcalendar::factory([Vcalendar::UNIQUE_ID => 'www.ep-reisen.de']);
|
||||
$vcalendar->setXprop(Vcalendar::X_WR_TIMEZONE, 'Europe/Berlin');
|
||||
|
||||
foreach ($this->buildRanges($calendarRows) as $range) {
|
||||
$vcalendar
|
||||
->newVevent()
|
||||
->setDtstart($range['from'])
|
||||
->setDtend($range['to']->modify('+1 day'))
|
||||
->setSummary(self::SUMMARY_UNAVAILABLE);
|
||||
}
|
||||
|
||||
return $vcalendar->vtimezonePopulate()->createCalendar();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{from: \DateTimeImmutable, to: \DateTimeImmutable}> unavailable runs, both ends inclusive
|
||||
*/
|
||||
private function buildRanges(array $calendarRows): array
|
||||
{
|
||||
$rows = [];
|
||||
foreach ($calendarRows as $row) {
|
||||
if (!is_array($row) || !isset($row['date'])) {
|
||||
continue;
|
||||
}
|
||||
$date = new \DateTimeImmutable($row['date']);
|
||||
$rows[$date->format('Y-m-d')] = ['date' => $date, 'status' => $this->normalizeStatus((string)($row['status'] ?? 'OK'))];
|
||||
}
|
||||
ksort($rows);
|
||||
|
||||
$ranges = [];
|
||||
$current = null;
|
||||
$lastDate = null;
|
||||
foreach ($rows as $row) {
|
||||
$isGap = $lastDate instanceof \DateTimeImmutable && $row['date'] > $lastDate->modify('+1 day');
|
||||
if (null !== $current && ($isGap || 'OK' === $row['status'] || $current['status'] !== $row['status'])) {
|
||||
$ranges[] = $current;
|
||||
$current = null;
|
||||
}
|
||||
if ('OK' !== $row['status']) {
|
||||
if (null === $current) {
|
||||
$current = ['from' => $row['date'], 'to' => $row['date'], 'status' => $row['status']];
|
||||
} else {
|
||||
$current['to'] = $row['date'];
|
||||
}
|
||||
}
|
||||
$lastDate = $row['date'];
|
||||
}
|
||||
if (null !== $current) {
|
||||
$ranges[] = $current;
|
||||
}
|
||||
|
||||
return array_map(fn (array $range): array => ['from' => $range['from'], 'to' => $range['to']], $ranges);
|
||||
}
|
||||
|
||||
private function normalizeStatus(string $status): string
|
||||
{
|
||||
$status = strtoupper(trim($status));
|
||||
if ('ONREQUEST' === $status) {
|
||||
return 'ON_REQUEST';
|
||||
}
|
||||
|
||||
return in_array($status, ['OK', 'ON_REQUEST', 'BLOCKED'], true) ? $status : 'OK';
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,6 @@ class SearchResultService implements SingletonInterface
|
||||
'boardUid' => ($row['boardUid'] ?? null),
|
||||
'dates' => [],
|
||||
'nights' => [],
|
||||
'teaserNights' => [],
|
||||
'rooms' => [],
|
||||
'new' => ($row['new'] ?? null),
|
||||
'skiPassIncluded' => ($row['skiPassIncluded'] ?? null),
|
||||
@@ -199,7 +198,6 @@ class SearchResultService implements SingletonInterface
|
||||
|| $item['minPrice'] > ($row['minPrice'] ?? null)) {
|
||||
$item['minPrice'] = ($row['minPrice'] ?? null);
|
||||
// Teaser-specific offer data must describe the cheapest row.
|
||||
$item['teaserNights'] = [($row['nights'] ?? null)];
|
||||
$item['board'] = ($row['board'] ?? null);
|
||||
$item['boardUid'] = ($row['boardUid'] ?? null);
|
||||
$item['daytrip'] = ($row['daytrip'] ?? null);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EP\EpProducts\Upgrades;
|
||||
|
||||
use TYPO3\CMS\Install\Attribute\UpgradeWizard;
|
||||
use TYPO3\CMS\Install\Updates\AbstractListTypeToCTypeUpdate;
|
||||
|
||||
/**
|
||||
* Turns the ep_products plugin elements (CType "list") into content elements of their own type, as TYPO3 14
|
||||
* no longer knows list_type. The types keep the plugin signatures as names. Also rewrites the
|
||||
* tt_content:list_type permissions of backend groups. Retired plugins stay "list" elements.
|
||||
*/
|
||||
#[UpgradeWizard('epProductsPluginListTypeToCType')]
|
||||
final class PluginListTypeToCTypeUpdate extends AbstractListTypeToCTypeUpdate
|
||||
{
|
||||
private const array PLUGINS = [
|
||||
'epproducts_searchresult',
|
||||
'epproducts_travelinfo',
|
||||
'epproducts_watchlist',
|
||||
'epproducts_product_detail',
|
||||
'epproducts_product_teasergroup',
|
||||
'epproducts_product_pricetable',
|
||||
'epproducts_snowreport_list',
|
||||
'epproducts_country_detail',
|
||||
'epproducts_region_teasergroup',
|
||||
'epproducts_region_detail',
|
||||
'epproducts_city_teasergroup',
|
||||
'epproducts_city_detail',
|
||||
'epproducts_hotel_teasergroup',
|
||||
'epproducts_hotel_teasergroup_nodate',
|
||||
'epproducts_hotel_detail',
|
||||
'epproducts_hotel_detail_event',
|
||||
'epproducts_concept_teasergroup',
|
||||
'epproducts_concept_detail',
|
||||
'epproducts_teammembers',
|
||||
'epproducts_faq',
|
||||
'epproducts_newsletter_subscription',
|
||||
'epproducts_maps',
|
||||
'epproducts_pricetable',
|
||||
];
|
||||
|
||||
protected function getListTypeToCTypeMapping(): array
|
||||
{
|
||||
return array_combine(self::PLUGINS, self::PLUGINS);
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return 'E&P Produkte: Plugins als eigene Inhaltstypen';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Migrates the ep_products plugin elements from list_type to CType, including backend group permissions.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
'epImports' => [
|
||||
'title' => 'Importe',
|
||||
'description' => 'Import-Status von Reisedaten, Schneehöhen und groups.swiss',
|
||||
'iconIdentifier' => 'ep-logo',
|
||||
'defaultWidgets' => ['epImportStatus'],
|
||||
'showInWizard' => true,
|
||||
],
|
||||
];
|
||||
@@ -9,4 +9,9 @@ return [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:ep_products/Resources/Public/Icons/logo.svg',
|
||||
],
|
||||
// Content element types of the plugins, the logo of the extension manager
|
||||
'ep-products-plugin' => [
|
||||
'provider' => SvgIconProvider::class,
|
||||
'source' => 'EXT:ep_products/Resources/Public/Icons/Extension.svg',
|
||||
],
|
||||
];
|
||||
@@ -72,3 +72,14 @@ services:
|
||||
$hotelImageService: '@ep_products.image_service.hotel'
|
||||
$productImageService: '@ep_products.image_service.product'
|
||||
$regionImageService: '@ep_products.image_service.region'
|
||||
|
||||
EP\EpProducts\Dashboard\ImportStatusWidget:
|
||||
tags:
|
||||
- name: dashboard.widget
|
||||
identifier: 'epImportStatus'
|
||||
groupNames: 'systemInfo'
|
||||
title: 'Import-Status'
|
||||
description: 'Wann Reisedaten, Schneehöhen und groups.swiss zuletzt gelaufen sind und welchen Stand die Daten haben'
|
||||
iconIdentifier: 'ep-logo'
|
||||
height: 'medium'
|
||||
width: 'large'
|
||||
@@ -12,247 +12,199 @@ call_user_func(function (): void {
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'searchresult',
|
||||
'Suchergebnis'
|
||||
'Suchergebnis',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'travelinfo',
|
||||
'Reiseinformationen'
|
||||
'Reiseinformationen',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'watchlist',
|
||||
'Merkliste'
|
||||
'Merkliste',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'product_detail',
|
||||
'Produkt: Details'
|
||||
'Produkt: Details',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'product_teasergroup',
|
||||
'Produkt: Teaser'
|
||||
'Produkt: Teaser',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'product_pricetable',
|
||||
'Buchungstabelle'
|
||||
'Buchungstabelle',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'snowreport_list',
|
||||
'Schneehöhen'
|
||||
'Schneehöhen',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'country_detail',
|
||||
'Land'
|
||||
'Land',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'region_teasergroup',
|
||||
'Gebiet: Teaser'
|
||||
'Gebiet: Teaser',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'region_detail',
|
||||
'Gebiet: Details'
|
||||
'Gebiet: Details',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'city_teasergroup',
|
||||
'Ort: Teaser'
|
||||
'Ort: Teaser',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'city_detail',
|
||||
'Ort: Details'
|
||||
'Ort: Details',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'hotel_teasergroup',
|
||||
'Hotel: Teaser'
|
||||
'Hotel: Teaser',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'hotel_teasergroup_nodate',
|
||||
'Hotel: Teaser (terminunabhängig)'
|
||||
'Hotel: Teaser (terminunabhängig)',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'hotel_detail',
|
||||
'Hotel: Details'
|
||||
'Hotel: Details',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'hotel_detail_event',
|
||||
'Hotel: Details (Event)'
|
||||
'Hotel: Details (Event)',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'concept_teasergroup',
|
||||
'Konzept: Teaser'
|
||||
'Konzept: Teaser',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'concept_detail',
|
||||
'Konzept: Details'
|
||||
'Konzept: Details',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'teammembers',
|
||||
'Team'
|
||||
'Team',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'faq',
|
||||
'FAQ'
|
||||
'FAQ',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'newsletter_subscription',
|
||||
'Newsletter-/RA-Registrierung'
|
||||
'Newsletter-/RA-Registrierung',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'maps',
|
||||
'Übersichtskarte Hotels/Produkte'
|
||||
'Übersichtskarte Hotels/Produkte',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
ExtensionUtility::registerPlugin(
|
||||
'EpProducts',
|
||||
'pricetable',
|
||||
'Preistabelle'
|
||||
'Preistabelle',
|
||||
'ep-products-plugin'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_product_detail'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_product_detail',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_product_detail.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_product_teasergroup'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_product_teasergroup',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_product_teaser.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_product_pricetable'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_product_pricetable',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_product_pricetable.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_snowreport_list'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_snowreport_list',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_snowreport.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_country_detail'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_country_detail',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_country.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_region_teasergroup'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_region_teasergroup',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_region_teaser.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_region_detail'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_region_detail',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_region_detail.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_city_teasergroup'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_city_teasergroup',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_city_teaser.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_city_detail'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_city_detail',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_city_detail.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_hotel_teasergroup'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_hotel_teasergroup',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_teaser.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_hotel_teasergroup_nodate'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_hotel_teasergroup_nodate',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_teaser_date_independent.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_hotel_detail'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_hotel_detail',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_detail.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_hotel_detail_event'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_hotel_detail_event',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_detail_event.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_concept_detail'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_concept_detail',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_concept_detail.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_faq'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_faq',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_faq.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_maps'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_maps',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_maps.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_pricetable'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_pricetable',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_pricetable.xml'
|
||||
);
|
||||
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['epproducts_teammembers'] = 'pi_flexform';
|
||||
ExtensionManagementUtility::addPiFlexFormValue(
|
||||
'epproducts_teammembers',
|
||||
'FILE:EXT:ep_products/Configuration/FlexForms/flexform_teammembers.xml'
|
||||
);
|
||||
// Content element types since 2026-10 (upgrade wizard epProductsPluginListTypeToCType); the form keeps the
|
||||
// fields of the former "list" type, with the plugin options in place of the plugin selector
|
||||
$flexForms = [
|
||||
'epproducts_product_detail' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_product_detail.xml',
|
||||
'epproducts_product_teasergroup' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_product_teaser.xml',
|
||||
'epproducts_product_pricetable' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_product_pricetable.xml',
|
||||
'epproducts_snowreport_list' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_snowreport.xml',
|
||||
'epproducts_country_detail' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_country.xml',
|
||||
'epproducts_region_teasergroup' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_region_teaser.xml',
|
||||
'epproducts_region_detail' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_region_detail.xml',
|
||||
'epproducts_city_teasergroup' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_city_teaser.xml',
|
||||
'epproducts_city_detail' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_city_detail.xml',
|
||||
'epproducts_hotel_teasergroup' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_teaser.xml',
|
||||
'epproducts_hotel_teasergroup_nodate' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_teaser_date_independent.xml',
|
||||
'epproducts_hotel_detail' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_detail.xml',
|
||||
'epproducts_hotel_detail_event' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_hotel_detail_event.xml',
|
||||
'epproducts_concept_detail' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_concept_detail.xml',
|
||||
'epproducts_faq' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_faq.xml',
|
||||
'epproducts_maps' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_maps.xml',
|
||||
'epproducts_pricetable' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_pricetable.xml',
|
||||
'epproducts_teammembers' => 'FILE:EXT:ep_products/Configuration/FlexForms/flexform_teammembers.xml',
|
||||
];
|
||||
foreach ($GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'] as $item) {
|
||||
$cType = (string)($item['value'] ?? '');
|
||||
if (!str_starts_with($cType, 'epproducts_')) {
|
||||
continue;
|
||||
}
|
||||
$GLOBALS['TCA']['tt_content']['types'][$cType]['showitem'] = str_replace(
|
||||
'list_type;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:list_type_formlabel,',
|
||||
isset($flexForms[$cType]) ? 'pi_flexform,' : '',
|
||||
$GLOBALS['TCA']['tt_content']['types']['list']['showitem']
|
||||
);
|
||||
if (isset($flexForms[$cType])) {
|
||||
ExtensionManagementUtility::addPiFlexFormValue('*', $flexForms[$cType], $cType);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -41,6 +41,7 @@ return [
|
||||
tour_thumbnail,
|
||||
--div--;Bewertung, --palette--;;rating, testimonials,
|
||||
--div--;Buchbarkeit, non_bookable_dates,
|
||||
--div--;Popup, popup_text,
|
||||
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language, sys_language_uid, l10n_parent',
|
||||
],
|
||||
],
|
||||
@@ -876,6 +877,18 @@ return [
|
||||
'maxitems' => 1,
|
||||
],
|
||||
],
|
||||
'popup_text' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Popup Text',
|
||||
'description' => 'Ersetzt für dieses Produkt den Popup-Text des Konzepts. Leer: Text des Konzepts. Ob ein Popup erscheint, legt weiterhin das Konzept fest.',
|
||||
'config' => [
|
||||
'type' => 'text',
|
||||
'cols' => 40,
|
||||
'rows' => 5,
|
||||
'eval' => 'trim',
|
||||
'enableRichtext' => true,
|
||||
],
|
||||
],
|
||||
'non_bookable_info' => [
|
||||
'exclude' => 0,
|
||||
'label' => 'Infobox nicht buchbar',
|
||||
|
||||
@@ -232,6 +232,7 @@ tx_epproducts_ical {
|
||||
xhtml_cleaning = 0
|
||||
admPanel = 0
|
||||
debug = 0
|
||||
tx_staticfilecache.disableCache = 1
|
||||
}
|
||||
|
||||
10 = USER
|
||||
@@ -239,14 +240,8 @@ tx_epproducts_ical {
|
||||
userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
|
||||
extensionName = EpProducts
|
||||
pluginName = ical
|
||||
vendorName = EP
|
||||
controller = Ical
|
||||
action = contingents
|
||||
switchableControllerActions {
|
||||
Ical {
|
||||
1 = contingents
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
options.dashboard.dashboardPresetsForNewUsers = default, epImports
|
||||
@@ -1,17 +0,0 @@
|
||||
Queries for plugin renaming
|
||||
===========================
|
||||
|
||||
```mysql
|
||||
update tt_content set list_type='epproducts_searchbar' where list_type='epproducts_pi1';
|
||||
update tt_content set list_type='epproducts_product_detail' where list_type='epproducts_pi3';
|
||||
update tt_content set list_type='epproducts_searchresult' where list_type='epproducts_pi2';
|
||||
update tt_content set list_type='epproducts_product_teasergroup' where list_type='epproducts_pi4';
|
||||
update tt_content set list_type='epproducts_country_detail' where list_type='epproducts_pi5';
|
||||
update tt_content set list_type='epproducts_region_teasergroup' where list_type='epproducts_pi6';
|
||||
update tt_content set list_type='epproducts_region_detail' where list_type='epproducts_pi10';
|
||||
update tt_content set list_type='epproducts_city_teasergroup' where list_type='epproducts_pi7';
|
||||
update tt_content set list_type='epproducts_city_detail' where list_type='epproducts_pi11';
|
||||
update tt_content set list_type='epproducts_hotel_teasergroup' where list_type='epproducts_pi8';
|
||||
update tt_content set list_type='epproducts_hotel_detail' where list_type='epproducts_pi12';
|
||||
update tt_content set list_type='epproducts_faq' where list_type='epproducts_pi9';
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
|
||||
<f:layout name="Widget/Widget" />
|
||||
<f:section name="main">
|
||||
|
||||
<table class="table table-sm table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Import</th>
|
||||
<th>Datenstand</th>
|
||||
<th>Letzter Lauf</th>
|
||||
<th>Nächster Lauf</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<f:for each="{rows}" as="row">
|
||||
<tr>
|
||||
<td>{row.label}</td>
|
||||
<td>
|
||||
<f:if condition="{row.dataAt}" then="{row.dataAt -> f:format.date(format: 'd.m.Y H:i')}" else="–" />
|
||||
</td>
|
||||
<f:if condition="{row.task}">
|
||||
<f:then>
|
||||
<td>
|
||||
<f:if condition="{row.task.lastRun}" then="{row.task.lastRun -> f:format.date(format: 'd.m.Y H:i')}" else="–" />
|
||||
</td>
|
||||
<td>
|
||||
<f:if condition="{row.task.disabled}" then="–" else="{row.task.nextRun -> f:format.date(format: 'd.m.Y H:i')}" />
|
||||
</td>
|
||||
<td>
|
||||
<f:if condition="{row.task.disabled}">
|
||||
<f:then><span class="badge badge-default">deaktiviert</span></f:then>
|
||||
<f:else if="{row.task.failure}">
|
||||
<span class="badge badge-danger" title="{row.task.failure}">Fehler</span>
|
||||
<div class="text-danger small">{row.task.failure}</div>
|
||||
</f:else>
|
||||
<f:else if="{row.task.overdue}">
|
||||
<span class="badge badge-warning">überfällig</span>
|
||||
<div class="small">Der Scheduler läuft nicht.</div>
|
||||
</f:else>
|
||||
<f:else><span class="badge badge-success">OK</span></f:else>
|
||||
</f:if>
|
||||
</td>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<td>–</td>
|
||||
<td>–</td>
|
||||
<td><span class="badge badge-warning">kein Scheduler-Task</span></td>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</tr>
|
||||
</f:for>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="mt-3 mb-0">
|
||||
Letzter Export in MyE&P:
|
||||
<f:if condition="{myEpUnreachable}">
|
||||
<f:then><span class="badge badge-danger">MyE&P nicht erreichbar</span></f:then>
|
||||
<f:else if="{myEpLastUpdate}">
|
||||
{myEpLastUpdate -> f:format.date(format: 'd.m.Y H:i')}
|
||||
<f:if condition="{importPending}">
|
||||
<span class="badge badge-info">wird mit dem nächsten Lauf importiert</span>
|
||||
</f:if>
|
||||
</f:else>
|
||||
<f:else>–</f:else>
|
||||
</f:if>
|
||||
</p>
|
||||
|
||||
</f:section>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.41421" viewBox="0 -25 124 124" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero"><path d="m107 .3c-36.4 5.5-81.6 19.1-81.6 38.2 0 17.7 41.8 13.2 41.8 13.2s-67 22.3-67-7c0-33.2 84.5-44.4 106.8-44.4z" fill="#0084d4"/><path d="m17 73.5c36.4-5.4 81.6-19.1 81.6-38.2 0-17.7-41.8-13.2-41.8-13.2s67-22.3 67 7c0 33.3-84.5 44.4-106.8 44.4z" fill="#f8c62f"/><path d="m60.7 34.3-12.8 4 12.2.8-.4-2.4z" fill="#0084d4"/><path d="m75.9 35.2-9.2 2.9-3.5 9.7-1.7-11.3 4-10.7 1.7 7.9z" fill="#f8c62f"/></g></svg>
|
||||
|
After Width: | Height: | Size: 593 B |
@@ -11,6 +11,7 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"kigkonsult/icalcreator": "^2.41",
|
||||
"league/oauth2-client": "^2.9",
|
||||
"league/period": "^4.12",
|
||||
"symfony/http-client": "^7.4",
|
||||
|
||||
@@ -40,7 +40,7 @@ bpnConnectApiTimeout = 5
|
||||
# cat=MyEpAPI; type=int; label=Bpn Connect API max total request duration in seconds (0 = unlimited)
|
||||
bpnConnectApiMaxDuration = 15
|
||||
|
||||
# cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints used by MyE&P (/api/hotel, /api/product, /api/content/*); sent as X-Api-Key or ?key=. Empty denies every request.
|
||||
# cat=ContentFeed; type=string; label=API key for the CMS backchannel endpoints (/api/hotel, /api/product, /api/calendar-hotels for MyE&P; /api/content/* for the chatbot; see docs/cms-api.md); sent as X-Api-Key or ?key=. Empty denies every request.
|
||||
cmsApiKey =
|
||||
|
||||
# cat=ContentFeed; type=boolean; label=Serve the chatbot content feed (off: all feed endpoints answer 503)
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
use TYPO3\CMS\Scheduler\Task\TableGarbageCollectionTask;
|
||||
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use EP\EpProducts\Controller\ProductController;
|
||||
use EP\EpProducts\Controller\CityController;
|
||||
use EP\EpProducts\Controller\ConceptController;
|
||||
use EP\EpProducts\Controller\CountryController;
|
||||
use EP\EpProducts\Controller\FaqController;
|
||||
use EP\EpProducts\Controller\HotelController;
|
||||
use EP\EpProducts\Controller\IcalController;
|
||||
use EP\EpProducts\Controller\MapController;
|
||||
use EP\EpProducts\Controller\NewsletterController;
|
||||
use EP\EpProducts\Controller\PricetableController;
|
||||
@@ -53,7 +55,9 @@ $boot = function (): void {
|
||||
'product_detail',
|
||||
[
|
||||
ProductController::class => 'detail,hotelList',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -61,7 +65,9 @@ $boot = function (): void {
|
||||
'product_teasergroup',
|
||||
[
|
||||
ProductController::class => 'teasergroup',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -69,7 +75,9 @@ $boot = function (): void {
|
||||
'product_pricetable',
|
||||
[
|
||||
ProductController::class => 'pricetable',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -77,7 +85,9 @@ $boot = function (): void {
|
||||
'country_detail',
|
||||
[
|
||||
CountryController::class => 'detail',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -85,7 +95,9 @@ $boot = function (): void {
|
||||
'region_teasergroup',
|
||||
[
|
||||
RegionController::class => 'teasergroup',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -93,7 +105,9 @@ $boot = function (): void {
|
||||
'region_detail',
|
||||
[
|
||||
RegionController::class => 'detail',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -101,7 +115,9 @@ $boot = function (): void {
|
||||
'city_teasergroup',
|
||||
[
|
||||
CityController::class => 'teasergroup',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -109,7 +125,9 @@ $boot = function (): void {
|
||||
'city_detail',
|
||||
[
|
||||
CityController::class => 'detail',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -117,7 +135,9 @@ $boot = function (): void {
|
||||
'hotel_teasergroup',
|
||||
[
|
||||
HotelController::class => 'teasergroup',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -125,7 +145,9 @@ $boot = function (): void {
|
||||
'hotel_teasergroup_nodate',
|
||||
[
|
||||
HotelController::class => 'dateIndependentTeasergroup',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -133,7 +155,9 @@ $boot = function (): void {
|
||||
'hotel_detail',
|
||||
[
|
||||
HotelController::class => 'detail',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -141,7 +165,9 @@ $boot = function (): void {
|
||||
'hotel_detail_event',
|
||||
[
|
||||
HotelController::class => 'detailEvent',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -149,7 +175,9 @@ $boot = function (): void {
|
||||
'faq',
|
||||
[
|
||||
FaqController::class => 'detail',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -157,7 +185,9 @@ $boot = function (): void {
|
||||
'concept_teasergroup',
|
||||
[
|
||||
ConceptController::class => 'teasergroup',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -165,7 +195,9 @@ $boot = function (): void {
|
||||
'concept_detail',
|
||||
[
|
||||
ConceptController::class => 'detail',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -173,7 +205,9 @@ $boot = function (): void {
|
||||
'teammembers',
|
||||
[
|
||||
TeamController::class => 'list,show',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -184,7 +218,8 @@ $boot = function (): void {
|
||||
],
|
||||
[
|
||||
NewsletterController::class => 'subscriptionForm,subscriptionFormSubmit',
|
||||
]
|
||||
],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -192,7 +227,9 @@ $boot = function (): void {
|
||||
'maps',
|
||||
[
|
||||
MapController::class => 'general',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -200,6 +237,19 @@ $boot = function (): void {
|
||||
'pricetable',
|
||||
[
|
||||
PricetableController::class => 'event',
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
'EpProducts',
|
||||
'ical',
|
||||
[
|
||||
IcalController::class => 'contingents',
|
||||
],
|
||||
[
|
||||
IcalController::class => 'contingents',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -211,7 +261,8 @@ $boot = function (): void {
|
||||
],
|
||||
[
|
||||
SearchController::class => 'searchresult',
|
||||
]
|
||||
],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
// The search bar in the page header (lib.searchBar), not a content element
|
||||
@@ -220,7 +271,9 @@ $boot = function (): void {
|
||||
'searchbar',
|
||||
[
|
||||
SearchbarController::class => 'index',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -228,7 +281,9 @@ $boot = function (): void {
|
||||
'watchlist',
|
||||
[
|
||||
WatchlistController::class => 'list',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -236,7 +291,9 @@ $boot = function (): void {
|
||||
'travelinfo',
|
||||
[
|
||||
TravelinfoController::class => 'travelInfo,travelCode',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -244,7 +301,9 @@ $boot = function (): void {
|
||||
'snowreport_list',
|
||||
[
|
||||
SnowreportController::class => 'list',
|
||||
]
|
||||
],
|
||||
[],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
ExtensionUtility::configurePlugin(
|
||||
@@ -255,7 +314,7 @@ $boot = function (): void {
|
||||
AjaxFilterPanelController::class => 'index',
|
||||
AjaxWatchlistController::class => 'list',
|
||||
AjaxTableController::class => 'datestable,pricetable,pricetableHtml,eventPricetable',
|
||||
AjaxCalendarController::class => 'priceCalendar,priceTable',
|
||||
AjaxCalendarController::class => 'priceCalendar,priceTable,availability',
|
||||
AjaxTeaserPopupController::class => 'concept',
|
||||
],
|
||||
[
|
||||
@@ -264,9 +323,19 @@ $boot = function (): void {
|
||||
AjaxWatchlistController::class => 'list',
|
||||
AjaxTableController::class => 'datestable,pricetable,pricetableHtml,eventPricetable',
|
||||
AjaxTeaserPopupController::class => 'concept',
|
||||
]
|
||||
],
|
||||
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
|
||||
);
|
||||
|
||||
// Elements not yet migrated by the upgrade wizard epProductsPluginListTypeToCType (deploy first, then run
|
||||
// the wizard) still render: list_type plugin => content element type of the same name
|
||||
$listTypeAliases = '';
|
||||
foreach (array_keys($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions']['EpProducts']['plugins']) as $pluginName) {
|
||||
$signature = 'epproducts_' . strtolower((string)$pluginName);
|
||||
$listTypeAliases .= 'tt_content.list.20.' . $signature . ' < tt_content.' . $signature . ".20\n";
|
||||
}
|
||||
ExtensionManagementUtility::addTypoScript('ep_products', 'setup', $listTypeAliases, 'defaultContentRendering');
|
||||
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = '_gl';
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'gclid';
|
||||
$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']['excludedParameters'][] = 'ref';
|
||||
|
||||
@@ -65,7 +65,8 @@ CREATE TABLE tx_epproducts_domain_model_product
|
||||
badge int(11) unsigned NOT NULL default '0',
|
||||
non_bookable_info int(11) unsigned NOT NULL default '0',
|
||||
non_bookable_dates varchar(255) DEFAULT '' NOT NULL,
|
||||
min_price int(11) unsigned NOT NULL default '0',
|
||||
popup_text text NOT NULL,
|
||||
min_price int(11) unsigned NOT NULL default '0',
|
||||
tstamp int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
crdate int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
cruser_id int(11) unsigned DEFAULT '0' NOT NULL,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
name: ep/theme
|
||||
label: 'EP Theme'
|
||||
@@ -0,0 +1 @@
|
||||
@import 'EXT:ep_theme/Configuration/TypoScript/constants.typoscript'
|
||||
@@ -0,0 +1,646 @@
|
||||
categories:
|
||||
epTheme:
|
||||
label: 'E&P Theme'
|
||||
epTheme.pages:
|
||||
label: 'Seiten und Navigation'
|
||||
parent: epTheme
|
||||
epTheme.contact:
|
||||
label: Kontakt
|
||||
parent: epTheme
|
||||
epTheme.display:
|
||||
label: Darstellung
|
||||
parent: epTheme
|
||||
epTheme.events:
|
||||
label: Events
|
||||
parent: epTheme
|
||||
epTheme.services:
|
||||
label: 'Dienste und Tracking'
|
||||
parent: epTheme
|
||||
epTheme.social:
|
||||
label: 'Social Media'
|
||||
parent: epTheme
|
||||
epTheme.seo:
|
||||
label: 'Sitemap und Feeds'
|
||||
parent: epTheme
|
||||
epTheme.records:
|
||||
label: Datensätze
|
||||
parent: epTheme
|
||||
epProducts:
|
||||
label: 'E&P Produkte'
|
||||
epProducts.storage:
|
||||
label: Speicherorte
|
||||
parent: epProducts
|
||||
epProducts.pages:
|
||||
label: Seiten
|
||||
parent: epProducts
|
||||
epProducts.search:
|
||||
label: 'Suche und Filter'
|
||||
parent: epProducts
|
||||
epProducts.groups:
|
||||
label: Gruppenanfragen
|
||||
parent: epProducts
|
||||
epProducts.misc:
|
||||
label: Sonstiges
|
||||
parent: epProducts
|
||||
|
||||
settings:
|
||||
plugin.tx_eptheme.settings.themekey:
|
||||
label: 'Theme key'
|
||||
type: 'string'
|
||||
default: 'ep'
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.topNavPid:
|
||||
label: 'Top navigation PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.mainNavPid:
|
||||
label: 'Main navigation PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.mobileNavPid:
|
||||
label: 'Mobile navigation PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.footerNavPid:
|
||||
label: 'Footer navigation PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.defaultHomeUid:
|
||||
label: 'Default homepage UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.mainSitePid:
|
||||
label: 'Main site PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.logoLink:
|
||||
label: 'Link to apply to logo (overrides defaultHomeUid)'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.defaultAjaxUid:
|
||||
label: 'Default page UID for ajax requests'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.defaultContactFormPageUid:
|
||||
label: 'Default contact form page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.legalNoticePageUid:
|
||||
label: 'Legal notice page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.termsAndConditionsPageUid:
|
||||
label: 'Terms and conditions page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.dataProtectionPageUid:
|
||||
label: 'Data protection page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.imageCreditsPageUid:
|
||||
label: 'Image credits page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.cookiePolicyPageUid:
|
||||
label: 'Cookie policy page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.faqPageUid:
|
||||
label: 'FAQ page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.infoPageUid:
|
||||
label: 'Info page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.groupsPageUid:
|
||||
label: 'Groups page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.blogDetailPageUid:
|
||||
label: 'Blog detail page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.blogTagListPageUid:
|
||||
label: 'Blog tag list page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.groupsContactFormPageUid:
|
||||
label: 'Groups contact form page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.ratingPageUid:
|
||||
label: 'Rating page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.watchlistPageUid:
|
||||
label: 'Watchlist page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.skipassPageUid:
|
||||
label: 'Skipass info page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.englishPageUid:
|
||||
label: 'English page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.germanPageUid:
|
||||
label: 'German page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.conceptsPageUid:
|
||||
label: 'Concepts page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.myEpPageUid:
|
||||
label: 'My E&P page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.bookingLinksPid:
|
||||
label: 'Booking links PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.groupsPricePopupPageUid:
|
||||
label: 'Groups price popup page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.travelAlertPageUid:
|
||||
label: "'Reisen Alert' page UID"
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.coronaUpdatesPageUid:
|
||||
label: "'Corona Updates' page UID"
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.pages'
|
||||
plugin.tx_eptheme.settings.phoneNumber:
|
||||
label: 'Phone number general'
|
||||
type: 'string'
|
||||
default: '0221 - 272 276 0'
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.whatsApp:
|
||||
label: 'Phone number WhatsApp'
|
||||
type: 'string'
|
||||
default: '0221 - 272 276 0'
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.phoneNumberLink:
|
||||
label: 'Phone number general'
|
||||
type: 'string'
|
||||
default: '+492212722760'
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.whatsAppNumberLink:
|
||||
label: 'Phone number WhatsApp'
|
||||
type: 'string'
|
||||
default: 'https://wa.me/492212722760'
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.businessHoursDays:
|
||||
label: 'Business hours days'
|
||||
type: 'string'
|
||||
default: 'Montag - Freitag'
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.businessHoursTime:
|
||||
label: 'Business hours time'
|
||||
type: 'string'
|
||||
default: '08:30 bis 18:30'
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.contactFormToEmail:
|
||||
label: 'Contact form email to'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.contactFormToName:
|
||||
label: 'Contact form name to'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.enableContactFormAutoreply:
|
||||
label: 'Enable contact form autoreply'
|
||||
type: 'string'
|
||||
default: '0'
|
||||
category: 'epTheme.contact'
|
||||
enum:
|
||||
'0': Nein
|
||||
'1': Ja
|
||||
plugin.tx_eptheme.settings.contactFormAutoreplySubject:
|
||||
label: 'Contact form autoreply subject'
|
||||
type: 'string'
|
||||
default: 'Ihre Anfrage bei E&P Reisen'
|
||||
category: 'epTheme.contact'
|
||||
plugin.tx_eptheme.settings.mapTileUrl:
|
||||
label: 'Map tile URL template (without apiKey)'
|
||||
type: 'string'
|
||||
default: 'https://api-eu.geoapify.com/v1/tile/osm-bright/{z}/{x}/{y}.png'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.mapApiKey:
|
||||
label: 'Map tile API key (Geoapify), restricted by referrer'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.mapAttribution:
|
||||
label: 'Map attribution (Geoapify and OpenStreetMap credits are both required)'
|
||||
type: 'string'
|
||||
default: 'Powered by <a href="https://www.geoapify.com/">Geoapify</a> | © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.teaserTextMaxChars:
|
||||
label: 'Teasertext max characters'
|
||||
type: 'string'
|
||||
default: '120'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.defaultBodyclass:
|
||||
label: 'Default bodyclass'
|
||||
type: 'string'
|
||||
default: 'subpage'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.logoImage:
|
||||
label: 'Logo image'
|
||||
type: 'string'
|
||||
default: 'EXT:ep_theme/Resources/Public/images/logo_wide.svg'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.faviconPath:
|
||||
label: 'Favicon folder (realfavicongenerator set, without leading/trailing slash)'
|
||||
type: 'string'
|
||||
default: 'fileadmin/user_upload/favicon/ep'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.faviconAppTitle:
|
||||
label: 'Home screen title (apple-mobile-web-app-title)'
|
||||
type: 'string'
|
||||
default: 'E&P Reisen und Events'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.forceActiveTopnavUid:
|
||||
label: 'Force active topnav uid'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.bpnBookingUrlTemplateCode:
|
||||
label: 'Booking URL template code'
|
||||
type: 'string'
|
||||
default: 'base'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.searchPageHeaderImage:
|
||||
label: 'Search page header image'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.ratingWidgetLabel:
|
||||
label: 'Rating widget label'
|
||||
type: 'string'
|
||||
default: 'Reisebewertung'
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.hideAreaNavigation:
|
||||
label: 'Hide area/top navigation'
|
||||
type: 'string'
|
||||
default: '0'
|
||||
category: 'epTheme.display'
|
||||
enum:
|
||||
'0': Nein
|
||||
'1': Ja
|
||||
plugin.tx_eptheme.settings.hideSearchbar:
|
||||
label: 'Hide searchbar'
|
||||
type: 'string'
|
||||
default: '0'
|
||||
category: 'epTheme.display'
|
||||
enum:
|
||||
'0': Nein
|
||||
'1': Ja
|
||||
plugin.tx_eptheme.settings.globalConceptCode:
|
||||
label: 'Global concept code'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.display'
|
||||
plugin.tx_eptheme.settings.eventDateDayFrom:
|
||||
label: 'Event date day from'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventDateDayTo:
|
||||
label: 'Event date day to'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventDateMonth:
|
||||
label: 'Event date month'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventHeroClaim:
|
||||
label: 'Event herounit claim'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventHeroDescription:
|
||||
label: 'Event herounit description'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventDisturberHeadline:
|
||||
label: 'Event disturber headline'
|
||||
type: 'string'
|
||||
default: 'Absolut<br>Unschlagbarer<br>Preis'
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventDisturberListitems:
|
||||
label: 'Event disturber list items'
|
||||
type: 'string'
|
||||
default: '7 Übernachtungen in Top Apartments;6 Tage Skipass Val Thoerens;Ski & Boarderweek Event-Ticket'
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventDisturberButtonLabel:
|
||||
label: 'Event disturber button label'
|
||||
type: 'string'
|
||||
default: 'Jetzt ab 249€ buchen'
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.eventDisturberButtonLink:
|
||||
label: 'Event disturber button link'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.forcePartnerIdSelect:
|
||||
label: 'Force Partner ID selection'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
enum:
|
||||
'0': Nein
|
||||
'1': Ja
|
||||
plugin.tx_eptheme.settings.partnerIdSelectLabel:
|
||||
label: 'Partner ID selection label'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.events'
|
||||
plugin.tx_eptheme.settings.cookieFirstScriptUrl:
|
||||
label: 'Cookiefirst Script'
|
||||
type: 'string'
|
||||
default: 'https://consent.cookiefirst.com/sites/ep-reisen.de-fabb1554-8932-4075-85dc-97fa68892918/consent.js'
|
||||
category: 'epTheme.services'
|
||||
plugin.tx_eptheme.settings.googleStaticMapsBaseUrl:
|
||||
label: 'Google Static Maps Base Url'
|
||||
type: 'string'
|
||||
default: 'https://maps.googleapis.com/maps/api/staticmap?key=AIzaSyDDOforD1O_j4xXy2nJybC2yQqbUAw9ClY'
|
||||
category: 'epTheme.services'
|
||||
plugin.tx_eptheme.settings.googleMapsJsApiUrl:
|
||||
label: 'Google Maps Js API Url'
|
||||
type: 'string'
|
||||
default: 'https://maps.googleapis.com/maps/api/js?key=AIzaSyDDOforD1O_j4xXy2nJybC2yQqbUAw9ClY'
|
||||
category: 'epTheme.services'
|
||||
plugin.tx_eptheme.settings.googleMapsApiKey:
|
||||
label: 'Google Maps Js API Url'
|
||||
type: 'string'
|
||||
default: 'AIzaSyDDOforD1O_j4xXy2nJybC2yQqbUAw9ClY'
|
||||
category: 'epTheme.services'
|
||||
plugin.tx_eptheme.settings.canonicalBaseUrl:
|
||||
label: 'Canonical base URL/Domain'
|
||||
type: 'string'
|
||||
default: 'https://www.ep-reisen.de'
|
||||
category: 'epTheme.services'
|
||||
plugin.tx_eptheme.settings.googleSiteVerification:
|
||||
label: 'Google site verification'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.services'
|
||||
plugin.tx_eptheme.settings.googleTagManagerId:
|
||||
label: 'Google Tag Manager ID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.services'
|
||||
plugin.tx_eptheme.settings.facebookUrl:
|
||||
label: 'Facebook URL'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.social'
|
||||
plugin.tx_eptheme.settings.instagramUrl:
|
||||
label: 'Instagram URL'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.social'
|
||||
plugin.tx_eptheme.settings.youtubeUrl:
|
||||
label: 'YouTube URL'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.social'
|
||||
plugin.tx_eptheme.settings.tiktokUrl:
|
||||
label: 'tiktok URL'
|
||||
type: 'string'
|
||||
default: 'https://www.tiktok.com/@ep_reisen'
|
||||
category: 'epTheme.social'
|
||||
plugin.tx_eptheme.settings.facebookViewContentProductUid:
|
||||
label: "Facebook ViewContent 'product uid'"
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.social'
|
||||
plugin.tx_eptheme.settings.facebookViewContentNameInternal:
|
||||
label: "Facebook ViewContent 'name internal'"
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.social'
|
||||
plugin.tx_eptheme.settings.xmlSitemapRootPageUid:
|
||||
label: 'XML sitemap root page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.seo'
|
||||
plugin.tx_eptheme.settings.xmlSitemapExcludeUids:
|
||||
label: 'XML sitemap page UIDs to exclude'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.seo'
|
||||
plugin.tx_eptheme.settings.contentOnlyExcludedCTypes:
|
||||
label: 'CTypes left out of the chatbot content-only page type (quoted, comma-separated)'
|
||||
type: 'string'
|
||||
default: "'list','form_formframework','menu_subpages','teaser_product','teaser_hotel','teaser_concept','teaser_country','teaser_region','ep_container_teasergroup'"
|
||||
category: 'epTheme.seo'
|
||||
plugin.tx_eptheme.settings.sliderStoragePid:
|
||||
label: 'Slider storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.records'
|
||||
plugin.tx_eptheme.settings.teamMemberStoragePid:
|
||||
label: 'Teammember storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.records'
|
||||
plugin.tx_eptheme.settings.disrupterStoragePid:
|
||||
label: 'Disrupter storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.records'
|
||||
plugin.tx_eptheme.settings.showSkipassBadge:
|
||||
label: "Show 'skipass inklusive' badge"
|
||||
type: 'string'
|
||||
default: '1'
|
||||
category: 'epTheme.records'
|
||||
enum:
|
||||
'0': Nein
|
||||
'1': Ja
|
||||
plugin.tx_eptheme.settings.badgeStoragePid:
|
||||
label: 'Custom badge storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epTheme.records'
|
||||
plugin.tx_epproducts.settings.productsStoragePid:
|
||||
label: 'Products storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.hotelsStoragePid:
|
||||
label: 'Hotels storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.conceptsStoragePid:
|
||||
label: 'Concepts storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.factsStoragePid:
|
||||
label: 'Facts storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.faqsStoragePid:
|
||||
label: 'Faqs storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.destinationsStoragePid:
|
||||
label: 'Destinations storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.regionsStoragePid:
|
||||
label: 'Regions storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.citiesStoragePid:
|
||||
label: 'Cities storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.teammemberStoragePid:
|
||||
label: 'Teammember storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.officetipStoragePid:
|
||||
label: 'Officetip storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.snowReportStoragePid:
|
||||
label: 'Snow report storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.persistence.storagePid:
|
||||
label: 'Default storage PID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.storage'
|
||||
plugin.tx_epproducts.settings.defaultSearchPageUid:
|
||||
label: 'Default search page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.pages'
|
||||
plugin.tx_epproducts.settings.defaultDetailPageUid:
|
||||
label: 'Default product detail page UID'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.pages'
|
||||
plugin.tx_epproducts.settings.resellerExportPageUid:
|
||||
label: 'Page UID with reseller export plugin'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.pages'
|
||||
plugin.tx_epproducts.settings.travelInfoPageUid:
|
||||
label: 'Page UID with travel info plugin'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.pages'
|
||||
plugin.tx_epproducts.settings.excludedConceptUids:
|
||||
label: 'Exclude products with concept UIDs from search result (CSV)'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.search'
|
||||
plugin.tx_epproducts.settings.excludedRegionUids:
|
||||
label: 'Exclude regions with these UIDs from filter options (CSV)'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.search'
|
||||
plugin.tx_epproducts.settings.forcedConceptUids:
|
||||
label: 'Force concept UID in all search related parts (CSV)'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.search'
|
||||
plugin.tx_epproducts.settings.noInfoConceptUids:
|
||||
label: "Don't show availability info for products with these concept UIDs"
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.search'
|
||||
plugin.tx_epproducts.settings.groupsPriceToEmail:
|
||||
label: 'Email recipient for groups price inquiries'
|
||||
type: 'string'
|
||||
default: '[email protected]'
|
||||
category: 'epProducts.groups'
|
||||
plugin.tx_epproducts.settings.groupsPriceToName:
|
||||
label: 'Email recipient for groups price inquiries'
|
||||
type: 'string'
|
||||
default: 'EP Reisen'
|
||||
category: 'epProducts.groups'
|
||||
plugin.tx_epproducts.settings.groupsBookingUrl:
|
||||
label: 'Email recipient for groups price inquiries'
|
||||
type: 'string'
|
||||
default: 'https://myep.ddev.site/groups/booking/init'
|
||||
category: 'epProducts.groups'
|
||||
plugin.tx_epproducts.settings.priceTableRoomTypes:
|
||||
label: 'Room types for pricetables (CSV)'
|
||||
type: 'string'
|
||||
default: '1,2,3,4,5,6,7,8,9,10'
|
||||
category: 'epProducts.misc'
|
||||
plugin.tx_epproducts.settings.searchLogIgnoreIps:
|
||||
label: 'Ignore following IPs in searchlog'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.misc'
|
||||
plugin.tx_epproducts.settings.groupInquiryFormConceptUids:
|
||||
label: 'Concept UIDs to trigger inquiry form on product detail page (CSV)'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.misc'
|
||||
plugin.tx_epproducts.settings.myEpUrl:
|
||||
label: 'Per-site MyE&P booking URL override'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.misc'
|
||||
plugin.tx_epproducts.settings.myEpStagingUrl:
|
||||
label: 'Per-site MyE&P staging booking URL override'
|
||||
type: 'string'
|
||||
default: ''
|
||||
category: 'epProducts.misc'
|
||||
@@ -0,0 +1 @@
|
||||
@import 'EXT:ep_theme/Configuration/TypoScript/setup.typoscript'
|
||||
@@ -32,13 +32,18 @@ call_user_func(function (): void {
|
||||
);
|
||||
|
||||
// Column layouts, migrated from gridelements in 2026 (commit 4172ed9). colPos = 200 + 10 × layout + column
|
||||
// ep_products plugins are content element types of their own since 2026-10; "list" stays for retired plugins
|
||||
$plugins = implode(',', array_filter(
|
||||
array_map(static fn(array $item): string => (string)($item['value'] ?? ''), $GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items']),
|
||||
static fn(string $cType): bool => str_starts_with($cType, 'epproducts_')
|
||||
));
|
||||
$menus = 'menu_abstract,menu_categorized_content,menu_categorized_pages,menu_recently_updated,menu_related_pages,menu_section_pages,menu_sitemap,menu_sitemap_pages';
|
||||
$columnContainers = [
|
||||
'ep_container_halves' => ['Spalten 50/50', 'Zwei gleich breite Spalten', 'halves', [[201], [202]]],
|
||||
'ep_container_thirds' => ['Spalten 33/33/33', 'Drei gleich breite Spalten', 'thirds', [[211], [212], [213]]],
|
||||
'ep_container_columns_quarters' => ['Spalten 25/25/25/25', 'Vier gleich breite Spalten', 'quarters', [[221], [222], [223], [224]]],
|
||||
'ep_container_half_and_two_quarters' => ['Spalten 50/25/25', 'Eine breite und zwei schmale Spalten', 'half_and_two_quarters', [[231, 'textmedia,teaser,list,form_formframework'], [232], [233]]],
|
||||
'ep_container_two_quarters_and_half' => ['Spalten 25/25/50', 'Zwei schmale und eine breite Spalte', 'two_quarters_and_half', [[241], [242], [243, 'textmedia,teaser,list,gmap,nlform,form_formframework,menu_pages,menu_subpages,menu_section']]],
|
||||
'ep_container_half_and_two_quarters' => ['Spalten 50/25/25', 'Eine breite und zwei schmale Spalten', 'half_and_two_quarters', [[231, 'textmedia,teaser,list,form_formframework,' . $plugins], [232], [233]]],
|
||||
'ep_container_two_quarters_and_half' => ['Spalten 25/25/50', 'Zwei schmale und eine breite Spalte', 'two_quarters_and_half', [[241], [242], [243, 'textmedia,teaser,list,gmap,nlform,form_formframework,menu_pages,menu_subpages,menu_section,' . $plugins]]],
|
||||
];
|
||||
// No container inside these columns (topLevelLayout of the gridelements configuration)
|
||||
$containers = implode(',', array_merge(['ep_container_quarters', 'ep_container_teasergroup'], array_keys($columnContainers)));
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
# Plugins (the booking widget on product detail pages), forms, sub-page menus and teaser cards
|
||||
# are left out: MyE&P has trip data first-hand, and the pages and records the cards link to are
|
||||
# in the feed in their own right. The filter applies to top-level elements only; a plugin nested
|
||||
# in a grid or container element is still rendered by its parent.
|
||||
# in a grid or container element is still rendered by its parent. The ep_products plugins are content
|
||||
# element types of their own (epproducts_*); "list" in the setting covers the retired plugins.
|
||||
lib.contentOnly < lib.dynamicContent
|
||||
lib.contentOnly.20.select.where = colPos={register:colPos} AND CType NOT IN ({$plugin.tx_eptheme.settings.contentOnlyExcludedCTypes})
|
||||
lib.contentOnly.20.select.where = colPos={register:colPos} AND CType NOT IN ({$plugin.tx_eptheme.settings.contentOnlyExcludedCTypes}) AND CType NOT LIKE 'epproducts_%'
|
||||
|
||||
tx_eptheme_content_only = PAGE
|
||||
tx_eptheme_content_only {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.41421" viewBox="0 -25 124 124" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero"><path d="m107 .3c-36.4 5.5-81.6 19.1-81.6 38.2 0 17.7 41.8 13.2 41.8 13.2s-67 22.3-67-7c0-33.2 84.5-44.4 106.8-44.4z" fill="#0084d4"/><path d="m17 73.5c36.4-5.4 81.6-19.1 81.6-38.2 0-17.7-41.8-13.2-41.8-13.2s67-22.3 67 7c0 33.3-84.5 44.4-106.8 44.4z" fill="#f8c62f"/><path d="m60.7 34.3-12.8 4 12.2.8-.4-2.4z" fill="#0084d4"/><path d="m75.9 35.2-9.2 2.9-3.5 9.7-1.7-11.3 4-10.7 1.7 7.9z" fill="#f8c62f"/></g></svg>
|
||||
|
After Width: | Height: | Size: 593 B |
@@ -0,0 +1,25 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
|
||||
// The page is cached alike for every visitor, so the link back to the visitor's own search is
|
||||
// taken from ?ref= here, and only when it points to this site: anything else, javascript: URLs
|
||||
// included, has a different origin
|
||||
export default class extends Controller {
|
||||
|
||||
connect() {
|
||||
const ref = new URLSearchParams(window.location.search).get('ref')
|
||||
if (null === ref) {
|
||||
return
|
||||
}
|
||||
let url
|
||||
try {
|
||||
url = new URL(ref, window.location.href)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (url.origin !== window.location.origin) {
|
||||
return
|
||||
}
|
||||
this.element.href = url.href
|
||||
this.element.classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,16 @@
|
||||
import { Controller } from '@hotwired/stimulus'
|
||||
import { useFetch } from '../mixins/use_fetch'
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
static targets = [
|
||||
'container',
|
||||
'field',
|
||||
'loadingIndicator',
|
||||
'month',
|
||||
]
|
||||
|
||||
static values = {
|
||||
loading: Boolean,
|
||||
uri: String,
|
||||
hotelUid: Number,
|
||||
}
|
||||
|
||||
initialize() {
|
||||
useFetch(this)
|
||||
}
|
||||
|
||||
connect() {
|
||||
let months = []
|
||||
let today = new Date()
|
||||
let year = today.getFullYear()
|
||||
let month = today.getMonth()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
let selectableDate = new Date(year, month, 1)
|
||||
months.push(selectableDate)
|
||||
if (month < 12) {
|
||||
month++
|
||||
} else {
|
||||
month = 1
|
||||
year++
|
||||
}
|
||||
this.fieldTarget.options[i] = new Option(selectableDate.toLocaleDateString('de-DE', {year: 'numeric', month: 'long'}), i.toString())
|
||||
}
|
||||
this.selectableMonths = months
|
||||
this.load(0)
|
||||
}
|
||||
|
||||
select(e) {
|
||||
let selectedIndex = e.currentTarget.value
|
||||
this.load(selectedIndex)
|
||||
}
|
||||
|
||||
load(selectedIndex) {
|
||||
this.loadingValue = true
|
||||
let selectedMonth = this.selectableMonths[selectedIndex]
|
||||
let data = new URLSearchParams()
|
||||
data.set('tx_epproducts_ajax[hotel]', this.hotelUidValue)
|
||||
data.set('tx_epproducts_ajax[month]', selectedMonth.getMonth() + 1)
|
||||
data.set('tx_epproducts_ajax[year]', selectedMonth.getFullYear())
|
||||
fetch(this.uriValue, { method: 'POST', body: data })
|
||||
.then(this.checkStatus)
|
||||
.then(this.parseHTML)
|
||||
.then(html => (
|
||||
this.containerTarget.innerHTML = html
|
||||
))
|
||||
.finally(() => {
|
||||
this.loadingValue = false
|
||||
})
|
||||
}
|
||||
|
||||
loadingValueChanged() {
|
||||
this.loadingIndicatorTarget.classList.toggle('hidden', this.loadingValue === false)
|
||||
select() {
|
||||
const selectedIndex = parseInt(this.fieldTarget.value, 10)
|
||||
this.monthTargets.forEach((month, index) => {
|
||||
month.classList.toggle('hidden', index !== selectedIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -55,12 +55,10 @@
|
||||
</div>
|
||||
</f:if>
|
||||
</div>
|
||||
<f:if condition="{referringUrl}">
|
||||
<a href="{referringUrl}" class="button bg-button button--small mt-8">
|
||||
{ep:icon(icon: 'chevron-left', class: 'w-4 h-4 text-white')}
|
||||
<span>{backlinkLabel}</span>
|
||||
</a>
|
||||
</f:if>
|
||||
<a class="button bg-button button--small mt-8 hidden" data-controller="backlink">
|
||||
{ep:icon(icon: 'chevron-left', class: 'w-4 h-4 text-white')}
|
||||
<span>zurück zum Suchergebnis</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<f:render partial="Badge" arguments="{badge: product.badge}"/>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<div class="flex-1 flex flex-col space-y-1 px-2 py-4 md:p-4">
|
||||
<a href="{targetUrl}" class="peer"
|
||||
<f:if condition="{concept.popupText} && {settings.globalConceptCode} != {concept.code}">
|
||||
hx-get="{ep:uri.ajax(action: 'concept', controller: 'AjaxTeaserPopup', arguments: '{concept: concept.uid, targetUrl: targetUrl}', format: 'html', pageUid: settings.defaultAjaxUid)}"
|
||||
hx-get="{ep:uri.ajax(action: 'concept', controller: 'AjaxTeaserPopup', arguments: '{concept: concept.uid, product: product.uid, targetUrl: targetUrl}', format: 'html', pageUid: settings.defaultAjaxUid)}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
</f:if>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
country: item.country,
|
||||
region: item.region,
|
||||
productImage: searchProductImage,
|
||||
nights: item.teaserNights,
|
||||
nights: item.nights,
|
||||
board: item.board,
|
||||
minPrice: item.minPrice,
|
||||
daytrip: item.daytrip,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<html data-namespace-typo3-fluid="true" lang="en"
|
||||
xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
|
||||
|
||||
<f:if condition="{error}">
|
||||
<f:then>
|
||||
<div class="text-sm text-red-600">
|
||||
Der Kalender konnte aktuell nicht geladen werden.
|
||||
</div>
|
||||
</f:then>
|
||||
<f:else if="{calendarMonths -> f:count()} == 0">
|
||||
<div class="text-sm">
|
||||
Für die Wintersaison liegen noch keine Belegungsdaten vor.
|
||||
</div>
|
||||
</f:else>
|
||||
<f:else>
|
||||
<div data-controller="calendar">
|
||||
<select class="w-full border-zinc-400 focus:border-zinc-400"
|
||||
data-calendar-target="field"
|
||||
data-action="calendar#select">
|
||||
<f:for each="{calendarMonths}" as="month" iteration="i">
|
||||
<option value="{i.index}"><f:format.date format="%B %Y">{month.date}</f:format.date></option>
|
||||
</f:for>
|
||||
</select>
|
||||
<f:for each="{calendarMonths}" as="month" iteration="i">
|
||||
<table class="min-w-full border border-zinc-400 border-collapse mt-4 mb-0{f:if(condition: i.isFirst, else: ' hidden')}"
|
||||
data-calendar-target="month">
|
||||
<thead>
|
||||
<tr>
|
||||
<f:for each="{0: 'Mo', 1: 'Di', 2: 'Mi', 3: 'Do', 4: 'Fr', 5: 'Sa', 6: 'So'}" as="weekday">
|
||||
<th class="px-1 py-2 border border-zinc-300 text-center">
|
||||
{weekday}
|
||||
</th>
|
||||
</f:for>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<f:for each="{month.weeks}" as="week">
|
||||
<tr>
|
||||
<f:for each="{week}" as="day">
|
||||
<td class="px-1 py-2 border border-zinc-300 text-center">
|
||||
<f:if condition="{day.inMonth}">
|
||||
<f:then>
|
||||
<f:variable name="dateKey" value="{f:format.date(format: 'Y-m-d', date: day.date)}"/>
|
||||
<span class="block p-1 leading-none text-sm {classByDate.{dateKey}}">{f:format.date(format: 'd', date: day.date)}</span>
|
||||
</f:then>
|
||||
<f:else>
|
||||
|
||||
</f:else>
|
||||
</f:if>
|
||||
</td>
|
||||
</f:for>
|
||||
</tr>
|
||||
</f:for>
|
||||
</tbody>
|
||||
</table>
|
||||
</f:for>
|
||||
</div>
|
||||
</f:else>
|
||||
</f:if>
|
||||
|
||||
</html>
|
||||
@@ -65,12 +65,8 @@
|
||||
{f:if(condition: '{row.rooms.{type}.undersubscription}', then: '*')}
|
||||
</f:link.typolink>
|
||||
</f:then>
|
||||
<f:else>
|
||||
<f:if condition="{row.rooms.{type}.available}">
|
||||
<f:else>
|
||||
<span class="line-through text-zinc-500">{row.rooms.{type}.price -> ep:price()}</span>
|
||||
</f:else>
|
||||
</f:if>
|
||||
<f:else if="{row.rooms.{type}.price}">
|
||||
<span class="line-through text-zinc-500">{row.rooms.{type}.price -> ep:price()}</span>
|
||||
</f:else>
|
||||
</f:if>
|
||||
</td>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<h3>
|
||||
{concept.name}
|
||||
</h3>
|
||||
{concept.popupText -> f:format.html()}
|
||||
{popupText -> f:format.html()}
|
||||
<a href="{targetUrl}" target="_blank" class="button bg-button">
|
||||
{concept.popupButtonLabel}
|
||||
</a>
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
<f:if condition="{showGallery}">
|
||||
<f:render section="SidebarGallery" arguments="{_all}"/>
|
||||
</f:if>
|
||||
<f:if condition="{product.calendarHotel} && {product.season} != 's'">
|
||||
<f:render section="AvailabilityCalendar" arguments="{_all}"/>
|
||||
</f:if>
|
||||
<f:if condition="{product.tourLink}">
|
||||
<div class="bg-zinc-50 p-4 mb-4"
|
||||
data-controller="lightbox">
|
||||
@@ -363,6 +366,26 @@
|
||||
</div>
|
||||
</f:section>
|
||||
|
||||
<f:section name="AvailabilityCalendar">
|
||||
<div class="bg-zinc-50 p-4 flex flex-col space-y-2 mb-4">
|
||||
<span class="block font-bold">
|
||||
Belegungskalender
|
||||
</span>
|
||||
<div hx-get="{ep:uri.ajax(action: 'availability', controller: 'AjaxCalendar', extensionName: 'epproducts', pageUid: settings.defaultAjaxUid, arguments: '{hotel: product.calendarHotel}', format: 'html')}"
|
||||
hx-trigger="intersect once">
|
||||
<div class="flex items-center justify-center space-x-4 text-sm p-4">
|
||||
<f:image class="block w-4 h-4" src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif" alt=""/>
|
||||
<span>Belegungskalender wird geladen …</span>
|
||||
</div>
|
||||
</div>
|
||||
<f:if condition="{product.concept.code} == 'gsr'">
|
||||
<f:link.typolink class="button bg-button" parameter="{settings.groupsContactFormPageUid}" additionalParams="&ref={currentPageUid}">
|
||||
Zum Anfrageformular
|
||||
</f:link.typolink>
|
||||
</f:if>
|
||||
</div>
|
||||
</f:section>
|
||||
|
||||
<f:section name="TableLoadingIndicator">
|
||||
<div class="flex items-center justify-center space-x-4 text-sm p-4">
|
||||
<f:image class="block w-4 h-4" src="EXT:ep_theme/Resources/Public/images/ajax-loader-white.gif" alt=""/>
|
||||
|
||||
@@ -8,7 +8,7 @@ $EM_CONF[$_EXTKEY] = [
|
||||
'category' => 'plugins',
|
||||
'author' => 'Björn Fromme',
|
||||
'author_email' => '[email protected]',
|
||||
'state' => 'beta',
|
||||
'state' => 'stable',
|
||||
'dividers2tabs' => true,
|
||||
'version' => '3.0.0',
|
||||
'constraints' => [
|
||||
|
||||
@@ -326,6 +326,13 @@ AddDefaultCharset utf-8
|
||||
RewriteCond %{HTTP_HOST} skiboarderweek\.(de|eu)$ [NC]
|
||||
RewriteRule ^(.*) https://www.ski-boarderweek.de/$1 [L,R=301]
|
||||
|
||||
RewriteCond %{HTTP_HOST} ^(www\.)?boarderweek\.de$ [NC]
|
||||
RewriteRule ^(.*) https://www.ski-boarderweek.de/$1 [L,R=301]
|
||||
|
||||
# Every page goes to the Haus Christiler page on semesterende-skireisen
|
||||
RewriteCond %{HTTP_HOST} ^(www\.)?ahrntal\-christiler\.de$ [NC]
|
||||
RewriteRule ^ https://www.semesterende-skireisen.de/reiseziele/italien/ahrntal/haus-christiler [L,R=301]
|
||||
|
||||
RewriteCond %{HTTP_HOST} ^ski\-wochenenden\.de$ [NC]
|
||||
RewriteRule ^(.*) https://www.ski-wochenenden.de/$1 [L,R=301]
|
||||
|
||||
@@ -336,12 +343,19 @@ AddDefaultCharset utf-8
|
||||
RewriteCond %{HTTP_HOST} ^(www\.)?ep\-events\.de$ [NC]
|
||||
RewriteRule ^ https://www.ep-reisen.de/reisen-fuer-gruppen/gruppenreisen/firmenreisen/ [L,R=301]
|
||||
|
||||
# Every page goes to the Haus Waldschlössli group house page
|
||||
RewriteCond %{HTTP_HOST} ^(www\.)?waldschloessli\-davos\.de$ [NC]
|
||||
RewriteRule ^ https://www.ep-reisen.de/reisen-fuer-gruppen/gruppenhaeuser/gruppenhaeuser-mieten/davos-haus-waldschloessli/ [L,R=301]
|
||||
|
||||
RewriteCond %{HTTP_HOST} ^ep\-reisen\.de$ [NC]
|
||||
RewriteRule ^(.*) https://www.ep-reisen.de/$1 [L,R=301]
|
||||
|
||||
RewriteCond %{HTTP_HOST} ^(www\.)?ep\-reisen\.(com|info)$ [NC]
|
||||
RewriteRule ^(.*) https://www.ep-reisen.de/$1 [L,R=301]
|
||||
|
||||
RewriteCond %{HTTP_HOST} ^(www\.)?epreisen\.de$ [NC]
|
||||
RewriteRule ^(.*) https://www.ep-reisen.de/$1 [L,R=301]
|
||||
|
||||
RewriteCond %{HTTP_HOST} ^(www\.)?schneereisen\.com$ [NC]
|
||||
RewriteRule ^(.*) https://www.ep-reisen.de/$1 [L,R=301]
|
||||
|
||||
|
||||
+2
-1
@@ -30,7 +30,8 @@ module.exports = {
|
||||
'bg-concept-sfl', 'bg-concept-sfl/70',
|
||||
'bg-concept-ser', 'bg-concept-ser/70',
|
||||
'flatpickr-input', 'flatpickr-calendar', 'flatpickr-day', 'flatpickr-day.selected',
|
||||
'availability-full-to-none', 'availability-full-to-partial', 'availability-none-to-full',
|
||||
'availability-full', 'availability-partial', 'availability-none',
|
||||
'availability-full-to-none','availability-full-to-partial', 'availability-none-to-full',
|
||||
'availability-partial-to-full', 'availability-none-to-partial', 'availability-partial-to-none',
|
||||
],
|
||||
theme: {
|
||||
|
||||
@@ -50,6 +50,8 @@ Encore
|
||||
.copyFiles([
|
||||
{ from: './packages/ep_theme/Resources/Private/Assets/images', to: 'images/[name].[ext]' },
|
||||
{ from: './packages/ep_theme/Resources/Private/Assets/images/icons', to: 'images/icons/[name].[ext]' },
|
||||
// TYPO3 takes the extension icon from this fixed path
|
||||
{ from: './packages/ep_theme/Resources/Private/Assets/icons', to: 'Icons/[name].[ext]' },
|
||||
])
|
||||
// The content hash is what lets .htaccess serve these as `immutable` for a year: a new
|
||||
// build is a new URL, so a browser never has to revalidate one. It also replaces TYPO3's
|
||||
|
||||
Reference in new issue
Block a user