Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1ec4b92
#74 removed conflitced reservations from export
developeregrem Jan 21, 2026
0dcdddb
added new module for housekeeping
developeregrem Jan 22, 2026
90189c2
added operations report view and new template type
developeregrem Jan 26, 2026
5fc3598
added statistic data to report
developeregrem Jan 27, 2026
88a0348
added tests
developeregrem Jan 27, 2026
f800669
add option to import missing default templates
developeregrem Jan 30, 2026
4fcf44a
#119 changed warning message and messages can be ignored
developeregrem Jan 30, 2026
dbb712e
added new front desk view, added new system reservation status
developeregrem Jan 30, 2026
785f453
added reservation status filter to statistics
developeregrem Jan 31, 2026
93bdfc9
cleaned up code
developeregrem Jan 31, 2026
3e58d3f
einvoice can be selected for mail attachment in correspondences
developeregrem Feb 1, 2026
db753b2
tourism statistic respects reservation status now
developeregrem Feb 2, 2026
8e68962
removed deprection
developeregrem Feb 2, 2026
b82b5a7
fixed id
developeregrem Feb 2, 2026
4b481d8
symfony update
developeregrem Feb 2, 2026
3890f7d
refactoring doctrine config
developeregrem Feb 3, 2026
9ec61db
added phpstan.dist
developeregrem Feb 3, 2026
10a8c8e
update lock file
developeregrem Feb 3, 2026
e707673
update recipes
developeregrem Feb 3, 2026
f28e362
added missing translation
developeregrem Feb 3, 2026
68d7130
upgrade doctrine and removed deprecations
developeregrem Feb 3, 2026
db9c572
closes #159 added configurable invoice file names
developeregrem Feb 4, 2026
15f5a8e
added missing translation
developeregrem Feb 4, 2026
a9a0fe0
#159 make file contain only ascii characters
developeregrem Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .env.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name
# define database server version
# when using maria db use the following syntax: 10.8.7-MariaDB
# see https://symfony.com/doc/current/reference/configuration/doctrine.html#doctrine-dbal-configuration
DB_SERVER_VERSION=5.6
DB_SERVER_VERSION=8.0
###< doctrine/doctrine-bundle ###

### mailer settings ###
Expand All @@ -45,7 +45,7 @@ USE_PASSWORD_BLACKLIST=true

###> symfony/mailer ###
# e.g. smtp://username:password@yourdomain.tld:port
MAILER_DSN=null://localhost
MAILER_DSN=null://null
###< symfony/mailer ###

###> web-auth/webauthn-symfony-bundle ###
Expand All @@ -58,3 +58,14 @@ PASSKEY_ENABLED=false

# comma-separated list of customer salutations
CUSTOMER_SALUTATIONS="Ms,Mr,Family"

# invoice export filename pattern (without extension)
# allowed placeholders: <company>, <lastname>, <firstname>, <status>, <payment>, <number>, <date>
# fallback syntax is supported: <company|lastname>, the first non-empty value will be used
INVOICE_FILENAME_PATTERN="Invoice-<number>"

###> symfony/routing ###
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
DEFAULT_URI=http://localhost
###< symfony/routing ###
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ package-lock.json
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###

###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
2 changes: 1 addition & 1 deletion assets/app.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import './bootstrap.js';
import './stimulus_bootstrap.js';
/*
* Welcome to your app's main JavaScript file!
*
Expand Down
6 changes: 4 additions & 2 deletions assets/controllers/csrf_protection_controller.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_\/+a-zA-Z0-9]{24,}$/;
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;

// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
// and thus this event-listener will not be executed.
document.addEventListener('submit', function (event) {
generateCsrfToken(event.target);
}, true);
Expand Down Expand Up @@ -33,8 +35,8 @@ export function generateCsrfToken (formElement) {
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
}
csrfField.dispatchEvent(new Event('change', { bubbles: true }));

if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
Expand Down
51 changes: 51 additions & 0 deletions assets/controllers/housekeeping_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Controller } from '@hotwired/stimulus';
import { request as httpRequest, serializeForm as httpSerializeForm } from './http_controller.js';

export default class extends Controller {
static targets = ['form', 'spinner'];

submitFilters(event) {
this.spin();
if (event) {
event.preventDefault();
}

if (this.formTarget && typeof this.formTarget.requestSubmit === 'function') {
this.formTarget.requestSubmit();
} else if (this.formTarget) {
this.formTarget.submit();
}
}

spin() {
if (this.hasSpinnerTarget) {
this.spinnerTarget.classList.add('fa-spin');
}
}

async saveRow(event) {
event.preventDefault();
const form = event.target;
const submitter = event.submitter || form.querySelector('button[type="submit"]');

if (submitter) {
submitter.disabled = true;
}

httpRequest({
url: form.action,
method: form.method || 'POST',
data: httpSerializeForm(form),
loader: false,
onSuccess: () => {},
onComplete: () => {
if (submitter) {
submitter.disabled = false;
}
},
onError: (message) => {
console.warn('[housekeeping] save failed', message);
},
});
}
}
69 changes: 69 additions & 0 deletions assets/controllers/operations_reports_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Controller } from '@hotwired/stimulus';
import { request as httpRequest, serializeForm as httpSerializeForm } from './http_controller.js';

export default class extends Controller {
static targets = ['form', 'download', 'preview', 'spinner'];

connect() {
this.updateLinks();
this.loadPreview();
}

preventSubmit(event) {
if (event) {
event.preventDefault();
}
}

updateLinks() {
if (!this.hasFormTarget) {
return;
}
const query = httpSerializeForm(this.formTarget);
if (this.hasDownloadTarget) {
const baseUrl = this.downloadTarget.dataset.baseUrl || this.downloadTarget.href;
this.downloadTarget.href = this.appendQuery(baseUrl, query);
}
if (this.hasPreviewTarget) {
const basePreview = this.previewTarget.dataset.basePreviewUrl || '';
if (basePreview) {
this.previewTarget.dataset.previewUrl = this.appendQuery(basePreview, query);
}
}
}

loadPreview(event) {
if (event) {
event.preventDefault();
}
this.updateLinks();
if (!this.hasPreviewTarget) {
return;
}
const url = this.previewTarget.dataset.previewUrl;
if (!url) {
return;
}
if (this.hasSpinnerTarget) {
this.spinnerTarget.classList.add('fa-spin');
}
httpRequest({
url,
method: 'GET',
target: this.previewTarget,
loader: false,
onComplete: () => {
if (this.hasSpinnerTarget) {
this.spinnerTarget.classList.remove('fa-spin');
}
},
});
}

appendQuery(baseUrl, query) {
if (!query) {
return baseUrl;
}
return baseUrl + (baseUrl.includes('?') ? '&' : '?') + query;
}
}
39 changes: 35 additions & 4 deletions assets/controllers/reservations_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -507,11 +507,17 @@ export default class extends Controller {
event.preventDefault();
const id = event.currentTarget.dataset.attachmentId;
const isInvoice = event.currentTarget.dataset.isInvoice === 'true';
const einvoiceCheckbox = event.currentTarget.querySelector('[data-einvoice-checkbox]');
const isEInvoice = !!(einvoiceCheckbox && einvoiceCheckbox.checked);
const url = event.currentTarget.dataset.url;
if (url && this.modalContent) {
this.modalContent.dataset.addAttachmentUrl = url;
}
this.addAsAttachment(id, isInvoice);
this.addAsAttachment(id, isInvoice, isEInvoice);
}

stopAttachmentRowClickAction(event) {
event.stopPropagation();
}

previewTemplateForReservationAction(event) {
Expand Down Expand Up @@ -1051,6 +1057,25 @@ export default class extends Controller {
}
}

updateReservationStatusAction(event) {
const select = event.currentTarget;
const url = select.dataset.url;
const token = select.dataset.token;
if (!url || !token) {
return;
}
select.disabled = true;
httpRequest({
url,
method: 'POST',
loader: false,
data: { status: select.value, _token: token },
onComplete: () => {
select.disabled = false;
}
});
}

showFeedback(data, target = null) {
if (!data || typeof data !== 'string' || data.trim().length === 0) {
return false;
Expand Down Expand Up @@ -1688,17 +1713,23 @@ export default class extends Controller {



addAsAttachment(id, isInvoice) {
addAsAttachment(id, isInvoice, isEInvoice = false) {
const url = this.getContextValue('addAttachmentUrl');
if (!url) {
return false;
}
httpRequest({
url,
method: 'POST',
data: { id, isInvoice },
data: { id, isInvoice, isEInvoice },
loader: false,
target: this.modalContent,
onSuccess: () => this.previewTemplateForReservation(0, 'false')
onSuccess: (data) => {
if (this.showFeedback(data)) {
return;
}
this.previewTemplateForReservation(0, 'false');
}
});
return false;
}
Expand Down
48 changes: 45 additions & 3 deletions assets/controllers/statistics_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default class extends Controller {
'monthlyChart',
'yearlyChart',
'invoiceStatusForm',
'reservationStatusForm',
'snapshotMonth',
'snapshotYear',
'snapshotArrivalsTotal',
Expand All @@ -34,6 +35,7 @@ export default class extends Controller {
monthlyOriginUrl: String,
yearlyOriginUrl: String,
snapshotUrl: String,
snapshotIgnoreUrl: String,
snapshotArrivalsLabel: String,
snapshotOvernightsLabel: String,
snapshotRoomLabel: String,
Expand Down Expand Up @@ -195,6 +197,9 @@ export default class extends Controller {
if (this.hasObjectsTarget) {
params.append('objectId', this.objectsTarget.value);
}
if (this.hasReservationStatusFormTarget) {
new FormData(this.reservationStatusFormTarget).forEach((v, k) => params.append(k, v));
}
if (!yearOnly) {
params.append('monthStart', this.monthlyStartTarget.value);
params.append('monthEnd', this.monthlyEndTarget.value);
Expand Down Expand Up @@ -312,6 +317,7 @@ export default class extends Controller {
const params = this.snapshotParams(force);
const response = await fetch(`${this.snapshotUrlValue}?${params.toString()}`);
const data = await response.json();
this.currentSnapshotId = data.id || null;
const countryNames = data.countryNames || {};

this.updateSnapshotSummary(data.metrics || {});
Expand All @@ -331,6 +337,9 @@ export default class extends Controller {
if (this.hasObjectsTarget) {
params.append('objectId', this.objectsTarget.value);
}
if (this.hasReservationStatusFormTarget) {
new FormData(this.reservationStatusFormTarget).forEach((v, k) => params.append(k, v));
}
if (this.hasSnapshotMonthTarget) {
params.append('month', this.snapshotMonthTarget.value);
}
Expand Down Expand Up @@ -369,20 +378,34 @@ export default class extends Controller {
updateSnapshotWarnings(warnings) {
if (!this.hasSnapshotWarningsTarget) return;
this.snapshotWarningsTarget.innerHTML = '';
if (!warnings.length) {
const activeWarnings = warnings.filter((warning) => !warning.ignored);
if (!activeWarnings.length) {
const li = document.createElement('li');
li.className = 'text-muted';
li.textContent = this.snapshotWarningsTarget.dataset.emptyText || '';
this.snapshotWarningsTarget.appendChild(li);
return;
}
warnings.forEach((warning) => {
activeWarnings.forEach((warning) => {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'form-check-input me-2';
checkbox.checked = false;
checkbox.addEventListener('change', async () => {
await this.toggleWarningIgnore(warning, checkbox.checked);
this.drawSnapshot(false);
});

const li = document.createElement('li');
li.className = 'd-flex align-items-start mb-1';
const start = warning.start_date || '';
const end = warning.end_date || '';
const roomLabel = this.snapshotRoomLabelValue || '';
const room = warning.appartment_number ? ` ${roomLabel} ${warning.appartment_number}` : '';
li.textContent = `${warning.message || ''}${room} ${start} - ${end}`.trim();
const text = document.createElement('span');
text.textContent = `${warning.message || ''}${room} ${start} - ${end}`.trim();
li.appendChild(checkbox);
li.appendChild(text);
this.snapshotWarningsTarget.appendChild(li);
});
}
Expand Down Expand Up @@ -475,4 +498,23 @@ export default class extends Controller {
const upper = code.toUpperCase();
return countryNames[upper] || countryNames[code] || code;
}

async toggleWarningIgnore(warning, ignored) {
if (!this.snapshotIgnoreUrlValue) return;
if (!this.currentSnapshotId) return;
const params = new URLSearchParams();
params.append('reservationId', warning.reservation_id);
params.append('ignored', ignored ? '1' : '0');
const csrfToken = document.getElementById('statistics_csrf_token');
if (csrfToken) {
params.append('_csrf_token', csrfToken.value);
}

const baseUrl = this.snapshotIgnoreUrlValue.replace(/\/0$/, '');
await fetch(`${baseUrl}/${this.currentSnapshotId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
}
}
Loading