diff --git a/saas-app/public/admin.php b/saas-app/public/admin.php
index f31e7be..6db4c3b 100644
--- a/saas-app/public/admin.php
+++ b/saas-app/public/admin.php
@@ -171,7 +171,7 @@ SELECT
t.tenant_key,
t.name,
t.status,
- COALESCE(lp.plan_key, 'team') AS plan_key
+ COALESCE(lp.plan_key, 'free') AS plan_key
FROM tenants t
LEFT JOIN tenant_licenses tl ON tl.tenant_id = t.id AND tl.status = 'active'
LEFT JOIN license_plans lp ON lp.id = tl.license_plan_id
@@ -182,7 +182,7 @@ SQL,
)
: app_query_one(
$pdo,
- "SELECT id, tenant_key, name, status, 'team' AS plan_key FROM tenants WHERE id = :id LIMIT 1",
+ "SELECT id, tenant_key, name, status, 'free' AS plan_key FROM tenants WHERE id = :id LIMIT 1",
['id' => (string) $_GET['edit']]
);
}
@@ -320,7 +320,7 @@ SQL,
-
+
Zur Übersicht
@@ -333,9 +333,9 @@ SQL,
Lizenzrahmen
-
Starter
Basisfunktionen wie Mitglieder, Buchungen, Einzahlungen und Inhalte.
-
Team
Zusätzlich Mandanten-Einstellungen, PDF-Listen, Papierlisten-Erfassung und Basis-Exporte.
-
Business & Enterprise
Vorbereitung für SSO, Importe, erweiterte Exporte, Benachrichtigungen und priorisierte Updates.
+
Free
Bis zu 10 aktive Mitglieder, inklusive Mandanten-Einstellungen, PDF-Listen und Papier-Nacherfassung.
+
Starter & Team
Mehr Mitglieder für wachsende Standorte, weiterhin mit vollem Drucklisten-Prozess als Kernfunktion.
+
Business & Enterprise
Zusätzliche Erweiterungen wie SSO, Benachrichtigungen, Auswertungen und individuelle Freischaltungen.
@@ -350,7 +350,7 @@ SQL,
= admin_h((string) $tenant['name']) ?> |
= admin_h((string) $tenant['tenant_key']) ?> |
= admin_badge(((string) $tenant['status']) === 'active' ? 'Aktiv' : ucfirst((string) $tenant['status']), ((string) $tenant['status']) === 'active' ? 'success' : 'warning') ?> |
-
= admin_badge((string) ($tenant['plan_name'] ?? 'Starter')) ?> |
+
= admin_badge((string) ($tenant['plan_name'] ?? 'Free')) ?> |
= admin_h((string) $tenant['member_count']) ?> |
= admin_h((string) $tenant['admin_count']) ?> |
= admin_h((string) $tenant['provider_count']) ?> |
diff --git a/saas-app/public/app-support.php b/saas-app/public/app-support.php
index 6bd108b..cce7947 100644
--- a/saas-app/public/app-support.php
+++ b/saas-app/public/app-support.php
@@ -29,6 +29,29 @@ function app_uses_mysql(): bool
return in_array(app_connection(), ['mysql', 'mariadb'], true);
}
+function app_table_has_column(PDO $pdo, string $tableName, string $columnName): bool
+{
+ if (!scripts_table_exists($pdo, $tableName)) {
+ return false;
+ }
+
+ return app_query_one(
+ $pdo,
+ <<<'SQL'
+SELECT 1
+FROM INFORMATION_SCHEMA.COLUMNS
+WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = :table_name
+ AND COLUMN_NAME = :column_name
+LIMIT 1
+SQL,
+ [
+ 'table_name' => $tableName,
+ 'column_name' => $columnName,
+ ]
+ ) !== null;
+}
+
function app_request_path(): string
{
$path = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH);
@@ -323,11 +346,11 @@ function app_handle_platform_admin_login(PDO $pdo): array
function app_admin_tenant_list(PDO $pdo): array
{
- $licenseSelect = "'starter' AS plan_key, 'Starter' AS plan_name";
+ $licenseSelect = "'free' AS plan_key, 'Free' AS plan_name";
$licenseJoins = '';
if (scripts_table_exists($pdo, 'tenant_licenses') && scripts_table_exists($pdo, 'license_plans')) {
- $licenseSelect = "COALESCE(lp.plan_key, 'starter') AS plan_key, COALESCE(lp.name, 'Starter') AS plan_name";
+ $licenseSelect = "COALESCE(lp.plan_key, 'free') AS plan_key, COALESCE(lp.name, 'Free') AS plan_name";
$licenseJoins = "\nLEFT JOIN tenant_licenses tl ON tl.tenant_id = t.id AND tl.status = 'active'\nLEFT JOIN license_plans lp ON lp.id = tl.license_plan_id";
}
@@ -656,6 +679,33 @@ function app_upsert_member(PDO $pdo, string $tenantId, array $data): void
throw new RuntimeException('Diese E-Mail-Adresse gehört bereits zu einer anderen Person im Mandanten.');
}
+ $license = app_tenant_license($pdo, $tenantId);
+ $memberLimit = isset($license['member_limit']) && (int) $license['member_limit'] > 0
+ ? (int) $license['member_limit']
+ : null;
+
+ if ($memberLimit !== null) {
+ $activeMembers = (int) app_query_value(
+ $pdo,
+ 'SELECT COUNT(*) FROM members WHERE tenant_id = :tenant_id AND status = :status',
+ ['tenant_id' => $tenantId, 'status' => 'active'],
+ 0
+ );
+ $wasActive = $editingMembership !== null
+ && (string) ($editingMembership['member_status'] ?? $editingMembership['status'] ?? 'active') === 'active';
+ $willBeActive = $status === 'active';
+
+ if ($willBeActive && !$wasActive && $activeMembers >= $memberLimit) {
+ throw new RuntimeException(
+ 'Das Mitgliederlimit des Lizenzplans '
+ . (string) ($license['plan_name'] ?? 'Free')
+ . ' ist erreicht. Aktuell sind maximal '
+ . $memberLimit
+ . ' aktive Mitglieder möglich.'
+ );
+ }
+ }
+
scripts_ensure_core_roles($pdo);
$tenantAdminRoleId = scripts_role_id('tenant_admin', 'tenant');
$now = date('Y-m-d H:i:s');
@@ -1067,21 +1117,84 @@ SQL,
function app_print_list_pdf_groups(PDO $pdo, string $tenantId, string $listId): array
{
- $heavyRows = [];
- $lightRows = [];
+ return app_print_list_pdf_groups_with_settings($pdo, $tenantId, $listId, app_tenant_settings($pdo, $tenantId));
+}
- foreach (app_print_list_export_rows($pdo, $tenantId, $listId) as $row) {
- if ((int) ($row['recent_strokes'] ?? 0) >= 10) {
- $heavyRows[] = $row;
- continue;
+function app_print_list_row_height_mm(array $settings): float
+{
+ $height = (float) ($settings['pdf_row_height_mm'] ?? 4.0);
+
+ return max(3.2, min(12.0, $height));
+}
+
+function app_print_list_layout(array $allRows, array $frontRows, array $backRows, array $settings): array
+{
+ $configuredHeight = app_print_list_row_height_mm($settings);
+ $maxTableHeightMm = 264.0;
+ $minimumHeight = 3.2;
+ $singleRequiredHeight = $maxTableHeightMm / max(1, count($allRows));
+
+ if ($singleRequiredHeight >= $minimumHeight) {
+ $autoHeight = min(12.0, max($configuredHeight, $singleRequiredHeight));
+
+ return [
+ 'mode' => 'single',
+ 'row_height_mm' => $autoHeight,
+ 'rows_per_page' => max(1, (int) floor($maxTableHeightMm / $autoHeight)),
+ ];
+ }
+
+ $largestSide = max(1, (int) ceil(count($allRows) / 2));
+ $duplexHeight = min($configuredHeight, $maxTableHeightMm / $largestSide);
+ $duplexHeight = max($minimumHeight, min(12.0, $duplexHeight));
+
+ return [
+ 'mode' => 'duplex',
+ 'row_height_mm' => $duplexHeight,
+ 'rows_per_page' => max(1, (int) floor($maxTableHeightMm / $duplexHeight)),
+ ];
+}
+
+function app_print_list_pdf_groups_with_settings(PDO $pdo, string $tenantId, string $listId, array $settings): array
+{
+ $allRows = app_print_list_export_rows($pdo, $tenantId, $listId);
+ $rankedRows = $allRows;
+
+ usort(
+ $rankedRows,
+ static function (array $left, array $right): int {
+ $strokeCompare = (int) ($right['recent_strokes'] ?? 0) <=> (int) ($left['recent_strokes'] ?? 0);
+
+ if ($strokeCompare !== 0) {
+ return $strokeCompare;
+ }
+
+ return strcasecmp((string) ($left['member_name'] ?? ''), (string) ($right['member_name'] ?? ''));
}
+ );
- $lightRows[] = $row;
+ $splitIndex = (int) ceil(count($rankedRows) / 2);
+ $heavyRows = array_slice($rankedRows, 0, $splitIndex);
+ $lightRows = array_slice($rankedRows, $splitIndex);
+
+ $layout = app_print_list_layout($allRows, $heavyRows, $lightRows, $settings);
+
+ if ($layout['mode'] === 'single') {
+ return [
+ 'mode' => 'single',
+ 'front' => $allRows,
+ 'back' => [],
+ 'row_height_mm' => $layout['row_height_mm'],
+ 'rows_per_page' => $layout['rows_per_page'],
+ ];
}
return [
+ 'mode' => 'duplex',
'front' => $heavyRows,
'back' => $lightRows,
+ 'row_height_mm' => $layout['row_height_mm'],
+ 'rows_per_page' => $layout['rows_per_page'],
];
}
@@ -1199,7 +1312,7 @@ function app_pdf_truncate(string $text, int $maxLength): string
return $text;
}
- return mb_substr($text, 0, $maxLength - 1, 'UTF-8') . '…';
+ return mb_substr($text, 0, $maxLength - 3, 'UTF-8') . '...';
}
if (strlen($text) <= $maxLength) {
@@ -1209,7 +1322,7 @@ function app_pdf_truncate(string $text, int $maxLength): string
return substr($text, 0, $maxLength - 3) . '...';
}
-function app_render_print_list_pdf_page(string $headline, array $headerFill, array $headerText, string $metaText, array $rows, int $targetRows, ?string $sideNote = null): string
+function app_render_print_list_pdf_page(string $headline, array $headerFill, array $headerText, string $metaText, array $rows, int $rowSlots, float $rowHeightMm, ?string $sideNote = null): string
{
$commands = "0.4 w\n";
$x = 5.0;
@@ -1219,11 +1332,11 @@ function app_render_print_list_pdf_page(string $headline, array $headerFill, arr
$strikeWidth = 140.0;
$headerHeight = 9.0;
$subHeaderHeight = 6.5;
- $rowHeight = 4.0;
+ $rowHeight = max(3.2, min(12.0, $rowHeightMm));
$commands .= app_pdf_rect($x, $y, $nameWidth + $balanceWidth, $headerHeight, $headerFill);
$commands .= app_pdf_rect($x + $nameWidth + $balanceWidth, $y, $strikeWidth, $headerHeight, $headerFill);
- $commands .= app_pdf_text($x + 2, $y + 5.8, $headline, 'F2', 13, $headerText);
+ $commands .= app_pdf_text($x + 2, $y + 5.8, app_pdf_truncate($headline, 34), 'F2', 13, $headerText);
$commands .= app_pdf_text($x + $nameWidth + $balanceWidth + 2, $y + 5.8, app_pdf_truncate($metaText, 86), 'F1', 9, $headerText);
$y += $headerHeight;
@@ -1235,10 +1348,13 @@ function app_render_print_list_pdf_page(string $headline, array $headerFill, arr
$commands .= app_pdf_text($x + $nameWidth + $balanceWidth + 2, $y + 4.4, 'Striche', 'F2', 9.5);
$y += $subHeaderHeight;
- $rowCount = 1;
+ $noteRows = ($sideNote !== null && $sideNote !== '') ? 1 : 0;
+ $dataRows = max(0, $rowSlots - $noteRows);
+ $baselineY = min($rowHeight - 1.1, 2.9 + max(0.0, ($rowHeight - 4.0) * 0.45));
+ $renderedRows = 0;
- foreach ($rows as $row) {
- $rowCount++;
+ foreach (array_slice($rows, 0, $dataRows) as $row) {
+ $renderedRows++;
$balance = (float) ($row['balance'] ?? 0);
$balanceFill = null;
$balanceTextColor = [0, 0, 0];
@@ -1254,23 +1370,24 @@ function app_render_print_list_pdf_page(string $headline, array $headerFill, arr
$commands .= app_pdf_rect($x, $y, $nameWidth, $rowHeight);
$commands .= app_pdf_rect($x + $nameWidth, $y, $balanceWidth, $rowHeight, $balanceFill);
$commands .= app_pdf_rect($x + $nameWidth + $balanceWidth, $y, $strikeWidth, $rowHeight);
- $commands .= app_pdf_text($x + 1.5, $y + 2.9, app_pdf_truncate((string) ($row['member_name'] ?? ''), 28), 'F1', 9);
- $commands .= app_pdf_text($x + $nameWidth + 1.5, $y + 2.9, app_pdf_money($balance), 'F1', 8.5, $balanceTextColor);
+ $commands .= app_pdf_text($x + 1.5, $y + $baselineY, app_pdf_truncate((string) ($row['member_name'] ?? ''), 28), 'F1', 9);
+ $commands .= app_pdf_text($x + $nameWidth + 1.5, $y + $baselineY, app_pdf_money($balance), 'F1', 8.5, $balanceTextColor);
$y += $rowHeight;
}
- for ($i = $rowCount; $i < $targetRows; $i++) {
+ while ($renderedRows < $dataRows) {
$commands .= app_pdf_rect($x, $y, $nameWidth, $rowHeight);
$commands .= app_pdf_rect($x + $nameWidth, $y, $balanceWidth, $rowHeight);
$commands .= app_pdf_rect($x + $nameWidth + $balanceWidth, $y, $strikeWidth, $rowHeight);
$y += $rowHeight;
+ $renderedRows++;
}
if ($sideNote !== null && $sideNote !== '') {
$commands .= app_pdf_rect($x, $y, $nameWidth, $rowHeight);
$commands .= app_pdf_rect($x + $nameWidth, $y, $balanceWidth, $rowHeight);
$commands .= app_pdf_rect($x + $nameWidth + $balanceWidth, $y, $strikeWidth, $rowHeight, [255, 255, 0]);
- $commands .= app_pdf_text($x + $nameWidth + $balanceWidth + 88, $y + 2.9, $sideNote, 'F2', 8.5);
+ $commands .= app_pdf_text($x + $nameWidth + $balanceWidth + 82, $y + $baselineY, $sideNote, 'F2', 8.5);
}
return $commands;
@@ -1280,8 +1397,8 @@ function app_build_simple_pdf(array $pageStreams): string
{
$objects = [];
$objects[1] = "<< /Type /Catalog /Pages 2 0 R >>";
- $objects[3] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
- $objects[4] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>";
+ $objects[3] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>";
+ $objects[4] = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>";
$kids = [];
$nextObject = 5;
@@ -1328,36 +1445,48 @@ function app_send_print_list_pdf(PDO $pdo, string $tenantId, string $listId): vo
}
$tenant = app_tenant_by_id($pdo, $tenantId) ?? ['name' => 'Kaffeeliste'];
- $groups = app_print_list_pdf_groups($pdo, $tenantId, $listId);
- $heavyRows = $groups['front'];
- $lightRows = $groups['back'];
+ $settings = app_tenant_settings($pdo, $tenantId);
+ $groups = app_print_list_pdf_groups_with_settings($pdo, $tenantId, $listId, $settings);
+ $frontRows = $groups['front'];
+ $backRows = $groups['back'];
+ $rowSlots = max(1, (int) ($groups['rows_per_page'] ?? 60));
+ $rowHeight = (float) ($groups['row_height_mm'] ?? app_print_list_row_height_mm($settings));
$unitPrice = (float) ($list['unit_price'] ?? 0.50);
$meta = '1 Strich = ' . app_pdf_money($unitPrice) . '. Bitte spätestens bei 10,00 € einzahlen. '
. date('d.m.Y H:i') . ' | ' . (string) ($tenant['name'] ?? 'Kaffeeliste');
$pageStreams = [];
- if ($heavyRows !== []) {
+ if (($groups['mode'] ?? 'single') === 'single') {
$pageStreams[] = app_render_print_list_pdf_page(
- 'Kaffeeliste - Vieltrinker',
- [0, 94, 63],
- [255, 255, 255],
- $meta,
- $heavyRows,
- 64,
- $lightRows !== [] ? 'Rückseite beachten!' : null
+ (string) ($list['title'] ?? ($settings['pdf_list_title'] ?? 'Kaffeeliste')),
+ [0, 94, 63],
+ [255, 255, 255],
+ $meta,
+ $frontRows,
+ $rowSlots,
+ $rowHeight
);
- }
-
- if ($lightRows !== [] || $heavyRows === []) {
+ } else {
$pageStreams[] = app_render_print_list_pdf_page(
- 'Kaffeeliste - Wenigtrinker',
- [91, 209, 215],
- [0, 0, 0],
- $meta,
- $lightRows,
- 65,
- $heavyRows !== [] ? 'Vorderseite beachten!' : null
+ (string) ($settings['front_page_label'] ?? 'Vorderseite') . ' - Vieltrinker',
+ [0, 94, 63],
+ [255, 255, 255],
+ $meta,
+ $frontRows,
+ $rowSlots,
+ $rowHeight,
+ $backRows !== [] ? 'Rückseite beachten!' : null
+ );
+ $pageStreams[] = app_render_print_list_pdf_page(
+ (string) ($settings['back_page_label'] ?? 'Rückseite') . ' - Wenigtrinker',
+ [91, 209, 215],
+ [0, 0, 0],
+ $meta,
+ $backRows,
+ $rowSlots,
+ $rowHeight,
+ $frontRows !== [] ? 'Vorderseite beachten!' : null
);
}
@@ -1852,9 +1981,9 @@ function app_feature_defaults(): array
'ledger' => true,
'payments' => true,
'content' => true,
- 'tenant_settings' => false,
- 'pdf_export' => false,
- 'paper_strike_entry' => false,
+ 'tenant_settings' => true,
+ 'pdf_export' => true,
+ 'paper_strike_entry' => true,
'basic_exports' => false,
'oidc' => false,
'imports' => false,
@@ -1872,25 +2001,32 @@ function app_tenant_license(PDO $pdo, string $tenantId): array
{
if (!app_supports_license_features($pdo)) {
return [
- 'plan_key' => 'starter',
- 'plan_name' => 'Starter',
+ 'plan_key' => 'free',
+ 'plan_name' => 'Free',
+ 'member_limit' => 10,
'features' => app_feature_defaults(),
];
}
+ $hasMemberLimitColumn = app_table_has_column($pdo, 'license_plans', 'member_limit');
$row = app_query_one(
$pdo,
- <<<'SQL'
+ str_replace(
+ '__MEMBER_LIMIT__',
+ $hasMemberLimitColumn ? 'lp.member_limit' : 'NULL AS member_limit',
+ <<<'SQL'
SELECT
lp.id AS plan_id,
lp.plan_key,
- lp.name AS plan_name
+ lp.name AS plan_name,
+ __MEMBER_LIMIT__
FROM tenant_licenses tl
INNER JOIN license_plans lp ON lp.id = tl.license_plan_id
WHERE tl.tenant_id = :tenant_id
AND tl.status = 'active'
LIMIT 1
SQL,
+ ),
['tenant_id' => $tenantId]
);
@@ -1898,8 +2034,9 @@ SQL,
if ($row === null) {
return [
- 'plan_key' => 'starter',
- 'plan_name' => 'Starter',
+ 'plan_key' => 'free',
+ 'plan_name' => 'Free',
+ 'member_limit' => 10,
'features' => $features,
];
}
@@ -1933,6 +2070,7 @@ SQL,
return [
'plan_key' => (string) $row['plan_key'],
'plan_name' => (string) $row['plan_name'],
+ 'member_limit' => isset($row['member_limit']) ? (int) $row['member_limit'] : null,
'features' => $features,
];
}
@@ -1950,9 +2088,16 @@ function app_license_plan_options(PDO $pdo): array
return [];
}
+ if (!app_table_has_column($pdo, 'license_plans', 'member_limit')) {
+ return app_query_all(
+ $pdo,
+ 'SELECT id, plan_key, name, NULL AS member_limit FROM license_plans WHERE is_active = 1 ORDER BY sort_order ASC, name ASC'
+ );
+ }
+
return app_query_all(
$pdo,
- 'SELECT id, plan_key, name FROM license_plans WHERE is_active = 1 ORDER BY sort_order ASC, name ASC'
+ 'SELECT id, plan_key, name, member_limit FROM license_plans WHERE is_active = 1 ORDER BY sort_order ASC, name ASC'
);
}
@@ -2008,6 +2153,7 @@ function app_tenant_settings_defaults(): array
'pdf_list_title' => 'Kaffeeliste',
'front_page_label' => 'Vorderseite',
'back_page_label' => 'Rückseite',
+ 'pdf_row_height_mm' => '4.00',
'support_email' => '',
'location_label' => '',
'allow_self_service_booking' => '1',
@@ -2041,6 +2187,7 @@ function app_save_tenant_settings(PDO $pdo, string $tenantId, array $data): void
'pdf_list_title' => trim((string) ($data['pdf_list_title'] ?? 'Kaffeeliste')),
'front_page_label' => trim((string) ($data['front_page_label'] ?? 'Vorderseite')),
'back_page_label' => trim((string) ($data['back_page_label'] ?? 'Rückseite')),
+ 'pdf_row_height_mm' => trim((string) ($data['pdf_row_height_mm'] ?? '4.00')),
'support_email' => trim((string) ($data['support_email'] ?? '')),
'location_label' => trim((string) ($data['location_label'] ?? '')),
'allow_self_service_booking' => !empty($data['allow_self_service_booking']) ? '1' : '0',
@@ -2051,6 +2198,10 @@ function app_save_tenant_settings(PDO $pdo, string $tenantId, array $data): void
throw new RuntimeException('Bitte gib einen gültigen Standardpreis pro Strich an.');
}
+ if (!is_numeric($values['pdf_row_height_mm']) || (float) $values['pdf_row_height_mm'] < 3.2 || (float) $values['pdf_row_height_mm'] > 12.0) {
+ throw new RuntimeException('Bitte gib eine gültige Zeilenhöhe zwischen 3,2 mm und 12,0 mm an.');
+ }
+
if ($values['support_email'] !== '' && !filter_var($values['support_email'], FILTER_VALIDATE_EMAIL)) {
throw new RuntimeException('Bitte gib eine gültige Support-E-Mail-Adresse an.');
}
diff --git a/saas-app/public/index.php b/saas-app/public/index.php
index b304a78..39b04b6 100644
--- a/saas-app/public/index.php
+++ b/saas-app/public/index.php
@@ -84,7 +84,7 @@ $memberSummary = null;
$loginFlow = ['state' => app_login_state(), 'message' => null, 'error' => null];
$editingMember = null;
$memberForm = app_member_form_defaults();
-$tenantLicense = ['plan_key' => 'starter', 'plan_name' => 'Starter', 'features' => app_feature_defaults()];
+$tenantLicense = ['plan_key' => 'free', 'plan_name' => 'Free', 'member_limit' => 10, 'features' => app_feature_defaults()];
$tenantSettings = app_tenant_settings_defaults();
$printLists = [];
$activePrintList = null;
@@ -251,7 +251,7 @@ $canManageTenant = app_can_manage_tenant($auth);
= badge((string) $auth['tenant_name']) ?>
= h((string) $auth['display_name']) ?> ist angemeldet als = h((string) $auth['email']) ?>
= app_is_platform_admin($auth) ? badge('Global-Admin', 'success') : (app_is_tenant_admin($auth) ? badge('Tenant-Admin', 'success') : badge('Mitglied')) ?>
- = badge('Lizenz ' . (string) ($tenantLicense['plan_name'] ?? 'Starter')) ?>
+ = badge('Lizenz ' . (string) ($tenantLicense['plan_name'] ?? 'Free')) ?>
@@ -605,6 +605,7 @@ $canManageTenant = app_can_manage_tenant($auth);