Aktualisierung Lizenz und PDF logik
This commit is contained in:
@@ -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,
|
||||
<label>Mandanten-Key<input name="tenant_key" value="<?= admin_h((string) ($editingTenant['tenant_key'] ?? '')) ?>" placeholder="werk-berlin"></label>
|
||||
<label>Name<input name="name" value="<?= admin_h((string) ($editingTenant['name'] ?? '')) ?>" placeholder="Werk Berlin"></label>
|
||||
<label>Status<select name="status"><?php foreach (['active' => 'Aktiv', 'inactive' => 'Inaktiv', 'sandbox' => 'Sandbox'] as $value => $label): ?><option value="<?= admin_h($value) ?>"<?= (($editingTenant['status'] ?? 'active') === $value) ? ' selected' : '' ?>><?= admin_h($label) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Lizenzplan<select name="plan_key"><?php foreach ($licensePlans as $plan): ?><option value="<?= admin_h((string) $plan['plan_key']) ?>"<?= ((string) ($editingTenant['plan_key'] ?? 'team') === (string) $plan['plan_key']) ? ' selected' : '' ?>><?= admin_h((string) $plan['name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Lizenzplan<select name="plan_key"><?php foreach ($licensePlans as $plan): ?><option value="<?= admin_h((string) $plan['plan_key']) ?>"<?= ((string) ($editingTenant['plan_key'] ?? 'free') === (string) $plan['plan_key']) ? ' selected' : '' ?>><?= admin_h((string) $plan['name']) ?><?= isset($plan['member_limit']) && (int) $plan['member_limit'] > 0 ? ' bis ' . admin_h((string) $plan['member_limit']) . ' Mitglieder' : ' individuell' ?></option><?php endforeach; ?></select></label>
|
||||
<div class="actions">
|
||||
<button type="submit">Speichern</button>
|
||||
<a class="button secondary" href="/admin/">Zur Übersicht</a>
|
||||
@@ -333,9 +333,9 @@ SQL,
|
||||
<article class="card">
|
||||
<h2>Lizenzrahmen</h2>
|
||||
<div class="stack">
|
||||
<div class="metric"><h3>Starter</h3><p>Basisfunktionen wie Mitglieder, Buchungen, Einzahlungen und Inhalte.</p></div>
|
||||
<div class="metric"><h3>Team</h3><p>Zusätzlich Mandanten-Einstellungen, PDF-Listen, Papierlisten-Erfassung und Basis-Exporte.</p></div>
|
||||
<div class="metric"><h3>Business & Enterprise</h3><p>Vorbereitung für SSO, Importe, erweiterte Exporte, Benachrichtigungen und priorisierte Updates.</p></div>
|
||||
<div class="metric"><h3>Free</h3><p>Bis zu 10 aktive Mitglieder, inklusive Mandanten-Einstellungen, PDF-Listen und Papier-Nacherfassung.</p></div>
|
||||
<div class="metric"><h3>Starter & Team</h3><p>Mehr Mitglieder für wachsende Standorte, weiterhin mit vollem Drucklisten-Prozess als Kernfunktion.</p></div>
|
||||
<div class="metric"><h3>Business & Enterprise</h3><p>Zusätzliche Erweiterungen wie SSO, Benachrichtigungen, Auswertungen und individuelle Freischaltungen.</p></div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
@@ -350,7 +350,7 @@ SQL,
|
||||
<td><strong><?= admin_h((string) $tenant['name']) ?></strong></td>
|
||||
<td><?= admin_h((string) $tenant['tenant_key']) ?></td>
|
||||
<td><?= admin_badge(((string) $tenant['status']) === 'active' ? 'Aktiv' : ucfirst((string) $tenant['status']), ((string) $tenant['status']) === 'active' ? 'success' : 'warning') ?></td>
|
||||
<td><?= admin_badge((string) ($tenant['plan_name'] ?? 'Starter')) ?></td>
|
||||
<td><?= admin_badge((string) ($tenant['plan_name'] ?? 'Free')) ?></td>
|
||||
<td><?= admin_h((string) $tenant['member_count']) ?></td>
|
||||
<td><?= admin_h((string) $tenant['admin_count']) ?></td>
|
||||
<td><?= admin_h((string) $tenant['provider_count']) ?></td>
|
||||
|
||||
+204
-53
@@ -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.');
|
||||
}
|
||||
|
||||
+35
-18
@@ -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']) ?>
|
||||
<span class="muted"><?= h((string) $auth['display_name']) ?> ist angemeldet als <?= h((string) $auth['email']) ?></span>
|
||||
<?= 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')) ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
@@ -605,6 +605,7 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<label>PDF-Listentitel<input name="pdf_list_title" value="<?= h((string) ($tenantSettings['pdf_list_title'] ?? 'Kaffeeliste')) ?>"></label>
|
||||
<label>Beschriftung Vorderseite<input name="front_page_label" value="<?= h((string) ($tenantSettings['front_page_label'] ?? 'Vorderseite')) ?>"></label>
|
||||
<label>Beschriftung Rückseite<input name="back_page_label" value="<?= h((string) ($tenantSettings['back_page_label'] ?? 'Rückseite')) ?>"></label>
|
||||
<label>Zeilenhöhe PDF-Liste in mm<input type="number" name="pdf_row_height_mm" min="3.2" max="12" step="0.1" value="<?= h((string) ($tenantSettings['pdf_row_height_mm'] ?? '4.00')) ?>"></label>
|
||||
<label>Standortbezeichnung<input name="location_label" value="<?= h((string) ($tenantSettings['location_label'] ?? '')) ?>"></label>
|
||||
<label>Support-E-Mail<input type="email" name="support_email" value="<?= h((string) ($tenantSettings['support_email'] ?? '')) ?>"></label>
|
||||
<label class="checkbox" style="grid-column:1 / -1;display:flex;flex-direction:row;align-items:center;font-weight:600;">
|
||||
@@ -618,15 +619,29 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
<article class="card">
|
||||
<h2>Aktuelle Lizenz</h2>
|
||||
<div class="stack">
|
||||
<div class="metric"><h3><?= h((string) ($tenantLicense['plan_name'] ?? 'Starter')) ?></h3><p>Freigeschalteter Lizenzplan für diesen Mandanten.</p></div>
|
||||
<div class="metric"><h3>Freigeschaltete Zusatzfunktionen</h3><ul class="list"><?php foreach (['tenant_settings' => 'Mandanten-Einstellungen', 'pdf_export' => 'PDF-Listen', 'paper_strike_entry' => 'Papierlisten-Erfassung', 'basic_exports' => 'Basis-Exporte'] as $featureKey => $featureLabel): ?><?php if (!empty($tenantLicense['features'][$featureKey])): ?><li><?= h($featureLabel) ?></li><?php endif; ?><?php endforeach; ?></ul></div>
|
||||
<div class="metric"><h3><?= h((string) ($tenantLicense['plan_name'] ?? 'Free')) ?></h3><p>Freigeschalteter Lizenzplan für diesen Mandanten.</p></div>
|
||||
<div class="metric"><h3>Mitgliederrahmen</h3><p><?= isset($tenantLicense['member_limit']) && (int) $tenantLicense['member_limit'] > 0 ? h((string) $tenantLicense['member_limit']) . ' aktive Mitglieder im Standardumfang.' : 'Individuell vereinbart.' ?></p></div>
|
||||
<div class="metric"><h3>Freigeschaltete Kernfunktionen</h3><ul class="list"><?php foreach (['tenant_settings' => 'Mandanten-Einstellungen', 'pdf_export' => 'PDF-Listen', 'paper_strike_entry' => 'Papierlisten-Erfassung'] as $featureKey => $featureLabel): ?><?php if (!empty($tenantLicense['features'][$featureKey])): ?><li><?= h($featureLabel) ?></li><?php endif; ?><?php endforeach; ?></ul></div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<?php elseif ($page === 'exports'): ?>
|
||||
<?php if (isset($_GET['view']) && $_GET['view'] === 'print' && is_array($activePrintList)): ?>
|
||||
<section class="hero"><div class="eyebrow">PDF-Vorschau</div><h1><?= h((string) $activePrintList['title']) ?></h1><p>Die echte Druckliste steht jetzt als generiertes PDF zur Verfügung. Diese Web-Ansicht bleibt nur als schnelle Vorschau.</p><div class="actions" style="margin-top:18px"><a class="button secondary" href="/exports/?list=<?= h((string) $activePrintList['id']) ?>">Zurück</a><a class="button" href="/exports/?download=print-list-pdf&list=<?= h((string) $activePrintList['id']) ?>">PDF herunterladen</a></div></section>
|
||||
<section class="card"><div class="grid grid-2"><article><h2><?= h((string) ($tenantSettings['front_page_label'] ?? 'Vorderseite')) ?></h2><div class="table"><table><thead><tr><th>#</th><th>Person</th><th>Striche</th></tr></thead><tbody><?php foreach (($activePrintList['items'] ?? []) as $item): ?><tr><td><?= h((string) $item['list_position']) ?></td><td><?= h((string) $item['member_name']) ?></td><td>________</td></tr><?php endforeach; ?></tbody></table></div></article><article><h2><?= h((string) ($tenantSettings['back_page_label'] ?? 'Rückseite')) ?></h2><div class="table"><table><thead><tr><th>#</th><th>Person</th><th>Striche</th></tr></thead><tbody><?php foreach (($activePrintList['items'] ?? []) as $item): ?><tr><td><?= h((string) $item['list_position']) ?></td><td><?= h((string) $item['member_name']) ?></td><td>________</td></tr><?php endforeach; ?></tbody></table></div></article></div></section>
|
||||
<section class="hero"><div class="eyebrow">PDF-Vorschau</div><h1><?= h((string) $activePrintList['title']) ?></h1><p><?= (($printListGroups['mode'] ?? 'single') === 'single') ? 'Diese Liste wird als einzelne PDF-Seite erzeugt.' : 'Diese Liste wird als Vorder- und Rückseite erzeugt.' ?> Die Zeilenhöhe liegt aktuell bei <?= h((string) ($printListGroups['row_height_mm'] ?? ($tenantSettings['pdf_row_height_mm'] ?? '4.00'))) ?> mm.</p><div class="actions" style="margin-top:18px"><a class="button secondary" href="/exports/?list=<?= h((string) $activePrintList['id']) ?>">Zurück</a><a class="button" href="/exports/?download=print-list-pdf&list=<?= h((string) $activePrintList['id']) ?>">PDF herunterladen</a></div></section>
|
||||
<section class="card">
|
||||
<div class="grid<?= (($printListGroups['mode'] ?? 'single') === 'single') ? '' : ' grid-2' ?>">
|
||||
<article>
|
||||
<h2><?= (($printListGroups['mode'] ?? 'single') === 'single') ? h((string) $activePrintList['title']) : h((string) ($tenantSettings['front_page_label'] ?? 'Vorderseite')) . ' / Vieltrinker' ?></h2>
|
||||
<div class="table"><table><thead><tr><th>#</th><th>Person</th><th>Striche</th></tr></thead><tbody><?php foreach (($printListGroups['front'] ?? []) as $item): ?><tr><td><?= h((string) ($item['list_position'] ?? '')) ?></td><td><?= h((string) $item['member_name']) ?></td><td>________</td></tr><?php endforeach; ?></tbody></table></div>
|
||||
</article>
|
||||
<?php if (($printListGroups['mode'] ?? 'single') !== 'single'): ?>
|
||||
<article>
|
||||
<h2><?= h((string) ($tenantSettings['back_page_label'] ?? 'Rückseite')) ?> / Wenigtrinker</h2>
|
||||
<div class="table"><table><thead><tr><th>#</th><th>Person</th><th>Striche</th></tr></thead><tbody><?php foreach (($printListGroups['back'] ?? []) as $item): ?><tr><td><?= h((string) ($item['list_position'] ?? '')) ?></td><td><?= h((string) $item['member_name']) ?></td><td>________</td></tr><?php endforeach; ?></tbody></table></div>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php else: ?>
|
||||
<section class="hero"><div class="eyebrow">Exporte und Drucklisten</div><h1>Drucklisten als PDF vorbereiten und später aus Papier nacherfassen</h1><p>Hier erzeugst du druckfertige Listen, speicherst sie als PDF und buchst anschließend die Striche aus Vorder- und Rückseite zurück ins System.</p></section>
|
||||
<section class="grid grid-2">
|
||||
@@ -652,8 +667,8 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<ul class="list">
|
||||
<li>Druckliste erzeugen und über die Druckansicht im Browser als PDF speichern.</li>
|
||||
<li>Ausgedruckte Vorder- und Rückseite manuell ausfüllen lassen.</li>
|
||||
<li>Druckliste erzeugen und direkt als saubere PDF-Datei herunterladen.</li>
|
||||
<li>Nur bei vielen Mitgliedern wird automatisch eine Vorder- und Rückseite erzeugt.</li>
|
||||
<li>Die Striche später gesammelt in derselben Liste nachtragen und verbuchen.</li>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
@@ -682,7 +697,7 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
</div>
|
||||
<div class="grid grid-2" style="margin-top:18px">
|
||||
<article>
|
||||
<h3><?= h((string) ($tenantSettings['front_page_label'] ?? 'Vorderseite')) ?> / Vieltrinker</h3>
|
||||
<h3><?= (($printListGroups['mode'] ?? 'single') === 'single') ? 'Druckliste' : h((string) ($tenantSettings['front_page_label'] ?? 'Vorderseite')) . ' / Vieltrinker' ?></h3>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead><tr><th>Person</th><th>Striche</th><th>Bereits erfasst</th></tr></thead>
|
||||
@@ -690,15 +705,17 @@ $canManageTenant = app_can_manage_tenant($auth);
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
<article>
|
||||
<h3><?= h((string) ($tenantSettings['back_page_label'] ?? 'Rückseite')) ?> / Wenigtrinker</h3>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead><tr><th>Person</th><th>Striche</th><th>Bereits erfasst</th></tr></thead>
|
||||
<tbody><?php foreach (($printListGroups['back'] ?? []) as $item): ?><tr><td><?= h((string) $item['member_name']) ?></td><td><input type="number" min="0" step="1" name="strokes[<?= h((string) $item['id']) ?>][back]" value="<?= h((string) (($printListItemMap[(string) $item['id']]['back_strokes'] ?? 0))) ?>"></td><td><?= h((string) (($printListItemMap[(string) $item['id']]['back_strokes'] ?? 0))) ?></td></tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
<?php if (($printListGroups['back'] ?? []) !== []): ?>
|
||||
<article>
|
||||
<h3><?= h((string) ($tenantSettings['back_page_label'] ?? 'Rückseite')) ?> / Wenigtrinker</h3>
|
||||
<div class="table">
|
||||
<table>
|
||||
<thead><tr><th>Person</th><th>Striche</th><th>Bereits erfasst</th></tr></thead>
|
||||
<tbody><?php foreach (($printListGroups['back'] ?? []) as $item): ?><tr><td><?= h((string) $item['member_name']) ?></td><td><input type="number" min="0" step="1" name="strokes[<?= h((string) $item['id']) ?>][back]" value="<?= h((string) (($printListItemMap[(string) $item['id']]['back_strokes'] ?? 0))) ?>"></td><td><?= h((string) (($printListItemMap[(string) $item['id']]['back_strokes'] ?? 0))) ?></td></tr><?php endforeach; ?></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if (false): ?><div class="table" style="display:none;margin-top:18px">
|
||||
<table>
|
||||
|
||||
@@ -22,6 +22,7 @@ CREATE TABLE IF NOT EXISTS license_plans (
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
plan_key VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
member_limit INT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
|
||||
$plans = [
|
||||
'starter' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a01', 'name' => 'Starter', 'sort_order' => 10],
|
||||
'team' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a02', 'name' => 'Team', 'sort_order' => 20],
|
||||
'business' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a03', 'name' => 'Business', 'sort_order' => 30],
|
||||
'enterprise' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a04', 'name' => 'Enterprise', 'sort_order' => 40],
|
||||
'free' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a00', 'name' => 'Free', 'sort_order' => 5, 'member_limit' => 10],
|
||||
'starter' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a01', 'name' => 'Starter', 'sort_order' => 10, 'member_limit' => 25],
|
||||
'team' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a02', 'name' => 'Team', 'sort_order' => 20, 'member_limit' => 75],
|
||||
'business' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a03', 'name' => 'Business', 'sort_order' => 30, 'member_limit' => 200],
|
||||
'enterprise' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a04', 'name' => 'Enterprise', 'sort_order' => 40, 'member_limit' => null],
|
||||
];
|
||||
|
||||
$features = [
|
||||
@@ -14,11 +15,11 @@ $features = [
|
||||
'content' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc1004', 'name' => 'Inhalte', 'description' => 'Hinweise, FAQ und einfache Inhalte pro Mandant.'],
|
||||
'tenant_settings' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2001', 'name' => 'Mandanten-Einstellungen', 'description' => 'Eigene Einstellungen pro Mandant verwalten.'],
|
||||
'pdf_export' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2002', 'name' => 'PDF-Listen', 'description' => 'Druckfertige Listen für den Export bereitstellen.'],
|
||||
'paper_strike_entry' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2003', 'name' => 'Papierlisten-Erfassung', 'description' => 'Striche aus Vorder- und Rückseiten-Listen nacherfassen.'],
|
||||
'basic_exports' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2004', 'name' => 'Basis-Exporte', 'description' => 'Grundlegende Exporte für operative Nutzung.'],
|
||||
'paper_strike_entry' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2003', 'name' => 'Papierlisten-Erfassung', 'description' => 'Striche aus Papierlisten sauber nacherfassen.'],
|
||||
'basic_exports' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2004', 'name' => 'Basis-Exporte', 'description' => 'Optionale CSV- und Datenexporte außerhalb des Standardumfangs.'],
|
||||
'oidc' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2005', 'name' => 'OIDC/SSO', 'description' => 'Zentrale Anmeldung über OIDC oder SSO.'],
|
||||
'imports' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2006', 'name' => 'Importe', 'description' => 'Import-Jobs für strukturierte Daten.'],
|
||||
'exports' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2007', 'name' => 'Erweiterte Exporte', 'description' => 'Zusätzliche Export-Jobs und Datenexporte.'],
|
||||
'exports' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2007', 'name' => 'Erweiterte Exporte', 'description' => 'Zusätzliche Export-Jobs und Datenausleitungen.'],
|
||||
'notifications' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2008', 'name' => 'Benachrichtigungen', 'description' => 'Systemische Benachrichtigungen pro Mandant.'],
|
||||
'surveys' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2009', 'name' => 'Umfragen', 'description' => 'Umfragen und Rückmeldungen erfassen.'],
|
||||
'advanced_reporting' => ['id' => '8f6c468f-8c14-4681-b262-4c8ffacc2010', 'name' => 'Erweiterte Auswertungen', 'description' => 'Vertiefte Berichte und Auswertungen.'],
|
||||
@@ -28,20 +29,31 @@ $features = [
|
||||
];
|
||||
|
||||
$planFeatures = [
|
||||
'starter' => ['members', 'ledger', 'payments', 'content'],
|
||||
'team' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry', 'basic_exports'],
|
||||
'business' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry', 'basic_exports', 'oidc', 'imports', 'exports', 'notifications', 'surveys', 'advanced_reporting'],
|
||||
'free' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry'],
|
||||
'starter' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry'],
|
||||
'team' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry', 'notifications'],
|
||||
'business' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry', 'oidc', 'imports', 'exports', 'notifications', 'surveys', 'advanced_reporting'],
|
||||
'enterprise' => array_keys($features),
|
||||
];
|
||||
|
||||
$statements = [];
|
||||
|
||||
foreach ($plans as $planKey => $plan) {
|
||||
$memberLimit = $plan['member_limit'] === null ? 'NULL' : (string) (int) $plan['member_limit'];
|
||||
$escapedName = addslashes($plan['name']);
|
||||
$statements[] = sprintf(
|
||||
"INSERT INTO license_plans (id, plan_key, name, sort_order, is_active, created_at, updated_at)\nSELECT '%s', '%s', '%s', %d, 1, NOW(), NOW()\nWHERE NOT EXISTS (SELECT 1 FROM license_plans WHERE plan_key = '%s');",
|
||||
"INSERT INTO license_plans (id, plan_key, name, member_limit, sort_order, is_active, created_at, updated_at)\nSELECT '%s', '%s', '%s', %s, %d, 1, NOW(), NOW()\nWHERE NOT EXISTS (SELECT 1 FROM license_plans WHERE plan_key = '%s');",
|
||||
$plan['id'],
|
||||
$planKey,
|
||||
addslashes($plan['name']),
|
||||
$escapedName,
|
||||
$memberLimit,
|
||||
$plan['sort_order'],
|
||||
$planKey
|
||||
);
|
||||
$statements[] = sprintf(
|
||||
"UPDATE license_plans SET name = '%s', member_limit = %s, sort_order = %d, is_active = 1, updated_at = NOW() WHERE plan_key = '%s';",
|
||||
$escapedName,
|
||||
$memberLimit,
|
||||
$plan['sort_order'],
|
||||
$planKey
|
||||
);
|
||||
@@ -56,8 +68,21 @@ foreach ($features as $featureKey => $feature) {
|
||||
addslashes($feature['description']),
|
||||
$featureKey
|
||||
);
|
||||
$statements[] = sprintf(
|
||||
"UPDATE features SET name = '%s', description = '%s', is_active = 1, updated_at = NOW() WHERE feature_key = '%s';",
|
||||
addslashes($feature['name']),
|
||||
addslashes($feature['description']),
|
||||
$featureKey
|
||||
);
|
||||
}
|
||||
|
||||
$statements[] = <<<'SQL'
|
||||
DELETE lpf
|
||||
FROM license_plan_features lpf
|
||||
INNER JOIN features f ON f.id = lpf.feature_id
|
||||
WHERE f.feature_key = 'basic_exports';
|
||||
SQL;
|
||||
|
||||
foreach ($planFeatures as $planKey => $featureKeys) {
|
||||
foreach ($featureKeys as $featureKey) {
|
||||
$mappingId = scripts_uuid_from_string('license-plan-feature:' . $planKey . ':' . $featureKey);
|
||||
@@ -74,6 +99,6 @@ foreach ($planFeatures as $planKey => $featureKeys) {
|
||||
return [
|
||||
'key' => '2026_03_22_000002_seed_license_plans_and_features',
|
||||
'title' => 'Lizenzpläne und Features füllen',
|
||||
'description' => 'Legt die SaaS-Lizenzstufen und die dazugehörigen Feature-Flags an.',
|
||||
'description' => 'Legt die mitgliederbasierten SaaS-Lizenzstufen und die dazugehörigen Feature-Flags an.',
|
||||
'statements' => $statements,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'key' => '2026_03_22_000005_add_member_limit_to_license_plans',
|
||||
'title' => 'Mitgliederlimit für Lizenzpläne ergänzen',
|
||||
'description' => 'Erweitert die Lizenzpläne um ein optionales Limit für aktive Mitglieder.',
|
||||
'statements' => [
|
||||
<<<'SQL'
|
||||
ALTER TABLE license_plans
|
||||
ADD COLUMN IF NOT EXISTS member_limit INT NULL AFTER name;
|
||||
SQL,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
$plans = [
|
||||
'free' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a00', 'name' => 'Free', 'sort_order' => 5, 'member_limit' => 10],
|
||||
'starter' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a01', 'name' => 'Starter', 'sort_order' => 10, 'member_limit' => 25],
|
||||
'team' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a02', 'name' => 'Team', 'sort_order' => 20, 'member_limit' => 75],
|
||||
'business' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a03', 'name' => 'Business', 'sort_order' => 30, 'member_limit' => 200],
|
||||
'enterprise' => ['id' => '0f6e004d-ec4c-4f12-8f91-fdf795b80a04', 'name' => 'Enterprise', 'sort_order' => 40, 'member_limit' => null],
|
||||
];
|
||||
|
||||
$planFeatures = [
|
||||
'free' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry'],
|
||||
'starter' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry'],
|
||||
'team' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry', 'notifications'],
|
||||
'business' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry', 'oidc', 'imports', 'exports', 'notifications', 'surveys', 'advanced_reporting'],
|
||||
'enterprise' => ['members', 'ledger', 'payments', 'content', 'tenant_settings', 'pdf_export', 'paper_strike_entry', 'basic_exports', 'oidc', 'imports', 'exports', 'notifications', 'surveys', 'advanced_reporting', 'white_label', 'custom_features', 'priority_updates'],
|
||||
];
|
||||
|
||||
$statements = [];
|
||||
|
||||
foreach ($plans as $planKey => $plan) {
|
||||
$memberLimit = $plan['member_limit'] === null ? 'NULL' : (string) (int) $plan['member_limit'];
|
||||
$statements[] = sprintf(
|
||||
"INSERT INTO license_plans (id, plan_key, name, member_limit, sort_order, is_active, created_at, updated_at)\nSELECT '%s', '%s', '%s', %s, %d, 1, NOW(), NOW()\nWHERE NOT EXISTS (SELECT 1 FROM license_plans WHERE plan_key = '%s');",
|
||||
$plan['id'],
|
||||
$planKey,
|
||||
addslashes($plan['name']),
|
||||
$memberLimit,
|
||||
$plan['sort_order'],
|
||||
$planKey
|
||||
);
|
||||
$statements[] = sprintf(
|
||||
"UPDATE license_plans SET name = '%s', member_limit = %s, sort_order = %d, is_active = 1, updated_at = NOW() WHERE plan_key = '%s';",
|
||||
addslashes($plan['name']),
|
||||
$memberLimit,
|
||||
$plan['sort_order'],
|
||||
$planKey
|
||||
);
|
||||
}
|
||||
|
||||
$statements[] = <<<'SQL'
|
||||
DELETE lpf
|
||||
FROM license_plan_features lpf
|
||||
INNER JOIN features f ON f.id = lpf.feature_id
|
||||
WHERE f.feature_key = 'basic_exports'
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM license_plans lp
|
||||
WHERE lp.id = lpf.license_plan_id
|
||||
AND lp.plan_key <> 'enterprise'
|
||||
);
|
||||
SQL;
|
||||
|
||||
foreach ($planFeatures as $planKey => $featureKeys) {
|
||||
foreach ($featureKeys as $featureKey) {
|
||||
$mappingId = scripts_uuid_from_string('license-plan-feature:' . $planKey . ':' . $featureKey);
|
||||
$statements[] = sprintf(
|
||||
"INSERT INTO license_plan_features (id, license_plan_id, feature_id, created_at)\nSELECT '%s', '%s', f.id, NOW()\nFROM features f\nINNER JOIN license_plans lp ON lp.id = '%s'\nWHERE f.feature_key = '%s'\n AND NOT EXISTS (\n SELECT 1 FROM license_plan_features lpf WHERE lpf.license_plan_id = '%s' AND lpf.feature_id = f.id\n );",
|
||||
$mappingId,
|
||||
$plans[$planKey]['id'],
|
||||
$plans[$planKey]['id'],
|
||||
$featureKey,
|
||||
$plans[$planKey]['id']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'key' => '2026_03_22_000006_align_member_based_license_model',
|
||||
'title' => 'Mitgliederbasiertes Lizenzmodell ausrichten',
|
||||
'description' => 'Stellt Free-, Starter-, Team-, Business- und Enterprise-Pläne mit passenden Mitgliedergrenzen bereit.',
|
||||
'statements' => $statements,
|
||||
];
|
||||
Reference in New Issue
Block a user