Compare commits

...
13 Commits
Author SHA1 Message Date
fromme 11f46d0acf fix: use utf8mb4 for the database connection and new tables 2026-10-20 11:48:10 +02:00
fromme 4279669aa3 fix: add TCA default 0 to relation selects in TCA overrides for MySQL strict mode 2026-10-20 10:16:40 +02:00
fromme aa3d81102e chore: re-activate chatbot in page footer 2026-10-19 09:12:37 +02:00
frommeandClaude Opus 5.5 739a4c49f7 fix: write cachetool temp script to shared typo3temp
PHP workers with a stale realpath cache still resolve the current symlink
to the previous release and returned 404 for the script placed in the new
one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-18 18:21:06 +02:00
fromme 88e0488b49 fix: adopt text orientation in page footer on mobile screens 2026-10-18 18:13:39 +02:00
fromme f57671b4d4 fix: single column in footer menu on mobile screens 2026-10-18 18:11:13 +02:00
fromme 313fb151bc fix: add missing customer code pattern 2026-10-13 17:09:06 +02:00
fromme 6350742084 feat: show pickups table below bus section on product detail pages 2026-10-13 16:10:23 +02:00
fromme ef7d81fe99 feat: remove the feature flag for the pickups table on product detail pages 2026-10-13 14:26:22 +02:00
fromme 0477589a7a feat: fall back to the travel code for dates without a customer selection 2026-10-13 14:26:17 +02:00
fromme 38962845d3 chore: cleanup htaccess 2026-10-08 17:34:15 +02:00
fromme c7f43ad02c feat: add a feature flag for the pickups table on product detail pages 2026-10-08 17:22:21 +02:00
fromme 1111c180f8 feat: hide pickups of group travel dates on product detail pages 2026-10-08 17:20:31 +02:00
14 changed files with 68 additions and 34 deletions

No files matched your search

+4 -1
View File
@@ -70,7 +70,10 @@ host('prod')
->set('writable_mode', 'chmod') ->set('writable_mode', 'chmod')
->set('rsync_src', __DIR__) ->set('rsync_src', __DIR__)
->set('rsync', $rsyncOptions) ->set('rsync', $rsyncOptions)
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{current_path}}/{{typo3_webroot}}/ --web-url=https://www.ep-reisen.de') // The temp script goes into the shared typo3temp, which resolves to the same directory from every
// release: PHP workers with a stale realpath cache still map "current" to the previous release and
// would 404 on a script placed inside the new one.
->set('cachetool_args', '--web=SymfonyHttpClient --web-path={{deploy_path}}/shared/{{typo3_webroot}}/typo3temp/ --web-url=https://www.ep-reisen.de/typo3temp')
; ;
task('deploy', [ task('deploy', [
-9
View File
@@ -285,9 +285,6 @@ AddDefaultCharset utf-8
<FilesMatch "\.(eot|otf|tt[cf]|woff2?)$"> <FilesMatch "\.(eot|otf|tt[cf]|woff2?)$">
Header set Access-Control-Allow-Origin "*" Header set Access-Control-Allow-Origin "*"
</FilesMatch> </FilesMatch>
<FilesMatch "\.(png|gif|jpe?g)$">
Header append Vary Accept
</FilesMatch>
</IfModule> </IfModule>
### End: Cross Origin ### ### End: Cross Origin ###
@@ -395,12 +392,6 @@ AddDefaultCharset utf-8
# NOTE: Add your additional local storages here # NOTE: Add your additional local storages here
RewriteRule ^(?:typo3/|fileadmin/|typo3conf/|typo3temp/|uploads/|favicon\.ico) - [L] RewriteRule ^(?:typo3/|fileadmin/|typo3conf/|typo3temp/|uploads/|favicon\.ico) - [L]
RewriteCond %{HTTP_ACCEPT} image/webp
RewriteCond %{REQUEST_FILENAME} (.*)\.(png|gif|jpe?g)$
RewriteCond %{REQUEST_FILENAME}\.webp -f
RewriteRule ^ %{REQUEST_FILENAME}\.webp [L,T=image/webp]
### Begin: StaticFileCache (preparation) #### ### Begin: StaticFileCache (preparation) ####
# Document root configuration # Document root configuration
@@ -90,6 +90,14 @@ $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['rootline']
// by the CDN, so stop generating them. mod_deflate still compresses direct origin hits. // by the CDN, so stop generating them. mod_deflate still compresses direct origin hits.
$GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] = 0; $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] = 0;
// utf8 in MySQL/MariaDB is utf8mb3 (max. 3 bytes) and rejects 4-byte characters like emoji.
// Set here because LocalConfiguration.php is a Deployer shared file and not deployed from the repo.
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['charset'] = 'utf8mb4';
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['tableoptions'] = [
'charset' => 'utf8mb4',
'collate' => 'utf8mb4_unicode_ci',
];
// suppress php deprecation warnings flooding typo3 logs // suppress php deprecation warnings flooding typo3 logs
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] = $GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] =
$GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] & ~E_DEPRECATED & ~E_USER_DEPRECATED; $GLOBALS['TYPO3_CONF_VARS']['SYS']['errorHandlerErrors'] & ~E_DEPRECATED & ~E_USER_DEPRECATED;
@@ -59,6 +59,7 @@ call_user_func(function() {
'label' => 'Reiseart', 'label' => 'Reiseart',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['Keine Zuordnung', 0], ['Keine Zuordnung', 0],
@@ -74,6 +75,7 @@ call_user_func(function() {
'label' => 'Angebot/Beispielangebot', 'label' => 'Angebot/Beispielangebot',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'items' => [ 'items' => [
['Keine Zuordnung', 0], ['Keine Zuordnung', 0],
@@ -33,6 +33,7 @@ class DateRepository extends AbstractRepository
->where( ->where(
$qb->expr()->eq('date.product', $qb->createNamedParameter($productUid, Connection::PARAM_INT)), $qb->expr()->eq('date.product', $qb->createNamedParameter($productUid, Connection::PARAM_INT)),
$qb->expr()->eq('date.hotel', $qb->createNamedParameter($hotelUid, Connection::PARAM_INT)), $qb->expr()->eq('date.hotel', $qb->createNamedParameter($hotelUid, Connection::PARAM_INT)),
$qb->expr()->eq('date.group_travel', $qb->createNamedParameter(0, Connection::PARAM_INT)),
'date.date_start >= NOW()' 'date.date_start >= NOW()'
) )
->orderBy('date.date_start', 'DESC') ->orderBy('date.date_start', 'DESC')
@@ -73,6 +73,13 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
const SELECTION_CUSTOMER_GROUP = 1418; const SELECTION_CUSTOMER_GROUP = 1418;
const SELECTION_CUSTOMER_END = 1419; const SELECTION_CUSTOMER_END = 1419;
/**
* Fallback for dates the office has not tagged with either selection: these
* trip type markers in front of the travel code's trailing date mean
* end-customer travel.
*/
const END_CUSTOMER_CODE_PATTERN = '/(?:MP|WE|KT|PM|MW|FA)\d{6}$/';
/** /**
* 'Leistungen Personal' occurs in no export so far, so its id is unknown and * 'Leistungen Personal' occurs in no export so far, so its id is unknown and
* it is still matched by label. Register its id once the warning fires. * it is still matched by label. Register its id once the warning fires.
@@ -422,8 +429,7 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$skiPassIncluded = (int) (count($includedSkiPass) > 0); $skiPassIncluded = (int) (count($includedSkiPass) > 0);
$date['skipass_included'] = $skiPassIncluded; $date['skipass_included'] = $skiPassIncluded;
$customer = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_CUSTOMER); $date['group_travel'] = (int) $this->isGroupTravel($row);
$date['group_travel'] = (int) isset($customer[static::SELECTION_CUSTOMER_GROUP]);
$servicesTransportation = $this->parseService($row['transportServices']); $servicesTransportation = $this->parseService($row['transportServices']);
$discount = $this->determineDiscount($servicesTransportation); $discount = $this->determineDiscount($servicesTransportation);
@@ -787,8 +793,27 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
} }
/** /**
* A date without 'Gruppenkunde' imports as end-customer travel, which is * 'Gruppenkunde' and 'Endkunde' decide. A date carrying neither is judged by
* only right when BusPro says 'Endkunde'. * its travel code.
*
* @param array $row
* @return bool
*/
protected function isGroupTravel(array $row): bool
{
$customer = $this->parseSelectionGroup($row['selectionGroups'], static::SELECTION_GROUP_CUSTOMER);
$isGroup = isset($customer[static::SELECTION_CUSTOMER_GROUP]);
$isEnd = isset($customer[static::SELECTION_CUSTOMER_END]);
if ($isGroup || $isEnd) {
return $isGroup;
}
return !preg_match(static::END_CUSTOMER_CODE_PATTERN, (string) $row['date']['code']);
}
/**
* Reports dates whose customer selection is missing or contradictory, along
* with what they import as.
* *
* @param array $row * @param array $row
*/ */
@@ -804,7 +829,8 @@ class DateImportService implements SingletonInterface, LoggerAwareInterface
$this->logger->warning( $this->logger->warning(
$isGroup $isGroup
? 'date has both Gruppenkunde and Endkunde selected, importing as group travel' ? 'date has both Gruppenkunde and Endkunde selected, importing as group travel'
: 'date has neither Gruppenkunde nor Endkunde selected, importing as end-customer travel', : 'date has neither Gruppenkunde nor Endkunde selected, falling back to travel code, importing as '
. ($this->isGroupTravel($row) ? 'group travel' : 'end-customer travel'),
['product' => $row['product']['code'], 'date' => $row['date']['code']] ['product' => $row['product']['code'], 'date' => $row['date']['code']]
); );
} }
@@ -7,10 +7,12 @@ use EP\EpProducts\Domain\Model\Product;
use EP\EpProducts\Domain\Repository\DateRepository; use EP\EpProducts\Domain\Repository\DateRepository;
/** /**
* Lists the pickup points of a product's latest upcoming date that has any, * Lists the pickup points of a product's latest upcoming end-customer date that
* with that date's time and round-trip surcharge per pickup point. Pickups, * has any, with that date's time and round-trip surcharge per pickup point.
* times and prices may differ on other dates; taking the latest one keeps the * Group travel dates are left out: their pickups are arranged per group and
* values unique and shows them for as long as the product is offered. * are not offered publicly. Pickups, times and prices may differ on other
* dates; taking the latest one keeps the values unique and shows them for as
* long as the product is offered.
* *
* The returned shape is meant to be served by MyE&P later on, so that only the * The returned shape is meant to be served by MyE&P later on, so that only the
* data source has to change once the local date table goes away. * data source has to change once the local date table goes away.
@@ -403,6 +403,7 @@ return [
'label' => 'Produkte', 'label' => 'Produkte',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectMultipleSideBySide', 'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_epproducts_domain_model_product', 'foreign_table' => 'tx_epproducts_domain_model_product',
'size' => 10, 'size' => 10,
@@ -24,6 +24,7 @@ call_user_func(function() {
'label' => 'Land', 'label' => 'Land',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_country', 'foreign_table' => 'tx_epproducts_domain_model_country',
'foreign_table_where' => 'ORDER BY name', 'foreign_table_where' => 'ORDER BY name',
@@ -39,6 +40,7 @@ call_user_func(function() {
'label' => 'Gebiet', 'label' => 'Gebiet',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_region', 'foreign_table' => 'tx_epproducts_domain_model_region',
'foreign_table_where' => 'ORDER BY name', 'foreign_table_where' => 'ORDER BY name',
@@ -106,6 +108,7 @@ call_user_func(function() {
'label' => 'Badge', 'label' => 'Badge',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_badge', 'foreign_table' => 'tx_epproducts_domain_model_badge',
'foreign_table_where' => 'ORDER BY title', 'foreign_table_where' => 'ORDER BY title',
@@ -22,6 +22,7 @@ call_user_func(function() {
'label' => 'Mitarbeiter', 'label' => 'Mitarbeiter',
'config' => [ 'config' => [
'type' => 'select', 'type' => 'select',
'default' => 0,
'renderType' => 'selectSingle', 'renderType' => 'selectSingle',
'foreign_table' => 'tx_epproducts_domain_model_teammember', 'foreign_table' => 'tx_epproducts_domain_model_teammember',
'foreign_table_where' => 'ORDER BY name', 'foreign_table_where' => 'ORDER BY name',
@@ -8,7 +8,7 @@
<f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: '-translate-y-2 text-white'}"/> <f:render partial="Disrupter/Outlines" section="Top1" arguments="{classes: '-translate-y-2 text-white'}"/>
</f:if> </f:if>
<div class="py-4 relative"> <div class="py-4 relative">
<div class="container grid grid-cols-2 lg:grid-cols-4 gap-8 pb-4"> <div class="container grid md:grid-cols-2 lg:grid-cols-4 gap-8 pb-4 text-center md:text-left">
<f:if condition="{settings.footerNavPid}"> <f:if condition="{settings.footerNavPid}">
<f:for each="{footernav}" as="menuheader"> <f:for each="{footernav}" as="menuheader">
<div> <div>
@@ -32,7 +32,7 @@
</f:if> </f:if>
</div> </div>
<div class="container py-4 text-white text-xl border-t border-white"> <div class="container py-4 text-white text-xl border-t border-white text-center md:text-left">
{totalRatings.totalAverage -> f:format.number(decimalSeparator: ',')} Sterne bei {totalRatings.totalCount} Bewertungen {totalRatings.totalAverage -> f:format.number(decimalSeparator: ',')} Sterne bei {totalRatings.totalCount} Bewertungen
</div> </div>
@@ -184,10 +184,8 @@
<f:format.raw>{totalRatings.jsonData->f:format.json()}</f:format.raw> <f:format.raw>{totalRatings.jsonData->f:format.json()}</f:format.raw>
</script> </script>
<f:comment><!-- <f:if condition="{site.identifier} == 'ep-reisen'">
<f:if condition="{site.identifier} == 'ep-reisen'"> <f:render partial="Chatbot" arguments="{_all}"/>
<f:render partial="Chatbot" arguments="{_all}"/> </f:if>
</f:if>
--></f:comment>
</html> </html>
@@ -2,7 +2,7 @@
xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"> xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="odd:bg-zinc-50 p-4" data-rte-content> <div class="group odd:bg-zinc-50 p-4" data-rte-content>
<f:if condition="{journey.byBus}"> <f:if condition="{journey.byBus}">
<h3> <h3>
Busanreise Busanreise
@@ -11,6 +11,9 @@
{journey.byBus -> f:format.html()} {journey.byBus -> f:format.html()}
</div> </div>
</f:if> </f:if>
<f:if condition="{pickupsOverview.pickups}">
<f:render partial="Product/Pickups" arguments="{pickupsOverview: pickupsOverview}"/>
</f:if>
<f:if condition="{journey.byCar}"> <f:if condition="{journey.byCar}">
<h3> <h3>
Eigenanreise Eigenanreise
@@ -2,7 +2,7 @@
xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://typo3.org/ns/fluid/ViewHelpers"> xmlns:f="http://typo3.org/ns/fluid/ViewHelpers">
<div class="group odd:bg-zinc-50 p-4" data-rte-content> <div class="pb-8">
<h3> <h3>
Bus-Zustiege Bus-Zustiege
</h3> </h3>
@@ -162,12 +162,7 @@
<div class="mb-4 hidden" <div class="mb-4 hidden"
data-tabs-target="tab" data-tabs-target="tab"
data-tab="journey"> data-tab="journey">
<f:if condition="{product.journey}"> <f:render partial="Journey/Details" arguments="{journey: product.journey, pickupsOverview: pickupsOverview, bars: 1}"/>
<f:render partial="Journey/Details" arguments="{journey: product.journey, bars: 1}"/>
</f:if>
<f:if condition="{pickupsOverview.pickups}">
<f:render partial="Product/Pickups" arguments="{pickupsOverview: pickupsOverview}"/>
</f:if>
</div> </div>
</f:if> </f:if>
</f:section> </f:section>