diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..21ef87d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "sqltools.connections": [ + { + "mysqlOptions": { + "authProtocol": "default", + "enableSsl": "Disabled" + }, + "ssh": "Disabled", + "previewLimit": 50, + "server": "mysql2f5b.netcup.net", + "port": 3306, + "driver": "MySQL", + "username": "k25330_kaffeetest", + "password": "k0Zg48~g75634", + "database": "k25330_kaffeetest", + "name": "kaffeeliste Test" + } + ] +} \ No newline at end of file diff --git a/saas-app/public/app-support.php b/saas-app/public/app-support.php index 4d78635..6adc573 100644 --- a/saas-app/public/app-support.php +++ b/saas-app/public/app-support.php @@ -808,6 +808,68 @@ function app_members_support_payment_reference(PDO $pdo): bool return app_table_has_column($pdo, 'members', 'payment_reference'); } +function app_ensure_member_rows_for_tenant(PDO $pdo, string $tenantId): void +{ + if ($tenantId === '' || !scripts_table_exists($pdo, 'members') || !scripts_table_exists($pdo, 'tenant_users')) { + return; + } + + $missingRows = app_query_all( + $pdo, + <<<'SQL' +SELECT + tu.id AS tenant_user_id, + tu.status AS tenant_user_status, + u.email, + u.display_name +FROM tenant_users tu +INNER JOIN users u ON u.id = tu.user_id +LEFT JOIN members m ON m.tenant_user_id = tu.id AND m.tenant_id = tu.tenant_id +WHERE tu.tenant_id = :tenant_id + AND m.id IS NULL +SQL, + ['tenant_id' => $tenantId] + ); + + if ($missingRows === []) { + return; + } + + $now = date('Y-m-d H:i:s'); + + foreach ($missingRows as $row) { + $status = ((string) ($row['tenant_user_status'] ?? 'active')) === 'inactive' ? 'inactive' : 'active'; + $params = [ + 'id' => app_uuid(), + 'tenant_id' => $tenantId, + 'tenant_user_id' => (string) ($row['tenant_user_id'] ?? ''), + 'display_name' => trim((string) ($row['display_name'] ?? '')) !== '' + ? trim((string) ($row['display_name'] ?? '')) + : trim((string) ($row['email'] ?? 'Mitglied')), + 'email' => strtolower(trim((string) ($row['email'] ?? ''))), + 'status' => $status, + 'created_at' => $now, + 'updated_at' => $now, + ]; + + if (app_members_support_payment_reference($pdo)) { + $params['payment_reference'] = null; + app_execute( + $pdo, + 'INSERT INTO members (id, tenant_id, tenant_user_id, display_name, email, payment_reference, status, created_at, updated_at) VALUES (:id, :tenant_id, :tenant_user_id, :display_name, :email, :payment_reference, :status, :created_at, :updated_at)', + $params + ); + continue; + } + + app_execute( + $pdo, + 'INSERT INTO members (id, tenant_id, tenant_user_id, display_name, email, status, created_at, updated_at) VALUES (:id, :tenant_id, :tenant_user_id, :display_name, :email, :status, :created_at, :updated_at)', + $params + ); + } +} + function app_tenant_user_by_id(PDO $pdo, string $tenantUserId, string $tenantId): ?array { $paymentReferenceSelect = app_members_support_payment_reference($pdo) @@ -3086,15 +3148,45 @@ SQL, function app_members_for_tenant(PDO $pdo, string $tenantId): array { + app_ensure_member_rows_for_tenant($pdo, $tenantId); + $paymentReferenceSelect = app_members_support_payment_reference($pdo) ? 'm.payment_reference,' : 'NULL AS payment_reference,'; - $sql = <<<'SQL' + $tenantUserSql = <<<'SQL' SELECT m.id, tu.id AS tenant_user_id, tu.user_id, + COALESCE(m.display_name, u.display_name) AS display_name, + COALESCE(m.email, u.email) AS email, + __PAYMENT_REFERENCE__ + COALESCE(m.status, tu.status) AS status, + u.display_name AS user_display_name, + GROUP_CONCAT(DISTINCT r.name ORDER BY r.name SEPARATOR ', ') AS roles, + GROUP_CONCAT(DISTINCT r.role_key ORDER BY r.role_key SEPARATOR ',') AS role_keys, + COALESCE(( + SELECT SUM(le.amount) + FROM ledger_entries le + WHERE le.member_id = m.id + AND le.tenant_id = tu.tenant_id + ), 0) AS current_balance +FROM tenant_users tu +INNER JOIN users u ON u.id = tu.user_id +LEFT JOIN members m ON m.tenant_user_id = tu.id AND m.tenant_id = tu.tenant_id +LEFT JOIN tenant_user_roles tur ON tur.tenant_user_id = tu.id +LEFT JOIN roles r ON r.id = tur.role_id +WHERE tu.tenant_id = :tenant_id +GROUP BY m.id, tu.id, tu.user_id, m.display_name, m.email, payment_reference, m.status, tu.status, u.display_name, u.email +ORDER BY COALESCE(m.display_name, u.display_name) ASC +SQL; + + $memberOnlySql = <<<'SQL' +SELECT + m.id, + m.tenant_user_id, + tu.user_id, m.display_name, m.email, __PAYMENT_REFERENCE__ @@ -3109,16 +3201,70 @@ SELECT AND le.tenant_id = m.tenant_id ), 0) AS current_balance FROM members m -LEFT JOIN tenant_users tu ON tu.id = m.tenant_user_id +LEFT JOIN tenant_users tu ON tu.id = m.tenant_user_id AND tu.tenant_id = m.tenant_id LEFT JOIN users u ON u.id = tu.user_id LEFT JOIN tenant_user_roles tur ON tur.tenant_user_id = tu.id LEFT JOIN roles r ON r.id = tur.role_id WHERE m.tenant_id = :tenant_id -GROUP BY m.id, tu.id, tu.user_id, m.display_name, m.email, payment_reference, m.status, u.display_name + AND ( + m.tenant_user_id IS NULL + OR tu.id IS NULL + ) +GROUP BY m.id, m.tenant_user_id, tu.user_id, m.display_name, m.email, payment_reference, m.status, u.display_name ORDER BY m.display_name ASC SQL; - return app_query_all($pdo, str_replace('__PAYMENT_REFERENCE__', $paymentReferenceSelect, $sql), ['tenant_id' => $tenantId]); + $rows = app_query_all( + $pdo, + str_replace('__PAYMENT_REFERENCE__', $paymentReferenceSelect, $tenantUserSql), + ['tenant_id' => $tenantId] + ); + + foreach ( + app_query_all( + $pdo, + str_replace('__PAYMENT_REFERENCE__', $paymentReferenceSelect, $memberOnlySql), + ['tenant_id' => $tenantId] + ) as $row + ) { + $rows[] = $row; + } + + $deduped = []; + foreach ($rows as $row) { + $id = trim((string) ($row['id'] ?? '')); + $tenantUserId = trim((string) ($row['tenant_user_id'] ?? '')); + $email = strtolower(trim((string) ($row['email'] ?? ''))); + $displayName = trim((string) ($row['display_name'] ?? '')); + $key = $id !== '' + ? 'member:' . $id + : ($tenantUserId !== '' + ? 'tenant-user:' . $tenantUserId + : ($email !== '' ? 'email:' . $email : 'name:' . $displayName)); + + if (!isset($deduped[$key])) { + $deduped[$key] = $row; + continue; + } + + $existing = $deduped[$key]; + $existingHasMemberId = trim((string) ($existing['id'] ?? '')) !== ''; + $rowHasMemberId = $id !== ''; + if (!$existingHasMemberId && $rowHasMemberId) { + $deduped[$key] = $row; + } + } + + $result = array_values($deduped); + usort( + $result, + static fn(array $left, array $right): int => strcasecmp( + trim((string) ($left['display_name'] ?? '')), + trim((string) ($right['display_name'] ?? '')) + ) + ); + + return $result; } function app_normalize_datetime_input(?string $value): string @@ -3205,32 +3351,50 @@ SQL, function app_members_for_scope(PDO $pdo, string $tenantId, string $scope = 'all'): array { $coffeeActiveSql = app_finance_entry_active_sql($pdo, 'coffee_entries', 'ce'); - $rows = app_query_all( + $members = array_values(array_filter( + app_members_for_tenant($pdo, $tenantId), + static function (array $member): bool { + $status = strtolower(trim((string) ($member['status'] ?? 'active'))); + return !in_array($status, ['inactive', 'disabled', 'archived'], true); + } + )); + + if ($members === []) { + return []; + } + + $recentStrokeRows = app_query_all( $pdo, str_replace( '__COFFEE_ACTIVE__', $coffeeActiveSql, <<<'SQL' SELECT - m.id, - m.display_name, - COALESCE(( - SELECT SUM(ce.strokes) - FROM coffee_entries ce - WHERE ce.tenant_id = m.tenant_id - AND ce.member_id = m.id - AND __COFFEE_ACTIVE__ - AND ce.booked_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 100 DAY) - ), 0) AS recent_strokes -FROM members m -WHERE m.tenant_id = :tenant_id - AND m.status = 'active' -ORDER BY m.display_name ASC -SQL, + ce.member_id, + COALESCE(SUM(ce.strokes), 0) AS recent_strokes +FROM coffee_entries ce +WHERE ce.tenant_id = :tenant_id + AND __COFFEE_ACTIVE__ + AND ce.booked_at >= DATE_SUB(CURRENT_DATE(), INTERVAL 100 DAY) +GROUP BY ce.member_id +SQL ), ['tenant_id' => $tenantId] ); + $recentStrokeMap = []; + foreach ($recentStrokeRows as $row) { + $recentStrokeMap[(string) ($row['member_id'] ?? '')] = (int) ($row['recent_strokes'] ?? 0); + } + + $rows = array_map( + static function (array $member) use ($recentStrokeMap): array { + $member['recent_strokes'] = $recentStrokeMap[(string) ($member['id'] ?? '')] ?? 0; + return $member; + }, + $members + ); + if ($scope === 'front') { return array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['recent_strokes'] ?? 0) >= 10)); } @@ -3279,16 +3443,26 @@ function app_paypal_amount_links(array $settings, float $balance): array function app_ledger_for_tenant(PDO $pdo, string $tenantId): array { - $coffeeStatusSelect = app_finance_entries_support_status($pdo, 'coffee_entries') + $coffeeSupportsStatus = app_finance_entries_support_status($pdo, 'coffee_entries'); + $paymentSupportsStatus = app_finance_entries_support_status($pdo, 'payment_entries'); + + $coffeeStatusSelect = $coffeeSupportsStatus ? 'ce.status AS coffee_reference_status,' : "'booked' AS coffee_reference_status,"; - $coffeeCancelledSelect = app_finance_entries_support_status($pdo, 'coffee_entries') + $coffeeCancelledSelect = $coffeeSupportsStatus ? 'ce.cancelled_at AS coffee_reference_cancelled_at,' : 'NULL AS coffee_reference_cancelled_at,'; - $paymentStatusSelect = app_finance_entries_support_status($pdo, 'payment_entries') + $paymentStatusSelect = $paymentSupportsStatus ? 'pe.status AS payment_reference_status, pe.cancelled_at AS payment_cancelled_at,' : "'booked' AS payment_reference_status, NULL AS payment_cancelled_at,"; - $referenceStatusSql = app_finance_entries_support_status($pdo, 'coffee_entries') || app_finance_entries_support_status($pdo, 'payment_entries') + $referenceCancelledAtSql = match (true) { + $coffeeSupportsStatus && $paymentSupportsStatus => 'COALESCE(ce.cancelled_at, pe.cancelled_at) AS reference_cancelled_at,', + $coffeeSupportsStatus => 'ce.cancelled_at AS reference_cancelled_at,', + $paymentSupportsStatus => 'pe.cancelled_at AS reference_cancelled_at,', + default => 'NULL AS reference_cancelled_at,', + }; + + $referenceStatusSql = $coffeeSupportsStatus || $paymentSupportsStatus ? <<<'SQL' CASE WHEN le.reference_type = 'coffee_entry' THEN COALESCE(ce.status, 'booked') @@ -3300,7 +3474,7 @@ SQL $sql = str_replace( ['__COFFEE_STATUS__', '__COFFEE_CANCELLED__', '__PAYMENT_STATUS__', '__REFERENCE_STATUS__', '__REFERENCE_CANCELLED_AT__'], - [$coffeeStatusSelect, $coffeeCancelledSelect, $paymentStatusSelect, $referenceStatusSql, 'COALESCE(coffee_reference_cancelled_at, payment_cancelled_at) AS reference_cancelled_at,'], + [$coffeeStatusSelect, $coffeeCancelledSelect, $paymentStatusSelect, $referenceStatusSql, $referenceCancelledAtSql], <<<'SQL' SELECT le.id, @@ -3331,8 +3505,8 @@ SQL function app_payments_for_tenant(PDO $pdo, string $tenantId): array { $paymentStatusSelect = app_finance_entries_support_status($pdo, 'payment_entries') - ? 'pe.status, pe.cancelled_at, pe.cancellation_reason,' - : "'booked' AS status, NULL AS cancelled_at, NULL AS cancellation_reason,"; + ? 'pe.status, pe.cancelled_at, pe.cancellation_reason' + : "'booked' AS status, NULL AS cancelled_at, NULL AS cancellation_reason"; $sql = str_replace( '__PAYMENT_STATUS__', diff --git a/saas-app/public/index.php b/saas-app/public/index.php index 807d25a..c9b0844 100644 --- a/saas-app/public/index.php +++ b/saas-app/public/index.php @@ -362,6 +362,21 @@ if ($auth !== null && $pdo instanceof PDO) { ? (string) ($_GET['scope'] ?? 'all') : 'all'; $ledgerMembers = app_members_for_scope($pdo, (string) $auth['tenant_id'], $ledgerScope); + if ($ledgerMembers === [] && $members !== []) { + $ledgerMembers = array_map( + static function (array $member): array { + $member['recent_strokes'] = (int) ($member['recent_strokes'] ?? 0); + return $member; + }, + array_values(array_filter( + $members, + static function (array $member): bool { + $status = strtolower(trim((string) ($member['status'] ?? 'active'))); + return !in_array($status, ['inactive', 'disabled', 'archived'], true); + } + )) + ); + } } if ($page === 'payments') { @@ -369,6 +384,21 @@ if ($auth !== null && $pdo instanceof PDO) { ? (string) ($_GET['scope'] ?? 'all') : 'all'; $paymentMembers = app_members_for_scope($pdo, (string) $auth['tenant_id'], $paymentScope); + if ($paymentMembers === [] && $members !== []) { + $paymentMembers = array_map( + static function (array $member): array { + $member['recent_strokes'] = (int) ($member['recent_strokes'] ?? 0); + return $member; + }, + array_values(array_filter( + $members, + static function (array $member): bool { + $status = strtolower(trim((string) ($member['status'] ?? 'active'))); + return !in_array($status, ['inactive', 'disabled', 'archived'], true); + } + )) + ); + } } if ($page === 'content' || $page === 'dashboard') { @@ -1505,7 +1535,7 @@ $marketing = app_marketing_messages();
- +
@@ -1560,7 +1590,7 @@ $marketing = app_marketing_messages();
- +