1
0

big restructure

This commit is contained in:
2026-02-11 19:28:12 +01:00
parent cd59ccd99b
commit 3eb9cc0ac4
209 changed files with 5101 additions and 2459 deletions

View File

@@ -4,5 +4,6 @@ return array (
'app_locale' => 'de',
'app_theme' => 'dark',
'app_theme_user' => '1',
'app_primary_color' => '#9b3dc7',
'app_registration' => '1',
'app_primary_color' => '#105433',
);

7
config/themes.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
return [
'light' => 'Light',
'dark' => 'Dark',
'dark-green' => 'Dark green',
];

View File

@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`uuid` CHAR(36) NOT NULL,
`first_name` VARCHAR(100) NOT NULL,
`last_name` VARCHAR(100) NOT NULL,
`display_name` VARCHAR(210) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`profile_description` TEXT NULL,
`job_title` VARCHAR(160) NULL,
@@ -19,6 +20,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`region` VARCHAR(100) NULL,
`hire_date` DATE NULL,
`email_verified_at` DATETIME NULL DEFAULT NULL,
`last_login_at` DATETIME NULL DEFAULT NULL,
`password` VARCHAR(255) NOT NULL,
`locale` VARCHAR(10) DEFAULT NULL,
`totp_secret` VARCHAR(255) DEFAULT NULL,
@@ -88,12 +90,16 @@ CREATE TABLE IF NOT EXISTS `roles` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uuid` CHAR(36) NOT NULL,
`description` VARCHAR(255) NOT NULL,
`code` VARCHAR(50) DEFAULT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`created_by` INT UNSIGNED DEFAULT NULL,
`modified_by` INT UNSIGNED DEFAULT NULL,
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modified` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_roles_uuid` (`uuid`),
UNIQUE KEY `uniq_roles_code` (`code`),
KEY `idx_roles_active` (`active`),
KEY `idx_roles_created_by` (`created_by`),
KEY `idx_roles_modified_by` (`modified_by`),
CONSTRAINT `fk_roles_created_by` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
@@ -104,21 +110,29 @@ CREATE TABLE IF NOT EXISTS `permissions` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`key` VARCHAR(191) NOT NULL,
`description` VARCHAR(255) DEFAULT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`is_system` TINYINT(1) NOT NULL DEFAULT 0,
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_permissions_key` (`key`)
UNIQUE KEY `uniq_permissions_key` (`key`),
KEY `idx_permissions_active` (`active`),
KEY `idx_permissions_system` (`is_system`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `departments` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`uuid` CHAR(36) NOT NULL,
`description` VARCHAR(255) NOT NULL,
`code` VARCHAR(50) DEFAULT NULL,
`cost_center` VARCHAR(50) DEFAULT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
`created_by` INT UNSIGNED DEFAULT NULL,
`modified_by` INT UNSIGNED DEFAULT NULL,
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modified` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_departments_uuid` (`uuid`),
KEY `idx_departments_active` (`active`),
KEY `idx_departments_created_by` (`created_by`),
KEY `idx_departments_modified_by` (`modified_by`),
CONSTRAINT `fk_departments_created_by` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE SET NULL,
@@ -274,6 +288,7 @@ CREATE TABLE IF NOT EXISTS `user_remember_tokens` (
`selector` CHAR(24) NOT NULL,
`token_hash` CHAR(64) NOT NULL,
`expires_at` DATETIME NOT NULL,
`expired_by_admin_at` DATETIME NULL DEFAULT NULL COMMENT 'Expired by admin action',
`created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_used` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`),
@@ -287,18 +302,19 @@ INSERT INTO `tenants` (`uuid`, `description`, `created`)
SELECT UUID(), 'Cronus', NOW()
WHERE NOT EXISTS (SELECT 1 FROM tenants WHERE description = 'Cronus');
INSERT INTO `departments` (`uuid`, `description`, `created`)
SELECT UUID(), 'IT', NOW()
INSERT INTO `departments` (`uuid`, `description`, `code`, `active`, `created`)
SELECT UUID(), 'IT', 'IT', 1, NOW()
WHERE NOT EXISTS (SELECT 1 FROM departments WHERE description = 'IT');
INSERT INTO `roles` (`uuid`, `description`, `created`)
SELECT UUID(), 'Admin', NOW()
INSERT INTO `roles` (`uuid`, `description`, `code`, `active`, `created`)
SELECT UUID(), 'Admin', 'ADMIN', 1, NOW()
WHERE NOT EXISTS (SELECT 1 FROM roles WHERE description = 'Admin');
INSERT INTO `users` (
`uuid`,
`first_name`,
`last_name`,
`display_name`,
`email`,
`password`,
`locale`,
@@ -315,6 +331,7 @@ SELECT
UUID(),
'Detlef',
'Demo',
'Detlef Demo',
'demo@user.com',
'$2y$12$KVCQvuy4Pl1aySBuzSpc7ehpZhAzYZkndDI9OaMi05E2P/Mhob5HO',
'de',
@@ -375,42 +392,47 @@ WHERE NOT EXISTS (
SELECT 1 FROM page_contents pc WHERE pc.page_id = p.id AND pc.locale = 'de'
);
INSERT INTO `permissions` (`key`, `description`)
INSERT INTO `permissions` (`key`, `description`, `active`, `is_system`)
VALUES
('users.view', 'Can view users'),
('users.create', 'Can create users'),
('users.update', 'Can update users'),
('users.delete', 'Can delete users'),
('users.self_update', 'Can update own user'),
('users.update_assignments', 'Can update user assignments'),
('address_book.view', 'Can view address book'),
('tenants.view', 'Can view tenants'),
('tenants.create', 'Can create tenants'),
('tenants.update', 'Can update tenants'),
('tenants.delete', 'Can delete tenants'),
('departments.view', 'Can view departments'),
('departments.create', 'Can create departments'),
('departments.update', 'Can update departments'),
('departments.delete', 'Can delete departments'),
('roles.view', 'Can view roles'),
('roles.create', 'Can create roles'),
('roles.update', 'Can update roles'),
('roles.delete', 'Can delete roles'),
('permissions.view', 'Can view permissions'),
('permissions.create', 'Can create permissions'),
('permissions.update', 'Can update permissions'),
('permissions.delete', 'Can delete permissions'),
('settings.view', 'Can view settings'),
('settings.update', 'Can update settings'),
('mail_log.view', 'Can view mail logs'),
('stats.view', 'Can view statistics')
ON DUPLICATE KEY UPDATE `description` = VALUES(`description`);
('users.view', 'Can view users', 1, 1),
('users.view_meta', 'Can view user status and meta', 1, 1),
('users.view_audit', 'Can view user audit log', 1, 1),
('users.create', 'Can create users', 1, 1),
('users.update', 'Can update users', 1, 1),
('users.delete', 'Can delete users', 1, 1),
('users.self_update', 'Can update own user', 1, 1),
('users.update_assignments', 'Can update user assignments', 1, 1),
('address_book.view', 'Can view address book', 1, 1),
('tenants.view', 'Can view tenants', 1, 1),
('tenants.create', 'Can create tenants', 1, 1),
('tenants.update', 'Can update tenants', 1, 1),
('tenants.delete', 'Can delete tenants', 1, 1),
('departments.view', 'Can view departments', 1, 1),
('departments.create', 'Can create departments', 1, 1),
('departments.update', 'Can update departments', 1, 1),
('departments.delete', 'Can delete departments', 1, 1),
('roles.view', 'Can view roles', 1, 1),
('roles.create', 'Can create roles', 1, 1),
('roles.update', 'Can update roles', 1, 1),
('roles.delete', 'Can delete roles', 1, 1),
('permissions.view', 'Can view permissions', 1, 1),
('permissions.create', 'Can create permissions', 1, 1),
('permissions.update', 'Can update permissions', 1, 1),
('permissions.delete', 'Can delete permissions', 1, 1),
('settings.view', 'Can view settings', 1, 1),
('settings.update', 'Can update settings', 1, 1),
('mail_log.view', 'Can view mail logs', 1, 1),
('stats.view', 'Can view statistics', 1, 1)
ON DUPLICATE KEY UPDATE
`description` = VALUES(`description`),
`active` = VALUES(`active`),
`is_system` = VALUES(`is_system`);
INSERT INTO `role_permissions` (`role_id`, `permission_id`, `created`)
SELECT r.id, p.id, NOW()
FROM roles r
JOIN permissions p ON p.`key` IN (
'users.view', 'users.create', 'users.update', 'users.delete',
'users.view', 'users.view_meta', 'users.view_audit', 'users.create', 'users.update', 'users.delete',
'users.self_update', 'users.update_assignments',
'address_book.view',
'tenants.view', 'tenants.create', 'tenants.update', 'tenants.delete',

View File

@@ -405,7 +405,9 @@ can('users.view');
```
web/css/
├── base/
── variables.css # CSS-Variablen
── variables.base.css # CSS-Variablen (Basis + Light/Dark)
│ ├── variables.theme-dark-green.css # Theme-Overrides
│ └── variables.contrast.css # High-Contrast Overrides
├── layout/
│ ├── app-shell.css # Haupt-Layout
│ ├── app-topbar.css # Header

24
docs/todo-2fa.md Normal file
View File

@@ -0,0 +1,24 @@
# TODO: 2FA (TOTP) Integration
Goal: Add optional TOTP-based 2FA for users (admin-managed, later self-managed).
Proposed library:
- Start with a lightweight TOTP library (e.g. remotemerge/totp-php) or a more established one (e.g. spomky-labs/otphp).
Data model:
- users: totp_secret (encrypted), totp_enabled (tinyint), totp_verified_at (nullable)
- user_backup_codes: user_id, code_hash, created_at, used_at (nullable)
Admin user edit flow:
- Enable 2FA: generate secret + show QR (otpauth URI).
- Verify code to activate.
- Generate backup codes (show once).
Login flow:
- After password: prompt TOTP if totp_enabled.
- Rate-limit attempts.
Security notes:
- Encrypt secret at rest.
- Accept small time drift (+/- 1 step).
- Default to SHA-1 for compatibility.

View File

@@ -148,6 +148,7 @@
"Theme": "Theme",
"Light": "Hell",
"Dark": "Dunkel",
"Dark green": "Dunkelgrün",
"Toggle theme": "Theme umschalten",
"Toggle Sidebar": "Sidebar umschalten",
"Toggle Detail Sidebar": "Detail-Seitenleiste umschalten",
@@ -242,6 +243,9 @@
"Files": "Dateien",
"Settings": "Einstellungen",
"System": "System",
"System permission": "Systemberechtigung",
"System permission can not be deleted": "Systemberechtigung kann nicht gelöscht werden",
"System permissions are core and should rarely be disabled.": "Systemberechtigungen sind kernrelevant und sollten nur selten deaktiviert werden.",
"Statistics": "Statistiken",
"Organization breakdown": "Organisations-Aufschlüsselung",
"Department assignments": "Abteilungs-Zuweisungen",
@@ -306,14 +310,17 @@
"Default language": "Standard-Sprache",
"setting.app_theme": "Standard-Theme, wenn der Benutzer keine Präferenz hat",
"setting.app_theme_user": "Benutzer dürfen ihr eigenes Theme wählen",
"setting.app_registration": "Öffentliche Selbstregistrierung erlauben",
"setting.app_primary_color": "Überschreibt die Standard-Primärfarbe (Hex wie #2FA4A4)",
"Default theme": "Standard-Theme",
"Allow user theme": "Benutzer-Theme erlauben",
"Allow registration": "Registrierung erlauben",
"General": "Allgemein",
"Appearance": "Darstellung",
"Use default color": "Standardfarbe verwenden",
"Tenants can override this color in their appearance settings.": "Mandanten können diese Farbe in ihren Darstellungseinstellungen überschreiben.",
"Using the default color applies the global system appearance.": "Standardfarbe verwenden heißt: Es werden die globalen Systemeinstellungen für die Darstellung genutzt.",
"Inactive tenants are excluded from user access and tenant-scoped data.": "Inaktive Mandanten werden vom Benutzerzugriff und mandantenspezifischen Daten ausgeschlossen.",
"Status & meta": "Status & Meta",
"Audit": "Audit",
"Overview": "Übersicht",
@@ -323,6 +330,10 @@
"Call mobile": "Mobil anrufen",
"Choose color": "Farbe wählen",
"Be careful - this resets the tenants color": "Achtung das setzt die Mandantenfarbe zurück",
"Code": "Code",
"Cost center": "Kostenstelle",
"Department code already exists": "Abteilungscode existiert bereits",
"Role code already exists": "Rollencode existiert bereits",
"Internationalization": "Internationalisierung",
"User creation rules": "Benutzeranlage-Regeln",
"Branding": "Branding",
@@ -449,6 +460,7 @@
"Switch tenant": "Mandant wechseln",
"Copy": "Kopieren",
"Registration successful! Please check your email for the verification code.": "Registrierung erfolgreich! Bitte prüfe deine E-Mails für den Bestätigungscode.",
"Registration is currently disabled": "Registrierung ist derzeit deaktiviert.",
"A new verification code has been sent.": "Ein neuer Bestätigungscode wurde gesendet.",
"Email already verified. Please login.": "E-Mail bereits bestätigt. Bitte einloggen.",
"If the email exists, a new verification code has been sent.": "Falls die E-Mail existiert, wurde ein neuer Bestätigungscode gesendet.",
@@ -477,5 +489,72 @@
"setting.smtp_password": "Leer lassen, um das bestehende Passwort zu behalten",
"setting.smtp_secure": "Verschlüsselung für SMTP",
"setting.smtp_from": "Absender-E-Mail für ausgehende Nachrichten",
"setting.smtp_from_name": "Absendername für ausgehende Nachrichten"
"setting.smtp_from_name": "Absendername für ausgehende Nachrichten",
"High contrast": "Hoher Kontrast",
"Toggle contrast": "Kontrast umschalten",
"View in address book": "Im Adressbuch ansehen",
"Overview": "Übersicht",
"Organization & Quality": "Organisation & Qualität",
"Security": "Sicherheit",
"Communication": "Kommunikation",
"Email security": "E-Mail Sicherheit",
"Recipient": "Empfänger",
"Error": "Fehler",
"Active tenants": "Aktive Mandanten",
"Inactive tenants": "Inaktive Mandanten",
"Active departments": "Aktive Abteilungen",
"Inactive departments": "Inaktive Abteilungen",
"Active roles": "Aktive Rollen",
"Inactive roles": "Inaktive Rollen",
"Roles without permissions": "Rollen ohne Berechtigungen",
"Permissions without roles": "Berechtigungen ohne Rollen",
"Users without roles": "Benutzer ohne Rollen",
"Users without tenants": "Benutzer ohne Mandant",
"Departments without tenants": "Abteilungen ohne Mandant",
"Mail failures": "E-Mail Fehler",
"Last email sent": "Letzte E-Mail gesendet",
"Never": "Nie",
"Recent logins": "Letzte Anmeldungen",
"Never logged in": "Nie angemeldet",
"Last login": "Letzte Anmeldung",
"User": "Benutzer",
"No entries found": "Keine Einträge gefunden",
"Login status": "Anmeldestatus",
"Logged in": "Angemeldet",
"More filters": "Weitere Filter",
"Email unverified": "E-Mail nicht verifiziert"
,
"Password resets": "Passwort-Resets",
"Requested at": "Angefordert am",
"Expires at": "Gültig bis",
"Used at": "Verwendet am",
"Attempts": "Versuche",
"Used": "Verwendet",
"Expired": "Abgelaufen",
"Expired by admin": "Durch Admin abgelaufen",
"Login tokens": "Login-Tokens",
"Expire all login tokens": "Alle Login-Tokens ablaufen",
"Expire all login tokens?": "Alle Login-Tokens ablaufen?",
"%d active login tokens": "%d aktive Login-Tokens",
"%d login tokens expired": "%d Login-Tokens abgelaufen",
"Danger zone": "Gefahrenzone",
"This will mark all remember-me tokens as expired. Users will need to sign in again.": "Alle Angemeldet-bleiben Tokens werden als abgelaufen markiert. Benutzer müssen sich erneut anmelden.",
"Last used": "Zuletzt verwendet",
"Keyboard shortcuts": "Tastenkürzel",
"Open search": "Suche öffnen",
"Switch sidebar section": "Sidebar-Bereich wechseln",
"Action": "Aktion",
"Mac": "Mac",
"Windows": "Windows",
"Email verification code": "E-Mail Verifizierungscode",
"Failed to switch tenant": "Mandantwechsel fehlgeschlagen",
"IDs": "IDs",
"Pages": "Seiten",
"Permission": "Berechtigung",
"Please verify your email first": "Bitte bestätige zuerst deine E-Mail",
"Toggle search details": "Suchdetails umschalten",
"Toggle sidebar": "Sidebar umschalten",
"Unverified": "Nicht verifiziert",
"Imprint": "Impressum",
"Privacy": "Datenschutz"
}

View File

@@ -148,6 +148,7 @@
"Theme": "Theme",
"Light": "Light",
"Dark": "Dark",
"Dark green": "Dark green",
"Toggle theme": "Toggle theme",
"Toggle Sidebar": "Toggle Sidebar",
"Toggle Detail Sidebar": "Toggle Detail Sidebar",
@@ -242,6 +243,9 @@
"Files": "Files",
"Settings": "Settings",
"System": "System",
"System permission": "System permission",
"System permission can not be deleted": "System permission can not be deleted",
"System permissions are core and should rarely be disabled.": "System permissions are core and should rarely be disabled.",
"Statistics": "Statistics",
"Organization breakdown": "Organization breakdown",
"Department assignments": "Department assignments",
@@ -306,14 +310,17 @@
"Default language": "Default language",
"setting.app_theme": "Default theme used when a user has no theme preference",
"setting.app_theme_user": "Allow users to choose their own theme",
"setting.app_registration": "Allow public self-registration",
"setting.app_primary_color": "Overrides the default primary color (hex like #2FA4A4)",
"Default theme": "Default theme",
"Allow user theme": "Allow user theme",
"Allow registration": "Allow registration",
"General": "General",
"Appearance": "Appearance",
"Use default color": "Use default color",
"Tenants can override this color in their appearance settings.": "Tenants can override this color in their appearance settings.",
"Using the default color applies the global system appearance.": "Using the default color applies the global system appearance.",
"Inactive tenants are excluded from user access and tenant-scoped data.": "Inactive tenants are excluded from user access and tenant-scoped data.",
"Status & meta": "Status & meta",
"Audit": "Audit",
"Overview": "Overview",
@@ -323,6 +330,10 @@
"Call mobile": "Call mobile",
"Choose color": "Choose color",
"Be careful - this resets the tenants color": "Be careful - this resets the tenant color",
"Code": "Code",
"Cost center": "Cost center",
"Department code already exists": "Department code already exists",
"Role code already exists": "Role code already exists",
"Internationalization": "Internationalization",
"User creation rules": "User creation rules",
"Branding": "Branding",
@@ -449,6 +460,7 @@
"Switch tenant": "Switch tenant",
"Copy": "Copy",
"Registration successful! Please check your email for the verification code.": "Registration successful! Please check your email for the verification code.",
"Registration is currently disabled": "Registration is currently disabled.",
"A new verification code has been sent.": "A new verification code has been sent.",
"Email already verified. Please login.": "Email already verified. Please login.",
"If the email exists, a new verification code has been sent.": "If the email exists, a new verification code has been sent.",
@@ -477,5 +489,72 @@
"setting.smtp_password": "Leave empty to keep the existing password",
"setting.smtp_secure": "SMTP encryption method",
"setting.smtp_from": "From address for outgoing emails",
"setting.smtp_from_name": "From name for outgoing emails"
"setting.smtp_from_name": "From name for outgoing emails",
"High contrast": "High contrast",
"Toggle contrast": "Toggle contrast",
"View in address book": "View in address book",
"Overview": "Overview",
"Organization & Quality": "Organization & Quality",
"Security": "Security",
"Communication": "Communication",
"Email security": "Email security",
"Recipient": "Recipient",
"Error": "Error",
"Active tenants": "Active tenants",
"Inactive tenants": "Inactive tenants",
"Active departments": "Active departments",
"Inactive departments": "Inactive departments",
"Active roles": "Active roles",
"Inactive roles": "Inactive roles",
"Roles without permissions": "Roles without permissions",
"Permissions without roles": "Permissions without roles",
"Users without roles": "Users without roles",
"Users without tenants": "Users without tenants",
"Departments without tenants": "Departments without tenants",
"Mail failures": "Mail failures",
"Last email sent": "Last email sent",
"Never": "Never",
"Recent logins": "Recent logins",
"Never logged in": "Never logged in",
"Last login": "Last login",
"User": "User",
"No entries found": "No entries found",
"Login status": "Login status",
"Logged in": "Logged in",
"More filters": "More filters",
"Email unverified": "Email unverified"
,
"Password resets": "Password resets",
"Requested at": "Requested at",
"Expires at": "Expires at",
"Used at": "Used at",
"Attempts": "Attempts",
"Used": "Used",
"Expired": "Expired",
"Expired by admin": "Expired by admin",
"Login tokens": "Login tokens",
"Expire all login tokens": "Expire all login tokens",
"Expire all login tokens?": "Expire all login tokens?",
"%d active login tokens": "%d active login tokens",
"%d login tokens expired": "%d login tokens expired",
"Danger zone": "Danger zone",
"This will mark all remember-me tokens as expired. Users will need to sign in again.": "This will mark all remember-me tokens as expired. Users will need to sign in again.",
"Last used": "Last used",
"Keyboard shortcuts": "Keyboard shortcuts",
"Open search": "Open search",
"Switch sidebar section": "Switch sidebar section",
"Action": "Action",
"Mac": "Mac",
"Windows": "Windows",
"Email verification code": "Email verification code",
"Failed to switch tenant": "Failed to switch tenant",
"IDs": "IDs",
"Pages": "Pages",
"Permission": "Permission",
"Please verify your email first": "Please verify your email first",
"Toggle search details": "Toggle search details",
"Toggle sidebar": "Toggle sidebar",
"Unverified": "Unverified",
"Imprint": "Imprint",
"Privacy": "Privacy"
}

View File

@@ -0,0 +1,140 @@
<?php
namespace MintyPHP\Repository\Access;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class PermissionRepository
{
public static function list(): array
{
$rows = DB::select('select id, `key`, description, active, is_system, created from permissions order by `key` asc');
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['permissions'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public static function listActive(): array
{
$rows = DB::select(
'select id, `key`, description, active, is_system, created from permissions where active = 1 order by `key` asc'
);
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['permissions'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public static function listPaged(array $options): array
{
$search = trim((string) ($options['search'] ?? ''));
$allowedOrder = [
'key' => '`key`',
'description' => 'description',
'active' => 'active',
'created' => 'created',
];
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($options);
[$order, $dir] = RepoQuery::sanitizeOrder($options, array_keys($allowedOrder), 'key', 'asc');
$orderBy = $allowedOrder[$order] ?? $allowedOrder['key'];
$where = [];
$params = [];
RepoQuery::addLikeFilter($where, $params, ['permissions.`key`', 'permissions.description'], $search);
$activeValue = array_key_exists('active', $options) ? $options['active'] : null;
RepoQuery::addEnumFilter($where, $params, $activeValue, [
['aliases' => ['1', 'true', 'active'], 'sql' => 'permissions.active = 1'],
['aliases' => ['0', 'false', 'inactive'], 'sql' => 'permissions.active = 0'],
]);
$whereSql = $where ? ' where ' . implode(' and ', $where) : '';
$total = DB::selectValue('select count(*) from permissions' . $whereSql, ...$params);
$query = 'select id, `key`, description, active, is_system, created from permissions' . $whereSql .
' order by ' . $orderBy . ' ' . $dir . ' limit ? offset ?';
$queryParams = array_merge($params, [(string) $limit, (string) $offset]);
$rows = DB::select($query, ...$queryParams);
$list = [];
foreach ($rows as $row) {
$data = $row['permissions'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return ['data' => $list, 'total' => (int) $total];
}
public static function find(int $id): ?array
{
$row = DB::selectOne(
'select id, `key`, description, active, is_system, created from permissions where id = ? limit 1',
(string) $id
);
$data = $row['permissions'] ?? $row;
return is_array($data) ? $data : null;
}
public static function findByKey(string $key): ?array
{
$row = DB::selectOne(
'select id, `key`, description, active, is_system, created from permissions where `key` = ? limit 1',
$key
);
$data = $row['permissions'] ?? $row;
return is_array($data) ? $data : null;
}
public static function create(array $data): ?int
{
$key = trim((string) ($data['key'] ?? ''));
$desc = trim((string) ($data['description'] ?? ''));
$active = (int) ($data['active'] ?? 1);
$isSystem = (int) ($data['is_system'] ?? 0);
$result = DB::insert(
'insert into permissions (`key`, description, active, is_system, created) values (?,?,?,?,NOW())',
$key,
$desc !== '' ? $desc : null,
(string) $active,
(string) $isSystem
);
return $result ? (int) $result : null;
}
public static function update(int $id, array $data): bool
{
$key = trim((string) ($data['key'] ?? ''));
$desc = trim((string) ($data['description'] ?? ''));
$active = (int) ($data['active'] ?? 1);
$isSystem = (int) ($data['is_system'] ?? 0);
$result = DB::update(
'update permissions set `key` = ?, description = ?, active = ?, is_system = ? where id = ?',
$key,
$desc !== '' ? $desc : null,
(string) $active,
(string) $isSystem,
(string) $id
);
return $result !== false;
}
public static function delete(int $id): bool
{
$result = DB::delete('delete from permissions where id = ?', (string) $id);
return (bool) $result;
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Access;
use MintyPHP\DB;
@@ -79,7 +79,9 @@ class RolePermissionRepository
return [];
}
$rows = DB::select(
'select p.`key` as `key` from role_permissions rp join permissions p on p.id = rp.permission_id ' .
'select p.`key` as `key` from role_permissions rp ' .
'join roles r on r.id = rp.role_id and r.active = 1 ' .
'join permissions p on p.id = rp.permission_id and p.active = 1 ' .
'where rp.role_id in (???)',
array_map('strval', $ids)
);
@@ -106,8 +108,8 @@ class RolePermissionRepository
}
$rows = DB::select(
'select rp.permission_id as permission_id, p.`key` as `key`, p.description as `description`, r.description as role ' .
'from role_permissions rp join permissions p on p.id = rp.permission_id ' .
'join roles r on r.id = rp.role_id where rp.role_id in (???) ' .
'from role_permissions rp join permissions p on p.id = rp.permission_id and p.active = 1 ' .
'join roles r on r.id = rp.role_id and r.active = 1 where rp.role_id in (???) ' .
'order by p.`key` asc, r.description asc',
array_map('strval', $ids)
);

View File

@@ -1,8 +1,9 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Access;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class RoleRepository
{
@@ -32,7 +33,15 @@ class RoleRepository
public static function list(): array
{
$rows = DB::select(
'select id, uuid, description, created_by, modified_by, created, modified from roles order by id desc'
'select id, uuid, description, code, active, created_by, modified_by, created, modified from roles order by id desc'
);
return self::unwrapList($rows);
}
public static function listActive(): array
{
$rows = DB::select(
'select id, uuid, description, code, active, created_by, modified_by, created, modified from roles where active = 1 order by description asc'
);
return self::unwrapList($rows);
}
@@ -53,45 +62,43 @@ class RoleRepository
return array_values(array_unique($ids));
}
public static function listActiveIds(): array
{
$rows = DB::select('select id from roles where active = 1');
if (!is_array($rows)) {
return [];
}
$ids = [];
foreach ($rows as $row) {
$data = $row['roles'] ?? $row;
if (is_array($data) && isset($data['id'])) {
$ids[] = (int) $data['id'];
}
}
return array_values(array_unique($ids));
}
public static function listPaged(array $options): array
{
$limit = (int) ($options['limit'] ?? 10);
if ($limit < 1) {
$limit = 10;
} elseif ($limit > 100) {
$limit = 100;
}
$offset = (int) ($options['offset'] ?? 0);
if ($offset < 0) {
$offset = 0;
}
$search = trim((string) ($options['search'] ?? ''));
$order = (string) ($options['order'] ?? 'id');
$dir = strtolower((string) ($options['dir'] ?? 'desc'));
$allowedOrder = ['id', 'uuid', 'description', 'created', 'modified'];
if (!in_array($order, $allowedOrder, true)) {
$order = 'id';
}
if (!in_array($dir, ['asc', 'desc'], true)) {
$dir = 'desc';
}
$allowedOrder = ['id', 'uuid', 'description', 'code', 'active', 'created', 'modified'];
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($options);
[$order, $dir] = RepoQuery::sanitizeOrder($options, $allowedOrder);
$where = [];
$params = [];
if ($search !== '') {
$like = '%' . $search . '%';
$where[] = '(description like ? or uuid like ?)';
$params[] = $like;
$params[] = $like;
}
RepoQuery::addLikeFilter($where, $params, ['description', 'uuid', 'code'], $search);
$activeValue = array_key_exists('active', $options) ? $options['active'] : null;
RepoQuery::addEnumFilter($where, $params, $activeValue, [
['aliases' => ['1', 'true', 'active'], 'sql' => 'roles.active = 1'],
['aliases' => ['0', 'false', 'inactive'], 'sql' => 'roles.active = 0'],
]);
$whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
$count = DB::selectValue('select count(*) from roles' . $whereSql, ...$params);
$total = $count ? (int) $count : 0;
$query = 'select id, uuid, description, created_by, modified_by, created, modified from roles' .
$query = 'select id, uuid, description, code, active, created_by, modified_by, created, modified from roles' .
$whereSql .
sprintf(' order by `%s` %s limit ? offset ?', $order, $dir);
@@ -107,7 +114,7 @@ class RoleRepository
public static function find(int $id): ?array
{
$row = DB::selectOne(
'select id, uuid, description, created_by, modified_by, created, modified from roles where id = ? limit 1',
'select id, uuid, description, code, active, created_by, modified_by, created, modified from roles where id = ? limit 1',
(string) $id
);
return self::unwrap($row);
@@ -116,12 +123,26 @@ class RoleRepository
public static function findByUuid(string $uuid): ?array
{
$row = DB::selectOne(
'select id, uuid, description, created_by, modified_by, created, modified from roles where uuid = ? limit 1',
'select id, uuid, description, code, active, created_by, modified_by, created, modified from roles where uuid = ? limit 1',
$uuid
);
return self::unwrap($row);
}
public static function existsByCode(string $code, int $excludeId = 0): bool
{
$code = trim($code);
if ($code === '') {
return false;
}
if ($excludeId > 0) {
$count = DB::selectValue('select count(*) from roles where code = ? and id != ?', $code, (string) $excludeId);
} else {
$count = DB::selectValue('select count(*) from roles where code = ?', $code);
}
return $count ? ((int) $count > 0) : false;
}
private static function uuidV4(): string
{
$data = random_bytes(16);
@@ -133,9 +154,11 @@ class RoleRepository
public static function create(array $data)
{
return DB::insert(
'insert into roles (uuid, description, created_by, created) values (?,?,?,NOW())',
'insert into roles (uuid, description, code, active, created_by, created) values (?,?,?,?,?,NOW())',
$data['uuid'] ?? self::uuidV4(),
$data['description'],
$data['code'] ?? null,
$data['active'] ?? 1,
$data['created_by'] ?? null
);
}
@@ -144,6 +167,8 @@ class RoleRepository
{
$fields = [
'description' => $data['description'],
'code' => $data['code'] ?? null,
'active' => $data['active'] ?? 1,
];
if (array_key_exists('modified_by', $data)) {
$fields['modified_by'] = $data['modified_by'];

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Access;
use MintyPHP\DB;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Auth;
use MintyPHP\DB;

View File

@@ -1,11 +1,26 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Auth;
use MintyPHP\DB;
class PasswordResetRepository
{
private static function unwrapList($rows): array
{
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['password_resets'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public static function create(int $userId, string $codeHash, string $expiresAt): ?int
{
$id = DB::insert(
@@ -68,4 +83,22 @@ class PasswordResetRepository
);
return $result !== false;
}
public static function listByUserId(int $userId, int $limit = 20): array
{
if ($userId <= 0) {
return [];
}
if ($limit < 1) {
$limit = 20;
} elseif ($limit > 100) {
$limit = 100;
}
$rows = DB::select(
'select id, user_id, expires_at, attempts, used_at, created from password_resets where user_id = ? order by id desc limit ?',
(string) $userId,
(string) $limit
);
return self::unwrapList($rows);
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace MintyPHP\Repository\Auth;
use MintyPHP\DB;
class RememberTokenRepository
{
private static function unwrapList($rows): array
{
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['user_remember_tokens'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public static function create(int $userId, string $selector, string $tokenHash, string $expiresAt): ?int
{
$id = DB::insert(
'insert into user_remember_tokens (user_id, selector, token_hash, expires_at, created) values (?,?,?,?,NOW())',
(string) $userId,
$selector,
$tokenHash,
$expiresAt
);
return $id ? (int) $id : null;
}
public static function findBySelector(string $selector): ?array
{
$row = DB::selectOne(
'select id, user_id, selector, token_hash, expires_at, expired_by_admin_at, last_used from user_remember_tokens where selector = ? limit 1',
$selector
);
if (!$row || !isset($row['user_remember_tokens'])) {
return null;
}
return $row['user_remember_tokens'];
}
public static function updateToken(int $id, string $tokenHash, string $expiresAt): bool
{
$result = DB::update(
'update user_remember_tokens set token_hash = ?, expires_at = ?, expired_by_admin_at = NULL, last_used = NOW() where id = ?',
$tokenHash,
$expiresAt,
(string) $id
);
return $result !== false;
}
public static function expireAllByAdmin(): int
{
$result = DB::update(
'update user_remember_tokens set expires_at = NOW(), expired_by_admin_at = NOW() where expires_at > NOW()'
);
return $result !== false ? (int) $result : 0;
}
public static function deleteById(int $id): bool
{
$result = DB::delete('delete from user_remember_tokens where id = ?', (string) $id);
return $result !== false;
}
public static function deleteByUserId(int $userId): bool
{
$result = DB::delete('delete from user_remember_tokens where user_id = ?', (string) $userId);
return $result !== false;
}
public static function listByUserId(int $userId, int $limit = 20): array
{
if ($userId <= 0) {
return [];
}
if ($limit < 1) {
$limit = 20;
} elseif ($limit > 100) {
$limit = 100;
}
$rows = DB::select(
'select id, user_id, selector, expires_at, expired_by_admin_at, last_used, created from user_remember_tokens where user_id = ? order by id desc limit ?',
(string) $userId,
(string) $limit
);
return self::unwrapList($rows);
}
public static function countActive(): int
{
$count = DB::selectValue('select count(*) from user_remember_tokens where expires_at > NOW()');
return $count ? (int) $count : 0;
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Content;
use MintyPHP\DB;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Content;
use MintyPHP\DB;

View File

@@ -1,8 +1,9 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Mail;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class MailLogRepository
{
@@ -65,46 +66,19 @@ class MailLogRepository
public static function listPaged(array $options): array
{
$limit = (int) ($options['limit'] ?? 10);
if ($limit < 1) {
$limit = 10;
} elseif ($limit > 100) {
$limit = 100;
}
$offset = (int) ($options['offset'] ?? 0);
if ($offset < 0) {
$offset = 0;
}
$search = trim((string) ($options['search'] ?? ''));
$order = (string) ($options['order'] ?? 'created_at');
$dir = strtolower((string) ($options['dir'] ?? 'desc'));
$status = trim((string) ($options['status'] ?? ''));
$createdFrom = trim((string) ($options['created_from'] ?? ''));
$createdTo = trim((string) ($options['created_to'] ?? ''));
$allowedOrder = ['id', 'created_at', 'sent_at', 'to_email', 'subject', 'status'];
if (!in_array($order, $allowedOrder, true)) {
$order = 'created_at';
}
if (!in_array($dir, ['asc', 'desc'], true)) {
$dir = 'desc';
}
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($options);
[$order, $dir] = RepoQuery::sanitizeOrder($options, $allowedOrder, 'created_at', 'desc');
$where = [];
$params = [];
if ($search !== '') {
$like = '%' . $search . '%';
$where[] = '(to_email like ? or subject like ? or template like ?)';
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
if ($status !== '') {
$where[] = 'status = ?';
$params[] = $status;
}
RepoQuery::addLikeFilter($where, $params, ['to_email', 'subject', 'template'], $search);
RepoQuery::addEqualsFilter($where, $params, $status, 'status = ?');
if ($createdFrom !== '') {
$where[] = 'created_at >= ?';
$params[] = $createdFrom . ' 00:00:00';

View File

@@ -1,8 +1,9 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Org;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class DepartmentRepository
{
@@ -32,7 +33,7 @@ class DepartmentRepository
public static function list(): array
{
$rows = DB::select(
'select id, uuid, description, created_by, modified_by, created, modified from departments order by id desc'
'select id, uuid, description, code, cost_center, active, created_by, modified_by, created, modified from departments order by id desc'
);
return self::unwrapList($rows);
}
@@ -62,10 +63,10 @@ class DepartmentRepository
}
$placeholders = implode(',', array_fill(0, count($tenantIds), '?'));
$rows = DB::select(
'select departments.id, departments.uuid, departments.description, departments.created_by, departments.modified_by, departments.created, departments.modified ' .
'select departments.id, departments.uuid, departments.description, departments.code, departments.cost_center, departments.active, departments.created_by, departments.modified_by, departments.created, departments.modified ' .
'from departments departments join tenant_departments td on td.department_id = departments.id ' .
'where td.tenant_id in (' . $placeholders . ') ' .
'group by departments.id, departments.uuid, departments.description, departments.created_by, departments.modified_by, departments.created, departments.modified ' .
'where departments.active = 1 and td.tenant_id in (' . $placeholders . ') ' .
'group by departments.id, departments.uuid, departments.description, departments.code, departments.cost_center, departments.active, departments.created_by, departments.modified_by, departments.created, departments.modified ' .
'order by departments.description asc',
...array_map('strval', $tenantIds)
);
@@ -81,7 +82,7 @@ class DepartmentRepository
}
$placeholders = implode(',', array_fill(0, count($departmentIds), '?'));
$rows = DB::select(
'select id, uuid, description, created_by, modified_by, created, modified from departments where id in (' . $placeholders . ') order by description asc',
'select id, uuid, description, code, cost_center, active, created_by, modified_by, created, modified from departments where active = 1 and id in (' . $placeholders . ') order by description asc',
...array_map('strval', $departmentIds)
);
return self::unwrapList($rows);
@@ -89,42 +90,31 @@ class DepartmentRepository
public static function listPaged(array $options): array
{
$limit = (int) ($options['limit'] ?? 10);
if ($limit < 1) {
$limit = 10;
} elseif ($limit > 100) {
$limit = 100;
}
$offset = (int) ($options['offset'] ?? 0);
if ($offset < 0) {
$offset = 0;
}
$search = trim((string) ($options['search'] ?? ''));
$tenant = trim((string) ($options['tenant'] ?? ''));
$order = (string) ($options['order'] ?? 'id');
$dir = strtolower((string) ($options['dir'] ?? 'desc'));
$allowedOrder = ['id', 'uuid', 'description', 'created', 'modified'];
if (!in_array($order, $allowedOrder, true)) {
$order = 'id';
}
if (!in_array($dir, ['asc', 'desc'], true)) {
$dir = 'desc';
}
$allowedOrder = ['id', 'uuid', 'description', 'code', 'cost_center', 'active', 'created', 'modified'];
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($options);
[$order, $dir] = RepoQuery::sanitizeOrder($options, $allowedOrder);
$where = [];
$params = [];
if ($search !== '') {
$like = '%' . $search . '%';
$where[] = '(description like ? or uuid like ?)';
$params[] = $like;
$params[] = $like;
}
if ($tenant !== '') {
$where[] = "exists (select 1 from tenant_departments td join tenants t on t.id = td.tenant_id and t.status = 'active' where td.department_id = departments.id and t.uuid = ?)";
$params[] = $tenant;
}
RepoQuery::addLikeFilter(
$where,
$params,
['description', 'uuid', 'code', 'cost_center'],
$search
);
$activeValue = array_key_exists('active', $options) ? $options['active'] : null;
RepoQuery::addEnumFilter($where, $params, $activeValue, [
['aliases' => ['1', 'true', 'active'], 'sql' => 'departments.active = 1'],
['aliases' => ['0', 'false', 'inactive'], 'sql' => 'departments.active = 0'],
]);
RepoQuery::addEqualsFilter(
$where,
$params,
$tenant,
"exists (select 1 from tenant_departments td join tenants t on t.id = td.tenant_id and t.status = 'active' where td.department_id = departments.id and t.uuid = ?)"
);
if (!empty($options['tenantUserId'])) {
$tenantUserId = (int) $options['tenantUserId'];
if ($tenantUserId > 0) {
@@ -168,7 +158,7 @@ class DepartmentRepository
$count = DB::selectValue('select count(*) from departments' . $whereSql, ...$params);
$total = $count ? (int) $count : 0;
$query = 'select id, uuid, description, created_by, modified_by, created, modified from departments' .
$query = 'select id, uuid, description, code, cost_center, active, created_by, modified_by, created, modified from departments' .
$whereSql .
sprintf(' order by `%s` %s limit ? offset ?', $order, $dir);
@@ -244,7 +234,7 @@ class DepartmentRepository
public static function find(int $id): ?array
{
$row = DB::selectOne(
'select id, uuid, description, created_by, modified_by, created, modified from departments where id = ? limit 1',
'select id, uuid, description, code, cost_center, active, created_by, modified_by, created, modified from departments where id = ? limit 1',
(string) $id
);
return self::unwrap($row);
@@ -253,12 +243,26 @@ class DepartmentRepository
public static function findByUuid(string $uuid): ?array
{
$row = DB::selectOne(
'select id, uuid, description, created_by, modified_by, created, modified from departments where uuid = ? limit 1',
'select id, uuid, description, code, cost_center, active, created_by, modified_by, created, modified from departments where uuid = ? limit 1',
$uuid
);
return self::unwrap($row);
}
public static function existsByCode(string $code, int $excludeId = 0): bool
{
$code = trim($code);
if ($code === '') {
return false;
}
if ($excludeId > 0) {
$count = DB::selectValue('select count(*) from departments where code = ? and id != ?', $code, (string) $excludeId);
} else {
$count = DB::selectValue('select count(*) from departments where code = ?', $code);
}
return (int) $count > 0;
}
private static function uuidV4(): string
{
$data = random_bytes(16);
@@ -270,9 +274,12 @@ class DepartmentRepository
public static function create(array $data)
{
return DB::insert(
'insert into departments (uuid, description, created_by, created) values (?,?,?,NOW())',
'insert into departments (uuid, description, code, cost_center, active, created_by, created) values (?,?,?,?,?,?,NOW())',
$data['uuid'] ?? self::uuidV4(),
$data['description'],
$data['code'] ?? null,
$data['cost_center'] ?? null,
$data['active'] ?? 1,
$data['created_by'] ?? null
);
}
@@ -281,6 +288,9 @@ class DepartmentRepository
{
$fields = [
'description' => $data['description'],
'code' => $data['code'] ?? null,
'cost_center' => $data['cost_center'] ?? null,
'active' => $data['active'] ?? 1,
];
if (array_key_exists('modified_by', $data)) {
$fields['modified_by'] = $data['modified_by'];

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Org;
use MintyPHP\DB;

View File

@@ -1,104 +0,0 @@
<?php
namespace MintyPHP\Repository;
use MintyPHP\DB;
class PermissionRepository
{
public static function list(): array
{
$rows = DB::select('select id, `key`, description, created from permissions order by `key` asc');
if (!is_array($rows)) {
return [];
}
$list = [];
foreach ($rows as $row) {
$data = $row['permissions'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return $list;
}
public static function listPaged(array $options): array
{
$limit = (int) ($options['limit'] ?? 10);
$offset = (int) ($options['offset'] ?? 0);
$search = trim((string) ($options['search'] ?? ''));
$order = (string) ($options['order'] ?? 'key');
$dir = strtolower((string) ($options['dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc';
$allowedOrder = ['key' => '`key`', 'description' => 'description', 'created' => 'created'];
$orderBy = $allowedOrder[$order] ?? $allowedOrder['key'];
$where = [];
$params = [];
if ($search !== '') {
$where[] = '(permissions.`key` like ? or permissions.description like ?)';
$params[] = '%' . $search . '%';
$params[] = '%' . $search . '%';
}
$whereSql = $where ? ' where ' . implode(' and ', $where) : '';
$total = DB::selectValue('select count(*) from permissions' . $whereSql, ...$params);
$query = 'select id, `key`, description, created from permissions' . $whereSql .
' order by ' . $orderBy . ' ' . $dir . ' limit ? offset ?';
$queryParams = array_merge($params, [(string) $limit, (string) $offset]);
$rows = DB::select($query, ...$queryParams);
$list = [];
foreach ($rows as $row) {
$data = $row['permissions'] ?? $row;
if (is_array($data)) {
$list[] = $data;
}
}
return ['data' => $list, 'total' => (int) $total];
}
public static function find(int $id): ?array
{
$row = DB::selectOne('select id, `key`, description, created from permissions where id = ? limit 1', (string) $id);
$data = $row['permissions'] ?? $row;
return is_array($data) ? $data : null;
}
public static function findByKey(string $key): ?array
{
$row = DB::selectOne('select id, `key`, description, created from permissions where `key` = ? limit 1', $key);
$data = $row['permissions'] ?? $row;
return is_array($data) ? $data : null;
}
public static function create(array $data): ?int
{
$key = trim((string) ($data['key'] ?? ''));
$desc = trim((string) ($data['description'] ?? ''));
$result = DB::insert(
'insert into permissions (`key`, description, created) values (?,?,NOW())',
$key,
$desc !== '' ? $desc : null
);
return $result ? (int) $result : null;
}
public static function update(int $id, array $data): bool
{
$key = trim((string) ($data['key'] ?? ''));
$desc = trim((string) ($data['description'] ?? ''));
$result = DB::update(
'update permissions set `key` = ?, description = ? where id = ?',
$key,
$desc !== '' ? $desc : null,
(string) $id
);
return $result !== false;
}
public static function delete(int $id): bool
{
$result = DB::delete('delete from permissions where id = ?', (string) $id);
return (bool) $result;
}
}

View File

@@ -1,55 +0,0 @@
<?php
namespace MintyPHP\Repository;
use MintyPHP\DB;
class RememberTokenRepository
{
public static function create(int $userId, string $selector, string $tokenHash, string $expiresAt): ?int
{
$id = DB::insert(
'insert into user_remember_tokens (user_id, selector, token_hash, expires_at, created) values (?,?,?,?,NOW())',
(string) $userId,
$selector,
$tokenHash,
$expiresAt
);
return $id ? (int) $id : null;
}
public static function findBySelector(string $selector): ?array
{
$row = DB::selectOne(
'select id, user_id, selector, token_hash, expires_at, last_used from user_remember_tokens where selector = ? limit 1',
$selector
);
if (!$row || !isset($row['user_remember_tokens'])) {
return null;
}
return $row['user_remember_tokens'];
}
public static function updateToken(int $id, string $tokenHash, string $expiresAt): bool
{
$result = DB::update(
'update user_remember_tokens set token_hash = ?, expires_at = ?, last_used = NOW() where id = ?',
$tokenHash,
$expiresAt,
(string) $id
);
return $result !== false;
}
public static function deleteById(int $id): bool
{
$result = DB::delete('delete from user_remember_tokens where id = ?', (string) $id);
return $result !== false;
}
public static function deleteByUserId(int $userId): bool
{
$result = DB::delete('delete from user_remember_tokens where user_id = ?', (string) $userId);
return $result !== false;
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Settings;
use MintyPHP\DB;

View File

@@ -0,0 +1,120 @@
<?php
namespace MintyPHP\Repository\Support;
class RepoQuery
{
private static function normalizeLikeValue(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
$value = str_replace('\\', '\\\\', $value);
$value = str_replace('_', '\\_', $value);
$value = str_replace('*', '%', $value);
if (!str_starts_with($value, '%')) {
$value = '%' . $value;
}
if (!str_ends_with($value, '%')) {
$value = $value . '%';
}
return $value;
}
public static function sanitizeLimitOffset(
array $options,
int $defaultLimit = 10,
int $minLimit = 1,
int $maxLimit = 100,
int $defaultOffset = 0
): array {
$limit = (int) ($options['limit'] ?? $defaultLimit);
if ($limit < $minLimit) {
$limit = $defaultLimit;
} elseif ($limit > $maxLimit) {
$limit = $maxLimit;
}
$offset = (int) ($options['offset'] ?? $defaultOffset);
if ($offset < 0) {
$offset = 0;
}
return [$limit, $offset];
}
public static function sanitizeOrder(
array $options,
array $allowedOrder,
string $defaultOrder = 'id',
string $defaultDir = 'desc'
): array {
$order = (string) ($options['order'] ?? $defaultOrder);
$dir = strtolower((string) ($options['dir'] ?? $defaultDir));
if (!in_array($order, $allowedOrder, true)) {
$order = $defaultOrder;
}
if (!in_array($dir, ['asc', 'desc'], true)) {
$dir = $defaultDir;
}
return [$order, $dir];
}
public static function addLikeFilter(array &$where, array &$params, array $fields, string $value): void
{
$like = self::normalizeLikeValue($value);
if ($like === '') {
return;
}
$clauses = [];
foreach ($fields as $field) {
$clauses[] = $field . " like ? escape '\\\\'";
$params[] = $like;
}
if ($clauses) {
$where[] = '(' . implode(' or ', $clauses) . ')';
}
}
public static function addEnumFilter(array &$where, array &$params, $value, array $map): void
{
if ($value === null) {
return;
}
$normalized = strtolower(trim((string) $value));
if ($normalized === '' || $normalized === 'all') {
return;
}
foreach ($map as $entry) {
$aliases = $entry['aliases'] ?? [];
$aliases = array_map('strtolower', $aliases);
if (!in_array($normalized, $aliases, true)) {
continue;
}
$sql = (string) ($entry['sql'] ?? '');
if ($sql === '') {
return;
}
$where[] = $sql;
$paramsToAdd = $entry['params'] ?? [];
foreach ($paramsToAdd as $param) {
$params[] = $param;
}
return;
}
}
public static function addEqualsFilter(array &$where, array &$params, $value, string $sql, bool $allowEmpty = false): void
{
// Use for simple "= ?" filters; skip when empty to keep WHERE clean.
if ($value === null) {
return;
}
$normalized = trim((string) $value);
if ($normalized === '' && !$allowEmpty) {
return;
}
$where[] = $sql;
$params[] = $normalized;
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Tenant;
use MintyPHP\DB;
@@ -47,6 +47,39 @@ class TenantDepartmentRepository
return array_values(array_unique($ids));
}
public static function listDepartmentMapByTenantIds(array $tenantIds): array
{
$tenantIds = array_values(array_unique(array_map('intval', $tenantIds)));
$tenantIds = array_filter($tenantIds, static fn ($id) => $id > 0);
if (!$tenantIds) {
return [];
}
$placeholders = implode(',', array_fill(0, count($tenantIds), '?'));
$rows = DB::select(
'select tenant_id, department_id from tenant_departments where tenant_id in (' . $placeholders . ')',
...array_map('strval', $tenantIds)
);
if (!is_array($rows)) {
return [];
}
$map = [];
foreach ($rows as $row) {
$data = $row['tenant_departments'] ?? $row;
if (is_array($data) && isset($data['tenant_id'], $data['department_id'])) {
$tenantId = (int) $data['tenant_id'];
$departmentId = (int) $data['department_id'];
if ($tenantId > 0 && $departmentId > 0) {
$map[$tenantId] ??= [];
$map[$tenantId][] = $departmentId;
}
}
}
foreach ($map as $tenantId => $items) {
$map[$tenantId] = array_values(array_unique($items));
}
return $map;
}
public static function replaceForDepartment(int $departmentId, array $tenantIds): bool
{
DB::delete('delete from tenant_departments where department_id = ?', (string) $departmentId);

View File

@@ -1,8 +1,9 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Tenant;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class TenantRepository
{
@@ -83,37 +84,14 @@ class TenantRepository
public static function listPaged(array $options): array
{
$limit = (int) ($options['limit'] ?? 10);
if ($limit < 1) {
$limit = 10;
} elseif ($limit > 100) {
$limit = 100;
}
$offset = (int) ($options['offset'] ?? 0);
if ($offset < 0) {
$offset = 0;
}
$search = trim((string) ($options['search'] ?? ''));
$order = (string) ($options['order'] ?? 'id');
$dir = strtolower((string) ($options['dir'] ?? 'desc'));
$allowedOrder = ['id', 'uuid', 'description', 'created', 'modified'];
if (!in_array($order, $allowedOrder, true)) {
$order = 'id';
}
if (!in_array($dir, ['asc', 'desc'], true)) {
$dir = 'desc';
}
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($options);
[$order, $dir] = RepoQuery::sanitizeOrder($options, $allowedOrder);
$where = [];
$params = [];
if ($search !== '') {
$like = '%' . $search . '%';
$where[] = '(description like ? or uuid like ?)';
$params[] = $like;
$params[] = $like;
}
RepoQuery::addLikeFilter($where, $params, ['description', 'uuid'], $search);
if (!empty($options['tenantUserId'])) {
$tenantUserId = (int) $options['tenantUserId'];
if ($tenantUserId > 0) {

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\Tenant;
use MintyPHP\DB;

View File

@@ -1,11 +1,247 @@
<?php
namespace MintyPHP\Repository;
namespace MintyPHP\Repository\User;
use MintyPHP\DB;
use MintyPHP\Repository\Support\RepoQuery;
class UserRepository
{
private static function buildUserFilters(array $options): array
{
$search = trim((string) ($options['search'] ?? ''));
$active = $options['active'] ?? null;
$createdFrom = trim((string) ($options['created_from'] ?? ''));
$createdTo = trim((string) ($options['created_to'] ?? ''));
$tenant = trim((string) ($options['tenant'] ?? ''));
$tenantUuids = array_filter(array_map('trim', explode(',', (string) ($options['tenants'] ?? ''))));
$roleIds = self::normalizeIdList($options['roles'] ?? []);
$departmentIds = self::normalizeIdList($options['departments'] ?? []);
$emailVerified = $options['email_verified'] ?? null;
$loginStatus = $options['login_status'] ?? null;
$where = [];
$params = [];
RepoQuery::addLikeFilter(
$where,
$params,
['users.first_name', 'users.last_name', 'users.email'],
$search
);
RepoQuery::addEnumFilter($where, $params, $active, [
['aliases' => ['1', 'true', 'active'], 'sql' => 'users.active = ?', 'params' => ['1']],
['aliases' => ['0', 'false', 'inactive'], 'sql' => 'users.active = ?', 'params' => ['0']],
]);
if ($createdFrom !== '') {
$where[] = 'users.created >= ?';
$params[] = $createdFrom . ' 00:00:00';
}
if ($createdTo !== '') {
$where[] = 'users.created <= ?';
$params[] = $createdTo . ' 23:59:59';
}
if ($tenantUuids) {
$where[] = 'exists (select 1 from user_tenants ut2 join tenants t2 on t2.id = ut2.tenant_id where ut2.user_id = users.id and t2.uuid in (???))';
$params[] = array_values($tenantUuids);
} else {
RepoQuery::addEqualsFilter(
$where,
$params,
$tenant,
'exists (select 1 from user_tenants ut2 join tenants t2 on t2.id = ut2.tenant_id where ut2.user_id = users.id and t2.uuid = ?)'
);
}
if ($roleIds) {
$where[] = 'exists (select 1 from user_roles ur2 where ur2.user_id = users.id and ur2.role_id in (???))';
$params[] = array_map('strval', $roleIds);
}
if ($departmentIds) {
$where[] = 'exists (select 1 from user_departments ud2 where ud2.user_id = users.id and ud2.department_id in (???))';
$params[] = array_map('strval', $departmentIds);
}
RepoQuery::addEnumFilter($where, $params, $emailVerified, [
['aliases' => ['1', 'true', 'verified', 'yes'], 'sql' => 'users.email_verified_at is not null'],
['aliases' => ['0', 'false', 'unverified', 'no'], 'sql' => 'users.email_verified_at is null'],
]);
RepoQuery::addEnumFilter($where, $params, $loginStatus, [
['aliases' => ['never', 'none', 'no'], 'sql' => 'users.last_login_at is null'],
['aliases' => ['ever', 'logged', 'yes'], 'sql' => 'users.last_login_at is not null'],
]);
if (!empty($options['tenantUserId'])) {
$tenantUserId = (int) $options['tenantUserId'];
if ($tenantUserId > 0) {
if (defined('TENANT_SCOPE_STRICT') && TENANT_SCOPE_STRICT) {
$where[] = 'exists (select 1 from user_tenants ut join user_tenants ut2 on ut2.tenant_id = ut.tenant_id ' .
"join tenants t on t.id = ut.tenant_id and t.status = 'active' " .
'where ut2.user_id = ? and ut.user_id = users.id)';
$params[] = (string) $tenantUserId;
} else {
$where[] = '(not exists (select 1 from user_tenants ut where ut.user_id = users.id) ' .
'or exists (select 1 from user_tenants ut join user_tenants ut2 on ut2.tenant_id = ut.tenant_id ' .
"join tenants t on t.id = ut.tenant_id and t.status = 'active' " .
'where ut2.user_id = ? and ut.user_id = users.id))';
$params[] = (string) $tenantUserId;
}
}
}
$whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
return [$whereSql, $params];
}
private static function buildListQuery(string $whereSql, string $order, string $dir): string
{
return 'select users.id, users.uuid, users.first_name, users.last_name, users.display_name, users.email, users.profile_description, users.job_title, users.phone, users.mobile, users.short_dial, users.address, users.postal_code, users.city, users.country, users.region, users.hire_date, users.theme, users.primary_tenant_id, users.created_by, users.modified_by, users.created, users.modified, users.last_login_at, users.active, ' .
'pt.description as primary_tenant_label ' .
"from users left join tenants pt on pt.id = users.primary_tenant_id and pt.status = 'active'" .
$whereSql .
sprintf(' order by `users`.`%s` %s limit ? offset ?', $order, $dir);
}
private static function extractIds(array $list): array
{
$ids = [];
foreach ($list as $user) {
if (isset($user['id'])) {
$ids[] = (int) $user['id'];
}
}
return $ids;
}
private static function collectLabels(array $rows, string $userKey, string $labelKey): array
{
$labelsByUser = [];
foreach ($rows as $row) {
$userId = 0;
$label = '';
if (isset($row[$userKey]) && is_array($row[$userKey])) {
$userId = (int) ($row[$userKey]['user_id'] ?? 0);
}
if (isset($row[$labelKey]) && is_array($row[$labelKey])) {
$label = (string) ($row[$labelKey]['description'] ?? '');
}
if ($userId === 0 || $label === '') {
foreach ($row as $value) {
if (!is_array($value)) {
continue;
}
if ($userId === 0 && isset($value['user_id'])) {
$userId = (int) $value['user_id'];
}
if ($label === '' && isset($value['description'])) {
$label = (string) $value['description'];
}
}
}
if ($userId === 0 && isset($row['user_id'])) {
$userId = (int) $row['user_id'];
}
if ($label === '' && isset($row['description'])) {
$label = (string) $row['description'];
}
if ($userId > 0 && $label !== '') {
$labelsByUser[$userId] ??= [];
$labelsByUser[$userId][] = $label;
}
}
return $labelsByUser;
}
private static function hydrateUserLabels(array $list, array $options): array
{
$ids = self::extractIds($list);
if (!$ids) {
return $list;
}
$scopeToActiveTenants = !empty($options['tenantUserId']);
$tenantLabelJoin = $scopeToActiveTenants
? "join tenants t on t.id = ut.tenant_id and t.status = 'active' "
: 'join tenants t on t.id = ut.tenant_id ';
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$labelRows = DB::select(
'select ut.user_id as user_id, t.id as tenant_id, t.description as description from user_tenants ut ' . $tenantLabelJoin .
'where ut.user_id in (' . $placeholders . ') order by t.description asc',
...array_map('strval', $ids)
);
$roleLabelRows = DB::select(
'select ur.user_id as user_id, r.description as description from user_roles ur join roles r on r.id = ur.role_id and r.active = 1 ' .
'where ur.user_id in (' . $placeholders . ') order by r.description asc',
...array_map('strval', $ids)
);
$departmentLabelRows = DB::select(
'select ud.user_id as user_id, d.description as description from user_departments ud join departments d on d.id = ud.department_id and d.active = 1 ' .
'where ud.user_id in (' . $placeholders . ') order by d.description asc',
...array_map('strval', $ids)
);
$tenantMapByUser = [];
foreach ($labelRows as $row) {
$userId = 0;
$tenantId = 0;
$label = '';
if (isset($row['ut']) && is_array($row['ut'])) {
$userId = (int) ($row['ut']['user_id'] ?? 0);
}
if (isset($row['t']) && is_array($row['t'])) {
$tenantId = (int) ($row['t']['tenant_id'] ?? 0);
$label = (string) ($row['t']['description'] ?? '');
}
if ($userId === 0 || $tenantId === 0 || $label === '') {
foreach ($row as $value) {
if (!is_array($value)) {
continue;
}
if ($userId === 0 && isset($value['user_id'])) {
$userId = (int) $value['user_id'];
}
if ($tenantId === 0 && isset($value['tenant_id'])) {
$tenantId = (int) $value['tenant_id'];
}
if ($label === '' && isset($value['description'])) {
$label = (string) $value['description'];
}
}
}
if ($userId === 0 && isset($row['user_id'])) {
$userId = (int) $row['user_id'];
}
if ($tenantId === 0 && isset($row['tenant_id'])) {
$tenantId = (int) $row['tenant_id'];
}
if ($label === '' && isset($row['description'])) {
$label = (string) $row['description'];
}
if ($userId > 0 && $tenantId > 0 && $label !== '') {
$tenantMapByUser[$userId] ??= [];
$tenantMapByUser[$userId][$tenantId] = $label;
}
}
$tenantLabelsByUser = [];
foreach ($tenantMapByUser as $userId => $map) {
$tenantLabelsByUser[$userId] = array_values($map);
}
$roleLabelsByUser = self::collectLabels($roleLabelRows, 'ur', 'r');
$departmentLabelsByUser = self::collectLabels($departmentLabelRows, 'ud', 'd');
foreach ($list as &$user) {
$userId = (int) ($user['id'] ?? 0);
$primaryId = (int) ($user['primary_tenant_id'] ?? 0);
$user['tenant_labels'] = $tenantLabelsByUser[$userId] ?? [];
if ($primaryId > 0 && isset($tenantMapByUser[$userId][$primaryId])) {
$user['primary_tenant_label'] = $tenantMapByUser[$userId][$primaryId];
}
$user['role_labels'] = $roleLabelsByUser[$userId] ?? [];
$user['department_labels'] = $departmentLabelsByUser[$userId] ?? [];
}
unset($user);
return $list;
}
private static function unwrap(?array $row): ?array
{
if (!$row) {
@@ -44,262 +280,35 @@ class UserRepository
public static function list(): array
{
$rows = DB::select(
'select id, uuid, first_name, last_name, email, profile_description, job_title, phone, mobile, short_dial, theme, primary_tenant_id, created_by, modified_by, created, modified, active from users order by id desc'
'select id, uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, theme, primary_tenant_id, created_by, modified_by, created, modified, active from users order by id desc'
);
return self::unwrapList($rows);
}
public static function updateLastLogin(int $userId): void
{
if ($userId <= 0) {
return;
}
DB::query('update users set last_login_at = UTC_TIMESTAMP() where id = ?', (string) $userId);
}
public static function listPaged(array $options): array
{
$limit = (int) ($options['limit'] ?? 10);
if ($limit < 1) {
$limit = 10;
} elseif ($limit > 100) {
$limit = 100;
}
$allowedOrder = ['id', 'uuid', 'first_name', 'last_name', 'email', 'created', 'modified', 'active', 'last_login_at'];
[$limit, $offset] = RepoQuery::sanitizeLimitOffset($options);
[$order, $dir] = RepoQuery::sanitizeOrder($options, $allowedOrder);
[$whereSql, $params] = self::buildUserFilters($options);
$offset = (int) ($options['offset'] ?? 0);
if ($offset < 0) {
$offset = 0;
}
$search = trim((string) ($options['search'] ?? ''));
$active = $options['active'] ?? null;
$createdFrom = trim((string) ($options['created_from'] ?? ''));
$createdTo = trim((string) ($options['created_to'] ?? ''));
$tenant = trim((string) ($options['tenant'] ?? ''));
$tenantUuids = array_filter(array_map('trim', explode(',', (string) ($options['tenants'] ?? ''))));
$roleIds = self::normalizeIdList($options['roles'] ?? []);
$departmentIds = self::normalizeIdList($options['departments'] ?? []);
$emailVerified = $options['email_verified'] ?? null;
$order = (string) ($options['order'] ?? 'id');
$dir = strtolower((string) ($options['dir'] ?? 'desc'));
$allowedOrder = ['id', 'uuid', 'first_name', 'last_name', 'email', 'created', 'modified', 'active'];
if (!in_array($order, $allowedOrder, true)) {
$order = 'id';
}
if (!in_array($dir, ['asc', 'desc'], true)) {
$dir = 'desc';
}
$where = [];
$params = [];
if ($search !== '') {
$like = '%' . $search . '%';
$where[] = '(users.first_name like ? or users.last_name like ? or users.email like ?)';
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
$activeFilter = null;
if ($active !== null && $active !== '' && $active !== 'all') {
$activeValue = strtolower((string) $active);
if (in_array($activeValue, ['1', 'true', 'active'], true)) {
$activeFilter = 1;
} elseif (in_array($activeValue, ['0', 'false', 'inactive'], true)) {
$activeFilter = 0;
}
}
if ($activeFilter !== null) {
$where[] = 'users.active = ?';
$params[] = (string) $activeFilter;
}
if ($createdFrom !== '') {
$where[] = 'users.created >= ?';
$params[] = $createdFrom . ' 00:00:00';
}
if ($createdTo !== '') {
$where[] = 'users.created <= ?';
$params[] = $createdTo . ' 23:59:59';
}
if ($tenantUuids) {
$where[] = 'exists (select 1 from user_tenants ut2 join tenants t2 on t2.id = ut2.tenant_id where ut2.user_id = users.id and t2.uuid in (???))';
$params[] = array_values($tenantUuids);
} elseif ($tenant !== '') {
$where[] = 'exists (select 1 from user_tenants ut2 join tenants t2 on t2.id = ut2.tenant_id where ut2.user_id = users.id and t2.uuid = ?)';
$params[] = $tenant;
}
if ($roleIds) {
$where[] = 'exists (select 1 from user_roles ur2 where ur2.user_id = users.id and ur2.role_id in (???))';
$params[] = array_map('strval', $roleIds);
}
if ($departmentIds) {
$where[] = 'exists (select 1 from user_departments ud2 where ud2.user_id = users.id and ud2.department_id in (???))';
$params[] = array_map('strval', $departmentIds);
}
if ($emailVerified !== null && $emailVerified !== '' && $emailVerified !== 'all') {
$emailVerifiedValue = strtolower((string) $emailVerified);
if (in_array($emailVerifiedValue, ['1', 'true', 'verified', 'yes'], true)) {
$where[] = 'users.email_verified_at is not null';
} elseif (in_array($emailVerifiedValue, ['0', 'false', 'unverified', 'no'], true)) {
$where[] = 'users.email_verified_at is null';
}
}
if (!empty($options['tenantUserId'])) {
$tenantUserId = (int) $options['tenantUserId'];
if ($tenantUserId > 0) {
if (defined('TENANT_SCOPE_STRICT') && TENANT_SCOPE_STRICT) {
$where[] = 'exists (select 1 from user_tenants ut join user_tenants ut2 on ut2.tenant_id = ut.tenant_id ' .
"join tenants t on t.id = ut.tenant_id and t.status = 'active' " .
'where ut2.user_id = ? and ut.user_id = users.id)';
$params[] = (string) $tenantUserId;
} else {
$where[] = '(not exists (select 1 from user_tenants ut where ut.user_id = users.id) ' .
'or exists (select 1 from user_tenants ut join user_tenants ut2 on ut2.tenant_id = ut.tenant_id ' .
"join tenants t on t.id = ut.tenant_id and t.status = 'active' " .
'where ut2.user_id = ? and ut.user_id = users.id))';
$params[] = (string) $tenantUserId;
}
}
}
$whereSql = $where ? (' where ' . implode(' and ', $where)) : '';
$count = DB::selectValue('select count(*) from users' . $whereSql, ...$params);
$total = $count ? (int) $count : 0;
$query = 'select users.id, users.uuid, users.first_name, users.last_name, users.email, users.profile_description, users.job_title, users.phone, users.mobile, users.short_dial, users.address, users.postal_code, users.city, users.country, users.region, users.hire_date, users.theme, users.primary_tenant_id, users.created_by, users.modified_by, users.created, users.modified, users.active, ' .
'pt.description as primary_tenant_label ' .
"from users left join tenants pt on pt.id = users.primary_tenant_id and pt.status = 'active'" .
$whereSql .
sprintf(' order by `users`.`%s` %s limit ? offset ?', $order, $dir);
$query = self::buildListQuery($whereSql, $order, $dir);
$queryParams = array_merge($params, [(string) $limit, (string) $offset]);
$rows = call_user_func_array(['MintyPHP\\DB', 'select'], array_merge([$query], $queryParams));
$list = self::unwrapList($rows);
$labelRows = [];
$tenantMapByUser = [];
$roleLabelRows = [];
$departmentLabelRows = [];
$ids = [];
foreach ($list as $user) {
if (isset($user['id'])) {
$ids[] = (int) $user['id'];
}
}
if ($ids) {
$scopeToActiveTenants = !empty($options['tenantUserId']);
$tenantLabelJoin = $scopeToActiveTenants
? "join tenants t on t.id = ut.tenant_id and t.status = 'active' "
: 'join tenants t on t.id = ut.tenant_id ';
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$labelRows = DB::select(
'select ut.user_id as user_id, t.id as tenant_id, t.description as description from user_tenants ut ' . $tenantLabelJoin .
'where ut.user_id in (' . $placeholders . ') order by t.description asc',
...array_map('strval', $ids)
);
$roleLabelRows = DB::select(
'select ur.user_id as user_id, r.description as description from user_roles ur join roles r on r.id = ur.role_id ' .
'where ur.user_id in (' . $placeholders . ') order by r.description asc',
...array_map('strval', $ids)
);
$departmentLabelRows = DB::select(
'select ud.user_id as user_id, d.description as description from user_departments ud join departments d on d.id = ud.department_id ' .
'where ud.user_id in (' . $placeholders . ') order by d.description asc',
...array_map('strval', $ids)
);
$collectLabels = static function (array $rows, string $userKey, string $labelKey): array {
$labelsByUser = [];
foreach ($rows as $row) {
$userId = 0;
$label = '';
if (isset($row[$userKey]) && is_array($row[$userKey])) {
$userId = (int) ($row[$userKey]['user_id'] ?? 0);
}
if (isset($row[$labelKey]) && is_array($row[$labelKey])) {
$label = (string) ($row[$labelKey]['description'] ?? '');
}
if ($userId === 0 || $label === '') {
foreach ($row as $value) {
if (!is_array($value)) {
continue;
}
if ($userId === 0 && isset($value['user_id'])) {
$userId = (int) $value['user_id'];
}
if ($label === '' && isset($value['description'])) {
$label = (string) $value['description'];
}
}
}
if ($userId === 0 && isset($row['user_id'])) {
$userId = (int) $row['user_id'];
}
if ($label === '' && isset($row['description'])) {
$label = (string) $row['description'];
}
if ($userId > 0 && $label !== '') {
$labelsByUser[$userId] ??= [];
$labelsByUser[$userId][] = $label;
}
}
return $labelsByUser;
};
foreach ($labelRows as $row) {
$userId = 0;
$tenantId = 0;
$label = '';
if (isset($row['ut']) && is_array($row['ut'])) {
$userId = (int) ($row['ut']['user_id'] ?? 0);
}
if (isset($row['t']) && is_array($row['t'])) {
$tenantId = (int) ($row['t']['tenant_id'] ?? 0);
$label = (string) ($row['t']['description'] ?? '');
}
if ($userId === 0 || $tenantId === 0 || $label === '') {
foreach ($row as $value) {
if (!is_array($value)) {
continue;
}
if ($userId === 0 && isset($value['user_id'])) {
$userId = (int) $value['user_id'];
}
if ($tenantId === 0 && isset($value['tenant_id'])) {
$tenantId = (int) $value['tenant_id'];
}
if ($label === '' && isset($value['description'])) {
$label = (string) $value['description'];
}
}
}
if ($userId === 0 && isset($row['user_id'])) {
$userId = (int) $row['user_id'];
}
if ($tenantId === 0 && isset($row['tenant_id'])) {
$tenantId = (int) $row['tenant_id'];
}
if ($label === '' && isset($row['description'])) {
$label = (string) $row['description'];
}
if ($userId > 0 && $tenantId > 0 && $label !== '') {
$tenantMapByUser[$userId] ??= [];
$tenantMapByUser[$userId][$tenantId] = $label;
}
}
$tenantLabelsByUser = [];
foreach ($tenantMapByUser as $userId => $map) {
$tenantLabelsByUser[$userId] = array_values($map);
}
$roleLabelsByUser = $collectLabels($roleLabelRows, 'ur', 'r');
$departmentLabelsByUser = $collectLabels($departmentLabelRows, 'ud', 'd');
foreach ($list as &$user) {
$userId = (int) ($user['id'] ?? 0);
$primaryId = (int) ($user['primary_tenant_id'] ?? 0);
$user['tenant_labels'] = $tenantLabelsByUser[$userId] ?? [];
if ($primaryId > 0 && isset($tenantMapByUser[$userId][$primaryId])) {
$user['primary_tenant_label'] = $tenantMapByUser[$userId][$primaryId];
}
$user['role_labels'] = $roleLabelsByUser[$userId] ?? [];
$user['department_labels'] = $departmentLabelsByUser[$userId] ?? [];
}
unset($user);
}
$list = self::hydrateUserLabels($list, $options);
$result = [
'total' => $total,
@@ -311,7 +320,7 @@ class UserRepository
public static function find(int $id): ?array
{
$row = DB::selectOne(
'select id, uuid, first_name, last_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, password, locale, totp_secret, theme, primary_tenant_id, created_by, modified_by, created, modified, active from users where id = ? limit 1',
'select id, uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, password, locale, totp_secret, theme, primary_tenant_id, created_by, modified_by, created, modified, active from users where id = ? limit 1',
(string) $id
);
return self::unwrap($row);
@@ -320,7 +329,7 @@ class UserRepository
public static function findByUuid(string $uuid): ?array
{
$row = DB::selectOne(
'select id, uuid, first_name, last_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, password, locale, totp_secret, theme, primary_tenant_id, created_by, modified_by, created, modified, active, active_changed_at, active_changed_by from users where uuid = ? limit 1',
'select id, uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, password, locale, totp_secret, theme, primary_tenant_id, created_by, modified_by, created, modified, active, active_changed_at, active_changed_by from users where uuid = ? limit 1',
$uuid
);
return self::unwrap($row);
@@ -329,12 +338,23 @@ class UserRepository
public static function findByEmail(string $email): ?array
{
$row = DB::selectOne(
'select id, uuid, first_name, last_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, email_verified_at, password, locale, totp_secret, theme, primary_tenant_id, created_by, modified_by, created, modified, active, active_changed_at, active_changed_by from users where email = ? limit 1',
'select id, uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, email_verified_at, email_verified_at, password, locale, totp_secret, theme, primary_tenant_id, created_by, modified_by, created, modified, active, active_changed_at, active_changed_by from users where email = ? limit 1',
$email
);
return self::unwrap($row);
}
private static function buildDisplayName(array $data): string
{
$first = trim((string) ($data['first_name'] ?? ''));
$last = trim((string) ($data['last_name'] ?? ''));
$name = trim($first . ' ' . $last);
if ($name === '') {
$name = trim((string) ($data['email'] ?? ''));
}
return $name;
}
private static function uuidV4(): string
{
$data = random_bytes(16);
@@ -347,10 +367,11 @@ class UserRepository
{
$hash = password_hash($data['password'], PASSWORD_DEFAULT);
return DB::insert(
'insert into users (uuid, first_name, last_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, password, locale, totp_secret, theme, primary_tenant_id, current_tenant_id, created_by, created, active, active_changed_at, active_changed_by) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),?,?,?)',
'insert into users (uuid, first_name, last_name, display_name, email, profile_description, job_title, phone, mobile, short_dial, address, postal_code, city, country, region, hire_date, password, locale, totp_secret, theme, primary_tenant_id, current_tenant_id, created_by, created, active, active_changed_at, active_changed_by) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(),?,?,?)',
$data['uuid'] ?? self::uuidV4(),
$data['first_name'],
$data['last_name'],
self::buildDisplayName($data),
$data['email'],
$data['profile_description'] ?? null,
$data['job_title'] ?? null,
@@ -381,6 +402,7 @@ class UserRepository
$fields = [
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'display_name' => self::buildDisplayName($data),
'email' => $data['email'],
'profile_description' => $data['profile_description'] ?? null,
'job_title' => $data['job_title'] ?? null,

View File

@@ -1,16 +1,18 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Access;
use MintyPHP\Repository\RolePermissionRepository;
use MintyPHP\Repository\UserRoleRepository;
use MintyPHP\Repository\PermissionRepository;
use MintyPHP\Repository\Access\RolePermissionRepository;
use MintyPHP\Repository\Access\UserRoleRepository;
use MintyPHP\Repository\Access\PermissionRepository;
class PermissionService
{
public const USERS_CREATE = 'users.create';
public const USERS_DELETE = 'users.delete';
public const USERS_VIEW = 'users.view';
public const USERS_VIEW_META = 'users.view_meta';
public const USERS_VIEW_AUDIT = 'users.view_audit';
public const USERS_UPDATE = 'users.update';
public const USERS_SELF_UPDATE = 'users.self_update';
public const USERS_UPDATE_ASSIGNMENTS = 'users.update_assignments';
@@ -96,6 +98,11 @@ class PermissionService
return PermissionRepository::list();
}
public static function listActive(): array
{
return PermissionRepository::listActive();
}
public static function listPaged(array $options): array
{
return PermissionRepository::listPaged($options);
@@ -152,6 +159,9 @@ class PermissionService
if (!$permission) {
return ['ok' => false, 'status' => 404, 'error' => 'not_found'];
}
if ((int) ($permission['is_system'] ?? 0) === 1) {
return ['ok' => false, 'status' => 403, 'error' => 'system_permission_protected'];
}
$deleted = PermissionRepository::delete($id);
if (!$deleted) {
return ['ok' => false, 'status' => 500, 'error' => 'delete_failed'];
@@ -164,6 +174,8 @@ class PermissionService
return [
'key' => trim((string) ($input['key'] ?? '')),
'description' => trim((string) ($input['description'] ?? '')),
'active' => self::normalizeActive($input['active'] ?? 1),
'is_system' => self::normalizeFlag($input['is_system'] ?? 0),
];
}
@@ -177,4 +189,25 @@ class PermissionService
}
return $errors;
}
private static function normalizeActive($value): int
{
$value = strtolower(trim((string) $value));
if (in_array($value, ['0', 'false', 'inactive'], true)) {
return 0;
}
return 1;
}
private static function normalizeFlag($value): int
{
if (is_bool($value)) {
return $value ? 1 : 0;
}
$value = strtolower(trim((string) $value));
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
return 1;
}
return 0;
}
}

View File

@@ -1,9 +1,9 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Access;
use MintyPHP\Repository\RoleRepository;
use MintyPHP\Service\SettingService;
use MintyPHP\Repository\Access\RoleRepository;
use MintyPHP\Service\Settings\SettingService;
class RoleService
{
@@ -12,6 +12,11 @@ class RoleService
return RoleRepository::list();
}
public static function listActive(): array
{
return RoleRepository::listActive();
}
public static function listPaged(array $options): array
{
return RoleRepository::listPaged($options);
@@ -30,7 +35,7 @@ class RoleService
public static function createFromAdmin(array $input, int $currentUserId = 0): array
{
$form = self::sanitizeBase($input);
$errors = self::validateBase($form);
$errors = self::validateBase($form, 0);
if ($errors) {
return ['ok' => false, 'errors' => $errors, 'form' => $form];
@@ -38,6 +43,8 @@ class RoleService
$createdId = RoleRepository::create([
'description' => $form['description'],
'code' => $form['code'] ?: null,
'active' => $form['active'],
'created_by' => $currentUserId > 0 ? $currentUserId : null,
]);
@@ -56,7 +63,7 @@ class RoleService
public static function updateFromAdmin(int $roleId, array $input, int $currentUserId = 0): array
{
$form = self::sanitizeBase($input);
$errors = self::validateBase($form);
$errors = self::validateBase($form, $roleId);
if ($errors) {
return ['ok' => false, 'errors' => $errors, 'form' => $form];
@@ -64,6 +71,8 @@ class RoleService
$updated = RoleRepository::update($roleId, [
'description' => $form['description'],
'code' => $form['code'] ?: null,
'active' => $form['active'],
'modified_by' => $currentUserId > 0 ? $currentUserId : null,
]);
@@ -102,15 +111,30 @@ class RoleService
{
return [
'description' => trim((string) ($input['description'] ?? '')),
'code' => trim((string) ($input['code'] ?? '')),
'active' => self::normalizeActive($input['active'] ?? 1),
];
}
private static function validateBase(array $form): array
private static function validateBase(array $form, int $excludeId): array
{
$errors = [];
if ($form['description'] === '') {
$errors[] = t('Description cannot be empty');
}
$code = $form['code'] ?? '';
if ($code !== '' && RoleRepository::existsByCode($code, $excludeId)) {
$errors[] = t('Role code already exists');
}
return $errors;
}
private static function normalizeActive($value): int
{
$value = strtolower(trim((string) $value));
if (in_array($value, ['0', 'false', 'inactive'], true)) {
return 0;
}
return 1;
}
}

View File

@@ -1,15 +1,15 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Auth;
use MintyPHP\Auth;
use MintyPHP\Router;
use MintyPHP\Support\Flash;
use MintyPHP\Service\UserService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\RememberMeService;
use MintyPHP\Service\EmailVerificationService;
use MintyPHP\Repository\UserRepository;
use MintyPHP\Service\User\UserService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Auth\RememberMeService;
use MintyPHP\Service\Auth\EmailVerificationService;
use MintyPHP\Repository\User\UserRepository;
class AuthService
{
@@ -60,6 +60,7 @@ class AuthService
'flash_key' => 'login_no_active_tenant',
];
}
UserRepository::updateLastLogin($userId);
}
return ['ok' => true];

View File

@@ -1,9 +1,9 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Auth;
use MintyPHP\Repository\EmailVerificationRepository;
use MintyPHP\Repository\UserRepository;
use MintyPHP\Repository\Auth\EmailVerificationRepository;
use MintyPHP\Repository\User\UserRepository;
use MintyPHP\I18n;
use MintyPHP\Http\Request;

View File

@@ -1,12 +1,12 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Auth;
use MintyPHP\Repository\PasswordResetRepository;
use MintyPHP\Repository\UserRepository;
use MintyPHP\Repository\Auth\PasswordResetRepository;
use MintyPHP\Repository\User\UserRepository;
use MintyPHP\I18n;
use MintyPHP\Http\Request;
use MintyPHP\Service\RememberMeService;
use MintyPHP\Service\Auth\RememberMeService;
class PasswordResetService
{

View File

@@ -1,13 +1,13 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Auth;
use MintyPHP\Session;
use MintyPHP\Repository\RememberTokenRepository;
use MintyPHP\Repository\UserRepository;
use MintyPHP\Repository\Auth\RememberTokenRepository;
use MintyPHP\Repository\User\UserRepository;
use MintyPHP\I18n;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\AuthService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Auth\AuthService;
class RememberMeService
{
@@ -83,6 +83,9 @@ class RememberMeService
if ($userId > 0) {
PermissionService::getUserPermissions($userId, true);
AuthService::loadTenantDataIntoSession($userId);
if (empty($_SESSION['no_active_tenant'])) {
UserRepository::updateLastLogin($userId);
}
}
$newToken = bin2hex(random_bytes(32));
@@ -115,6 +118,11 @@ class RememberMeService
RememberTokenRepository::deleteByUserId($userId);
}
public static function expireAllTokensByAdmin(): int
{
return RememberTokenRepository::expireAllByAdmin();
}
private static function setCookie(string $selector, string $token): void
{
$value = $selector . ':' . $token;

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Branding;
class BrandingFaviconService
{
@@ -19,7 +19,7 @@ class BrandingFaviconService
if (defined('APP_STORAGE_PATH') && APP_STORAGE_PATH) {
return rtrim(APP_STORAGE_PATH, '/');
}
return rtrim(dirname(__DIR__, 2) . '/storage', '/');
return rtrim(dirname(__DIR__, 3) . '/storage', '/');
}
public static function storageDir(): string
@@ -29,7 +29,7 @@ class BrandingFaviconService
public static function publicDir(): string
{
return rtrim(dirname(__DIR__, 2) . '/web/favicon', '/');
return rtrim(dirname(__DIR__, 3) . '/web/favicon', '/');
}
public static function hasFavicon(): bool
@@ -195,8 +195,8 @@ class BrandingFaviconService
}
$title = null;
if (class_exists('MintyPHP\\Service\\SettingService')) {
$title = \MintyPHP\Service\SettingService::getAppTitle();
if (class_exists('MintyPHP\\Service\\Settings\\SettingService')) {
$title = \MintyPHP\Service\Settings\SettingService::getAppTitle();
}
if ($title === null || $title === '') {
$title = defined('APP_NAME') ? APP_NAME : 'IMVS';

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Branding;
class BrandingLogoService
{
@@ -13,7 +13,7 @@ class BrandingLogoService
if (defined('APP_STORAGE_PATH') && APP_STORAGE_PATH) {
return rtrim(APP_STORAGE_PATH, '/');
}
return rtrim(dirname(__DIR__, 2) . '/storage', '/');
return rtrim(dirname(__DIR__, 3) . '/storage', '/');
}
public static function brandingDir(): string

View File

@@ -1,9 +1,9 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Content;
use MintyPHP\Repository\PageRepository;
use MintyPHP\Repository\PageContentRepository;
use MintyPHP\Repository\Content\PageRepository;
use MintyPHP\Repository\Content\PageContentRepository;
use MintyPHP\I18n;
class PageService

View File

@@ -1,8 +1,8 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Mail;
use MintyPHP\Repository\MailLogRepository;
use MintyPHP\Repository\Mail\MailLogRepository;
class MailLogService
{

View File

@@ -1,10 +1,10 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Mail;
use MintyPHP\Repository\MailLogRepository;
use MintyPHP\Repository\Mail\MailLogRepository;
use MintyPHP\I18n;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\Settings\SettingService;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception as MailerException;
@@ -69,7 +69,7 @@ class MailService
private static function renderTemplate(string $template, array $vars, string $locale): array
{
$base = dirname(__DIR__, 2) . '/templates/emails';
$base = dirname(__DIR__, 3) . '/templates/emails';
$htmlPath = $base . '/' . $locale . '/' . $template . '.html';
$textPath = $base . '/' . $locale . '/' . $template . '.txt';
$htmlHeaderPath = $base . '/' . $locale . '/partials/header.html';

View File

@@ -1,12 +1,12 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Org;
use MintyPHP\Repository\DepartmentRepository;
use MintyPHP\Repository\TenantDepartmentRepository;
use MintyPHP\Repository\UserDepartmentRepository;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Repository\Org\DepartmentRepository;
use MintyPHP\Repository\Tenant\TenantDepartmentRepository;
use MintyPHP\Repository\Org\UserDepartmentRepository;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Tenant\TenantScopeService;
class DepartmentService
{
@@ -71,13 +71,17 @@ class DepartmentService
{
$form = self::sanitizeBase($input);
$errors = self::validateBase($form);
$warnings = self::warningsForCode($form, 0);
if ($errors) {
return ['ok' => false, 'errors' => $errors, 'form' => $form];
return ['ok' => false, 'errors' => $errors, 'warnings' => $warnings, 'form' => $form];
}
$createdId = DepartmentRepository::create([
'description' => $form['description'],
'code' => $form['code'] ?: null,
'cost_center' => $form['cost_center'] ?: null,
'active' => $form['active'],
'created_by' => $currentUserId > 0 ? $currentUserId : null,
]);
@@ -90,20 +94,24 @@ class DepartmentService
if (!empty($input['is_default']) && $createdId) {
SettingService::setDefaultDepartmentId((int) $createdId);
}
return ['ok' => true, 'form' => $form, 'uuid' => $uuid, 'id' => (int) $createdId];
return ['ok' => true, 'form' => $form, 'warnings' => $warnings, 'uuid' => $uuid, 'id' => (int) $createdId];
}
public static function updateFromAdmin(int $departmentId, array $input, int $currentUserId = 0): array
{
$form = self::sanitizeBase($input);
$errors = self::validateBase($form);
$warnings = self::warningsForCode($form, $departmentId);
if ($errors) {
return ['ok' => false, 'errors' => $errors, 'form' => $form];
return ['ok' => false, 'errors' => $errors, 'warnings' => $warnings, 'form' => $form];
}
$updated = DepartmentRepository::update($departmentId, [
'description' => $form['description'],
'code' => $form['code'] ?: null,
'cost_center' => $form['cost_center'] ?: null,
'active' => $form['active'],
'modified_by' => $currentUserId > 0 ? $currentUserId : null,
]);
@@ -111,7 +119,7 @@ class DepartmentService
return ['ok' => false, 'errors' => [t('Department can not be updated')], 'form' => $form];
}
return ['ok' => true, 'form' => $form];
return ['ok' => true, 'form' => $form, 'warnings' => $warnings];
}
public static function syncTenants(int $departmentId, array $tenantIds): int
@@ -147,6 +155,9 @@ class DepartmentService
{
return [
'description' => trim((string) ($input['description'] ?? '')),
'code' => trim((string) ($input['code'] ?? '')),
'cost_center' => trim((string) ($input['cost_center'] ?? '')),
'active' => self::normalizeActive($input['active'] ?? 1),
];
}
@@ -158,4 +169,23 @@ class DepartmentService
}
return $errors;
}
private static function warningsForCode(array $form, int $excludeId): array
{
$warnings = [];
$code = $form['code'] ?? '';
if ($code !== '' && DepartmentRepository::existsByCode($code, $excludeId)) {
$warnings[] = t('Department code already exists');
}
return $warnings;
}
private static function normalizeActive($value): int
{
$value = strtolower(trim((string) $value));
if (in_array($value, ['0', 'false', 'inactive'], true)) {
return 0;
}
return 1;
}
}

View File

@@ -1,11 +1,11 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Settings;
use MintyPHP\Repository\SettingRepository;
use MintyPHP\Repository\TenantRepository;
use MintyPHP\Repository\RoleRepository;
use MintyPHP\Repository\DepartmentRepository;
use MintyPHP\Repository\Settings\SettingRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Repository\Access\RoleRepository;
use MintyPHP\Repository\Org\DepartmentRepository;
class SettingService
{
@@ -16,6 +16,7 @@ class SettingService
public const APP_LOCALE_KEY = 'app_locale';
public const APP_THEME_KEY = 'app_theme';
public const APP_THEME_USER_KEY = 'app_theme_user';
public const APP_REGISTRATION_KEY = 'app_registration';
public const APP_PRIMARY_COLOR_KEY = 'app_primary_color';
public const SMTP_HOST_KEY = 'smtp_host';
public const SMTP_PORT_KEY = 'smtp_port';
@@ -149,7 +150,7 @@ class SettingService
{
$value = SettingRepository::getValue(self::APP_THEME_KEY);
$value = $value !== null ? strtolower(trim((string) $value)) : '';
if (!in_array($value, ['light', 'dark'], true)) {
if (!in_array($value, self::allowedThemes(), true)) {
return null;
}
return $value;
@@ -158,7 +159,7 @@ class SettingService
public static function setAppTheme(?string $theme, ?string $description = null): bool
{
$value = $theme !== null ? strtolower(trim((string) $theme)) : '';
if ($value === '' || !in_array($value, ['light', 'dark'], true)) {
if ($value === '' || !in_array($value, self::allowedThemes(), true)) {
$value = null;
}
$desc = $description ?? 'setting.app_theme';
@@ -181,6 +182,43 @@ class SettingService
return SettingRepository::set(self::APP_THEME_USER_KEY, $value, $desc);
}
private static function allowedThemes(): array
{
$file = dirname(__DIR__, 3) . '/config/themes.php';
if (!is_file($file)) {
return ['light', 'dark'];
}
$themes = include $file;
if (!is_array($themes)) {
return ['light', 'dark'];
}
$keys = [];
foreach ($themes as $key => $label) {
$key = strtolower(trim((string) $key));
$label = trim((string) $label);
if ($key !== '' && $label !== '') {
$keys[] = $key;
}
}
return $keys ?: ['light', 'dark'];
}
public static function isRegistrationEnabled(): bool
{
$value = SettingRepository::getValue(self::APP_REGISTRATION_KEY);
if ($value === null || $value === '') {
return true;
}
return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
}
public static function setRegistrationEnabled(bool $allowed, ?string $description = null): bool
{
$value = $allowed ? '1' : '0';
$desc = $description ?? 'setting.app_registration';
return SettingRepository::set(self::APP_REGISTRATION_KEY, $value, $desc);
}
public static function getAppPrimaryColor(): ?string
{
$value = SettingRepository::getValue(self::APP_PRIMARY_COLOR_KEY);

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Tenant;
class TenantAvatarService
{
@@ -18,7 +18,7 @@ class TenantAvatarService
if (defined('APP_STORAGE_PATH') && APP_STORAGE_PATH) {
return rtrim(APP_STORAGE_PATH, '/');
}
return rtrim(dirname(__DIR__, 2) . '/storage', '/');
return rtrim(dirname(__DIR__, 3) . '/storage', '/');
}
public static function tenantDir(string $uuid): string

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Tenant;
class TenantFaviconService
{
@@ -24,7 +24,7 @@ class TenantFaviconService
if (defined('APP_STORAGE_PATH') && APP_STORAGE_PATH) {
return rtrim(APP_STORAGE_PATH, '/');
}
return rtrim(dirname(__DIR__, 2) . '/storage', '/');
return rtrim(dirname(__DIR__, 3) . '/storage', '/');
}
public static function storageDir(string $uuid): string
@@ -34,7 +34,7 @@ class TenantFaviconService
public static function publicDir(string $uuid): string
{
return rtrim(dirname(__DIR__, 2) . '/web/favicon/tenants/' . $uuid, '/');
return rtrim(dirname(__DIR__, 3) . '/web/favicon/tenants/' . $uuid . '/favicon', '/');
}
public static function hasFavicon(string $uuid): bool

View File

@@ -1,11 +1,11 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Tenant;
use MintyPHP\Repository\TenantDepartmentRepository;
use MintyPHP\Repository\UserTenantRepository;
use MintyPHP\Repository\TenantRepository;
use MintyPHP\Service\PermissionService;
use MintyPHP\Repository\Tenant\TenantDepartmentRepository;
use MintyPHP\Repository\Tenant\UserTenantRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Access\PermissionService;
class TenantScopeService
{

View File

@@ -1,9 +1,9 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\Tenant;
use MintyPHP\Repository\TenantRepository;
use MintyPHP\Service\SettingService;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Service\Settings\SettingService;
class TenantService
{

View File

@@ -1,6 +1,6 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\User;
class UserAvatarService
{
@@ -18,7 +18,7 @@ class UserAvatarService
if (defined('APP_STORAGE_PATH') && APP_STORAGE_PATH) {
return rtrim(APP_STORAGE_PATH, '/');
}
return rtrim(dirname(__DIR__, 2) . '/storage', '/');
return rtrim(dirname(__DIR__, 3) . '/storage', '/');
}
public static function userDir(string $uuid): string

View File

@@ -1,18 +1,18 @@
<?php
namespace MintyPHP\Service;
namespace MintyPHP\Service\User;
use MintyPHP\I18n;
use MintyPHP\Repository\DepartmentRepository;
use MintyPHP\Repository\RoleRepository;
use MintyPHP\Repository\TenantRepository;
use MintyPHP\Repository\UserRepository;
use MintyPHP\Repository\UserDepartmentRepository;
use MintyPHP\Repository\UserRoleRepository;
use MintyPHP\Repository\UserTenantRepository;
use MintyPHP\Repository\TenantDepartmentRepository;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Repository\Org\DepartmentRepository;
use MintyPHP\Repository\Access\RoleRepository;
use MintyPHP\Repository\Tenant\TenantRepository;
use MintyPHP\Repository\User\UserRepository;
use MintyPHP\Repository\Org\UserDepartmentRepository;
use MintyPHP\Repository\Access\UserRoleRepository;
use MintyPHP\Repository\Tenant\UserTenantRepository;
use MintyPHP\Repository\Tenant\TenantDepartmentRepository;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantScopeService;
class UserService
{
@@ -447,7 +447,7 @@ class UserService
{
$ids = array_values(array_unique(array_map('intval', $roleIds)));
$ids = array_filter($ids, static fn ($id) => $id > 0);
$validIds = RoleRepository::listIds();
$validIds = RoleRepository::listActiveIds();
if ($validIds) {
$validMap = array_fill_keys($validIds, true);
$ids = array_values(array_filter($ids, static fn ($id) => isset($validMap[$id])));
@@ -520,8 +520,9 @@ class UserService
private static function normalizeTheme($value): string
{
$theme = strtolower(trim((string) $value));
if (!in_array($theme, ['dark', 'light'], true)) {
return 'light';
$themes = function_exists('appThemes') ? appThemes() : ['light' => 'Light', 'dark' => 'Dark'];
if ($theme === '' || !isset($themes[$theme])) {
return function_exists('appDefaultTheme') ? appDefaultTheme() : 'light';
}
return $theme;
}

View File

@@ -3,9 +3,9 @@
namespace MintyPHP\Support;
use MintyPHP\Router;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantService;
use MintyPHP\Service\AuthService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\Auth\AuthService;
use MintyPHP\Http\Request;
class Guard

View File

@@ -2,8 +2,8 @@
namespace MintyPHP\Support;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\UserAvatarService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\User\UserAvatarService;
class SearchConfig
{
@@ -17,97 +17,125 @@ class SearchConfig
];
}
private static function normalizeLikeQuery(string $query): string
{
$query = trim($query);
if ($query === '') {
return '';
}
$query = str_replace('\\', '\\\\', $query);
$query = str_replace('_', '\\_', $query);
$query = str_replace('*', '%', $query);
if (!str_starts_with($query, '%')) {
$query = '%' . $query;
}
if (!str_ends_with($query, '%')) {
$query = $query . '%';
}
return $query;
}
public static function normalizeScoreQuery(string $query): string
{
$query = trim($query);
if ($query == '') {
return '';
}
return str_replace(['*', '%', '_'], '', $query);
}
public static function resources(string $query, string $locale): array
{
$like = '%' . $query . '%';
$like = self::normalizeLikeQuery($query);
return [
[
'key' => 'address-book',
'label' => t('Address book'),
'permission' => PermissionService::ADDRESS_BOOK_VIEW,
'countSql' => 'select count(*) from users where active = 1 and (first_name like ? or last_name like ? or email like ?) {{tenantFilter}}',
'countSql' => "select count(*) from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}}",
'countParams' => [$like, $like, $like],
'previewSql' => 'select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? or last_name like ? or email like ?) {{tenantFilter}} order by last_name, first_name limit ?',
'previewSql' => "select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name limit ?",
'previewParams' => [$like, $like, $like],
'resultSql' => 'select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? or last_name like ? or email like ?) {{tenantFilter}} order by last_name, first_name',
'resultSql' => "select uuid, first_name, last_name, email from users where active = 1 and (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name",
'resultParams' => [$like, $like, $like],
],
[
'key' => 'users',
'label' => t('Users'),
'permission' => PermissionService::USERS_VIEW,
'countSql' => 'select count(*) from users where (first_name like ? or last_name like ? or email like ?) {{tenantFilter}}',
'countSql' => "select count(*) from users where (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}}",
'countParams' => [$like, $like, $like],
'previewSql' => 'select uuid, first_name, last_name, email from users where (first_name like ? or last_name like ? or email like ?) {{tenantFilter}} order by last_name, first_name limit ?',
'previewSql' => "select uuid, first_name, last_name, email from users where (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name limit ?",
'previewParams' => [$like, $like, $like],
'resultSql' => 'select uuid, first_name, last_name, email from users where (first_name like ? or last_name like ? or email like ?) {{tenantFilter}} order by last_name, first_name',
'resultSql' => "select uuid, first_name, last_name, email from users where (first_name like ? escape '\\\\' or last_name like ? escape '\\\\' or email like ? escape '\\\\') {{tenantFilter}} order by last_name, first_name",
'resultParams' => [$like, $like, $like],
],
[
'key' => 'tenants',
'label' => t('Tenants'),
'permission' => PermissionService::TENANTS_VIEW,
'countSql' => 'select count(*) from tenants where description like ? {{tenantFilter}}',
'countSql' => "select count(*) from tenants where description like ? escape '\\\\' {{tenantFilter}}",
'countParams' => [$like],
'previewSql' => 'select uuid, description from tenants where description like ? {{tenantFilter}} order by description limit ?',
'previewSql' => "select uuid, description from tenants where description like ? escape '\\\\' {{tenantFilter}} order by description limit ?",
'previewParams' => [$like],
'resultSql' => 'select uuid, description from tenants where description like ? {{tenantFilter}} order by description',
'resultSql' => "select uuid, description from tenants where description like ? escape '\\\\' {{tenantFilter}} order by description",
'resultParams' => [$like],
],
[
'key' => 'departments',
'label' => t('Departments'),
'permission' => PermissionService::DEPARTMENTS_VIEW,
'countSql' => 'select count(*) from departments where description like ? {{tenantFilter}}',
'countSql' => "select count(*) from departments where description like ? escape '\\\\' {{tenantFilter}}",
'countParams' => [$like],
'previewSql' => 'select uuid, description from departments where description like ? {{tenantFilter}} order by description limit ?',
'previewSql' => "select uuid, description from departments where description like ? escape '\\\\' {{tenantFilter}} order by description limit ?",
'previewParams' => [$like],
'resultSql' => 'select uuid, description from departments where description like ? {{tenantFilter}} order by description',
'resultSql' => "select uuid, description from departments where description like ? escape '\\\\' {{tenantFilter}} order by description",
'resultParams' => [$like],
],
[
'key' => 'roles',
'label' => t('Roles'),
'permission' => PermissionService::ROLES_VIEW,
'countSql' => 'select count(*) from roles where description like ?',
'countSql' => "select count(*) from roles where active = 1 and description like ? escape '\\\\'",
'countParams' => [$like],
'previewSql' => 'select uuid, description from roles where description like ? order by description limit ?',
'previewSql' => "select uuid, description from roles where active = 1 and description like ? escape '\\\\' order by description limit ?",
'previewParams' => [$like],
'resultSql' => 'select uuid, description from roles where description like ? order by description',
'resultSql' => "select uuid, description from roles where active = 1 and description like ? escape '\\\\' order by description",
'resultParams' => [$like],
],
[
'key' => 'permissions',
'label' => t('Permissions'),
'permission' => PermissionService::PERMISSIONS_VIEW,
'countSql' => 'select count(*) from permissions where `key` like ? or description like ?',
'countSql' => "select count(*) from permissions where active = 1 and (`key` like ? escape '\\\\' or description like ? escape '\\\\')",
'countParams' => [$like, $like],
'previewSql' => 'select id, description, `key` from permissions where `key` like ? or description like ? order by `key` limit ?',
'previewSql' => "select id, description, `key` from permissions where active = 1 and (`key` like ? escape '\\\\' or description like ? escape '\\\\') order by `key` limit ?",
'previewParams' => [$like, $like],
'resultSql' => 'select id, description, `key` from permissions where `key` like ? or description like ? order by `key`',
'resultSql' => "select id, description, `key` from permissions where active = 1 and (`key` like ? escape '\\\\' or description like ? escape '\\\\') order by `key`",
'resultParams' => [$like, $like],
],
[
'key' => 'pages',
'label' => t('Pages'),
'permission' => '',
'countSql' => 'select count(*) from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? or pc.content like ?',
'countSql' => "select count(*) from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? escape '\\\\' or pc.content like ? escape '\\\\'",
'countParams' => [$locale, $like, $like],
'previewSql' => 'select p.slug from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? or pc.content like ? order by p.slug limit ?',
'previewSql' => "select p.slug from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? escape '\\\\' or pc.content like ? escape '\\\\' order by p.slug limit ?",
'previewParams' => [$locale, $like, $like],
'resultSql' => 'select p.slug from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? or pc.content like ? order by p.slug',
'resultSql' => "select p.slug from pages p join page_contents pc on pc.page_id = p.id and pc.locale = ? where p.slug like ? escape '\\\\' or pc.content like ? escape '\\\\' order by p.slug",
'resultParams' => [$locale, $like, $like],
],
[
'key' => 'settings',
'label' => t('Settings'),
'permission' => PermissionService::SETTINGS_VIEW,
'countSql' => 'select count(*) from settings where `key` like ? or value like ?',
'countSql' => "select count(*) from settings where `key` like ? escape '\\\\' or value like ? escape '\\\\'",
'countParams' => [$like, $like],
'previewSql' => null,
'previewParams' => [],
'resultSql' => 'select `key`, value from settings where `key` like ? or value like ? order by `key`',
'resultSql' => "select `key`, value from settings where `key` like ? escape '\\\\' or value like ? escape '\\\\' order by `key`",
'resultParams' => [$like, $like],
],
];

View File

@@ -14,6 +14,8 @@ class Tile
'iconBg' => null,
'iconColor' => null,
'class' => '',
'tooltip' => '',
'tooltipPos' => 'top',
];
$data = array_merge($defaults, $options);

View File

@@ -10,6 +10,23 @@ function asset(string $path): string
return \MintyPHP\Router::getBaseUrl() . ltrim($path, '/');
}
function templatePath(string $path): string
{
return dirname(__DIR__, 3) . '/templates/' . ltrim($path, '/');
}
function assetVersion(string $path): string
{
$path = ltrim($path, '/');
$file = dirname(__DIR__, 3) . '/web/' . $path;
$url = asset($path);
$version = @filemtime($file);
if ($version === false) {
return $url;
}
return $url . '?v=' . $version;
}
function localeBase(): string
{
$locale = \MintyPHP\I18n::$locale ?? \MintyPHP\I18n::$defaultLocale;
@@ -58,6 +75,16 @@ function appUrl(string $path = ''): string
return $base . '/' . $path;
}
function currentUserDisplayName(): string
{
$user = $_SESSION['user'] ?? [];
$name = trim((string) (($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? '')));
if ($name !== '') {
return $name;
}
return trim((string) ($user['email'] ?? ''));
}
function appLogoUrlAbsolute(int $size = 128): string
{
$url = appLogoUrl($size);
@@ -79,6 +106,36 @@ function appSetting(string $key): ?string
return $value !== '' ? $value : null;
}
function appThemes(): array
{
$file = dirname(__DIR__, 3) . '/config/themes.php';
if (!is_file($file)) {
return [
'light' => 'Light',
'dark' => 'Dark',
];
}
$themes = include $file;
if (!is_array($themes)) {
return [
'light' => 'Light',
'dark' => 'Dark',
];
}
$clean = [];
foreach ($themes as $key => $label) {
$key = strtolower(trim((string) $key));
$label = trim((string) $label);
if ($key !== '' && $label !== '') {
$clean[$key] = $label;
}
}
return $clean ?: [
'light' => 'Light',
'dark' => 'Dark',
];
}
function appDefaultLocale(): ?string
{
$locale = appSetting('app_locale');
@@ -94,13 +151,29 @@ function appDefaultLocale(): ?string
function appDefaultTheme(): string
{
$themes = appThemes();
$setting = appSetting('app_theme');
if (in_array($setting, ['light', 'dark'], true)) {
if ($setting !== null && isset($themes[$setting])) {
return $setting;
}
$envTheme = getenv('APP_THEME') ?: 'light';
$envTheme = strtolower(trim((string) $envTheme));
return in_array($envTheme, ['light', 'dark'], true) ? $envTheme : 'light';
return isset($themes[$envTheme]) ? $envTheme : 'light';
}
function currentTheme(): string
{
$themes = appThemes();
$theme = appDefaultTheme();
if (!allowUserTheme()) {
return $theme;
}
$user = $_SESSION['user'] ?? [];
$userTheme = strtolower(trim((string) ($user['theme'] ?? '')));
if ($userTheme !== '' && isset($themes[$userTheme])) {
return $userTheme;
}
return $theme;
}
function allowUserTheme(): bool
@@ -112,6 +185,15 @@ function allowUserTheme(): bool
return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
}
function allowRegistration(): bool
{
$value = appSetting('app_registration');
if ($value === null || $value === '') {
return true;
}
return in_array(strtolower($value), ['1', 'true', 'yes', 'on'], true);
}
function appPrimaryColor(): ?string
{
$tenantColor = $_SESSION['current_tenant']['primary_color'] ?? null;
@@ -147,26 +229,40 @@ function appPrimaryCssVars(): string
$g = hexdec(substr($hex, 2, 2)) / 255;
$b = hexdec(substr($hex, 4, 2)) / 255;
$monoThreshold = 0.000001;
if (abs($r - $g) < $monoThreshold && abs($g - $b) < $monoThreshold) {
$l = round($r * 100, 2) . '%';
return "--app-primary-h-base: 0; --app-primary-s-base: 0%; --app-primary-l-base: {$l}; --app-primary-h-light: 0; --app-primary-s-light: 0%; --app-primary-l-light: {$l};";
}
$max = max($r, $g, $b);
$min = min($r, $g, $b);
$delta = $max - $min;
if ($delta == 0.0) {
$l = round((($max + $min) / 2) * 100, 2) . '%';
return "--app-primary-h-base: 0; --app-primary-s-base: 0%; --app-primary-l-base: {$l}; --app-primary-h-light: 0; --app-primary-s-light: 0%; --app-primary-l-light: {$l};";
}
$h = 0.0;
if ($delta !== 0.0) {
if ($max === $r) {
$h = 60 * fmod((($g - $b) / $delta), 6);
} elseif ($max === $g) {
$h = 60 * ((($b - $r) / $delta) + 2);
} else {
$h = 60 * ((($r - $g) / $delta) + 4);
}
if ($max === $r) {
$h = 60 * fmod((($g - $b) / $delta), 6);
} elseif ($max === $g) {
$h = 60 * ((($b - $r) / $delta) + 2);
} else {
$h = 60 * ((($r - $g) / $delta) + 4);
}
if ($h < 0) {
$h += 360;
}
$l = ($max + $min) / 2;
$s = $delta === 0.0 ? 0.0 : $delta / (1 - abs(2 * $l - 1));
$denominator = 1 - abs(2 * $l - 1);
if ($delta === 0.0 || $denominator == 0.0) {
$s = 0.0;
} else {
$s = $delta / $denominator;
}
$h = round($h, 2);
$s = round($s * 100, 2) . '%';
@@ -177,7 +273,7 @@ function appPrimaryCssVars(): string
function appLogoUrl(?int $size = null): string
{
if (class_exists('MintyPHP\\Service\\BrandingLogoService') && \MintyPHP\Service\BrandingLogoService::hasLogo()) {
if (class_exists('MintyPHP\\Service\\Branding\\BrandingLogoService') && \MintyPHP\Service\Branding\BrandingLogoService::hasLogo()) {
$query = $size ? '?size=' . (int) $size : '';
return lurl('branding/logo' . $query);
}
@@ -187,9 +283,9 @@ function appLogoUrl(?int $size = null): string
function appFaviconUrl(string $file): string
{
$tenantUuid = $_SESSION['current_tenant']['uuid'] ?? '';
if ($tenantUuid !== '' && class_exists('MintyPHP\\Service\\TenantFaviconService')) {
if (\MintyPHP\Service\TenantFaviconService::hasFavicon($tenantUuid)) {
return asset('favicon/tenants/' . $tenantUuid . '/' . ltrim($file, '/'));
if ($tenantUuid !== '' && class_exists('MintyPHP\\Service\\Tenant\\TenantFaviconService')) {
if (\MintyPHP\Service\Tenant\TenantFaviconService::hasFavicon($tenantUuid)) {
return asset('favicon/tenants/' . $tenantUuid . '/favicon/' . ltrim($file, '/'));
}
}
return asset('favicon/' . ltrim($file, '/'));

View File

@@ -13,12 +13,12 @@ function can(string $permissionKey): bool
if ($userId <= 0) {
return false;
}
if (!class_exists('MintyPHP\\Service\\PermissionService')) {
if (!class_exists('MintyPHP\\Service\\Access\\PermissionService')) {
return false;
}
$keys = \MintyPHP\Service\PermissionService::getCachedPermissions($userId);
$keys = \MintyPHP\Service\Access\PermissionService::getCachedPermissions($userId);
if (!$keys) {
$keys = \MintyPHP\Service\PermissionService::getUserPermissions($userId);
$keys = \MintyPHP\Service\Access\PermissionService::getUserPermissions($userId);
if (!$keys) {
return false;
}

View File

@@ -2,9 +2,9 @@
use MintyPHP\Router;
use MintyPHP\Support\Guard;
use MintyPHP\Service\UserService;
use MintyPHP\Service\UserAvatarService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\User\UserService;
use MintyPHP\Service\User\UserAvatarService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::ADDRESS_BOOK_VIEW);

View File

@@ -3,11 +3,12 @@
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Service\TenantService;
use MintyPHP\Service\DepartmentService;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantScopeService;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Repository\Tenant\TenantDepartmentRepository;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::ADDRESS_BOOK_VIEW);
@@ -39,7 +40,22 @@ $departments = $tenantIds
? DepartmentService::listByTenantIds($tenantIds)
: (TenantScopeService::isStrict() ? [] : DepartmentService::list());
$roles = RoleService::list();
$roles = RoleService::listActive();
$tenantDepartmentMap = [];
if ($tenantIds) {
$departmentMapByTenantId = TenantDepartmentRepository::listDepartmentMapByTenantIds($tenantIds);
foreach ($tenants as $tenant) {
$tenantId = (int) ($tenant['id'] ?? 0);
$tenantUuid = (string) ($tenant['uuid'] ?? '');
if ($tenantId > 0 && $tenantUuid !== '') {
$tenantDepartmentMap[$tenantUuid] = array_map(
'strval',
$departmentMapByTenantId[$tenantId] ?? []
);
}
}
}
$csrfKey = Session::$csrfSessionKey;
$csrfToken = $_SESSION[$csrfKey] ?? '';

View File

@@ -1,9 +1,13 @@
<?php
use MintyPHP\Buffer;
?>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Address book')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Address book')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-list-titlebar">
<h1><?php e(t('Address book')); ?></h1>
<div class="app-list-titlebar-actions">
@@ -19,7 +23,11 @@ use MintyPHP\Buffer;
</button>
</div>
</div>
<div class="app-list-toolbar" id="address-book-toolbar">
<div
class="app-list-toolbar"
id="address-book-toolbar"
data-tenant-dept-map="<?php e(json_encode($tenantDepartmentMap ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); ?>"
>
<label class="app-field">
<span><?php e(t('Search')); ?></span>
<input type="search" id="address-book-search" placeholder="<?php e(t('Search...')); ?>">
@@ -32,27 +40,19 @@ use MintyPHP\Buffer;
<div id="address-book-grid"></div>
</div>
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="module">
import { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
initToolbarToggles();
import { initMultiSelect } from "<?php e(asset('js/components/app-multiselect-init.js')); ?>";
import { initMultiSelectCascade } from "<?php e(asset('js/components/app-multiselect-cascade.js')); ?>";
import { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
import { getAppBase, escapeHtml, buildBadgeList } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
const appBase = "<?php e(localeBase()); ?>";
const appBase = getAppBase();
const toolbar = document.querySelector('#address-book-toolbar');
const tenantDepartmentMap = toolbar?.dataset?.tenantDeptMap
? JSON.parse(toolbar.dataset.tenantDeptMap)
: {};
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
}[char]));
const buildBadgeList = (items) => {
if (!Array.isArray(items) || items.length === 0) return '';
const badges = items.map((label) => `<span class="badge" data-variant="neutral">${escapeHtml(label)}</span>`).join(' ');
return `<div class="badge-list">${badges}</div>`;
};
const uuidIndex = 1;
const normalizeCommaSeparated = (value) => {
if (Array.isArray(value)) return value.filter(Boolean);
@@ -97,9 +97,9 @@ use MintyPHP\Buffer;
{ name: "<?php e(t('Phone')); ?>", sort: false },
{ name: "<?php e(t('Mobile')); ?>", sort: false },
{ name: "<?php e(t('Short dial')); ?>", sort: false },
{ name: "<?php e(t('Tenants')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
{ name: "<?php e(t('Departments')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
{ name: "<?php e(t('Roles')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) },
{ name: "<?php e(t('Tenants')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) }
{ name: "<?php e(t('Roles')); ?>", sort: false, formatter: (cell) => gridjs.html(buildBadgeList(cell)) }
],
sortColumns: [null, null, 'first_name', 'last_name', 'email', null, null, null, null, null, null],
paginationLimit: 10,
@@ -113,9 +113,9 @@ use MintyPHP\Buffer;
row.phone,
row.mobile,
row.short_dial,
row.tenants,
row.departments,
row.roles,
row.tenants
row.roles
]),
search: {
input: '#address-book-search',
@@ -153,4 +153,14 @@ use MintyPHP\Buffer;
}
}
});
initMultiSelect().then(() => {
initMultiSelectCascade({
parent: '#address-book-tenant-filter',
child: '#address-book-department-filter',
map: tenantDepartmentMap,
mode: 'disable',
clearInvalid: true
});
});
</script>

View File

@@ -5,9 +5,9 @@ use MintyPHP\DB;
use MintyPHP\Router;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Service\UserService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantScopeService;
use MintyPHP\Service\User\UserService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::ADDRESS_BOOK_VIEW);
@@ -70,7 +70,7 @@ $departmentRows = DB::select(
(string) $userId
);
$roleRows = DB::select(
'select r.description as description from user_roles ur join roles r on r.id = ur.role_id where ur.user_id = ? order by r.description asc',
'select r.description as description from user_roles ur join roles r on r.id = ur.role_id and r.active = 1 where ur.user_id = ? order by r.description asc',
(string) $userId
);

View File

@@ -1,6 +1,6 @@
<?php
use MintyPHP\Service\UserAvatarService;
use MintyPHP\Service\User\UserAvatarService;
use MintyPHP\I18n;
$values = $user ?? [];
@@ -44,10 +44,9 @@ if ($displayName !== '') {
$initials = strtoupper($chars !== '' ? $chars : '?');
}
$tenantGroups = $values['tenant_groups'] ?? [];
$roleLabels = $values['role_labels'] ?? [];
$hasContact = ($email !== '' || $phone !== '' || $mobile !== '' || $shortDial !== '');
$hasAddress = ($address !== '' || $postalCode !== '' || $city !== '' || $region !== '' || $country !== '');
$hasOrganization = (!empty($tenantGroups) || !empty($roleLabels));
$hasOrganization = (!empty($tenantGroups));
$aboutSummary = '';
if ($profileDescription !== '') {
foreach (preg_split('/\\r\\n|\\r|\\n/', $profileDescription) as $line) {
@@ -63,17 +62,13 @@ if ($profileDescription !== '') {
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="address-book"><?php e(t('Address book')); ?></a><i
class="bi bi-chevron-right"></i><?php e($displayName); ?>
</div>
<!-- <div class="app-details-titlebar">
<h1>
<a href="address-book" title="<?php e(t('Back')); ?>"><i class="bi bi-arrow-left"></i></a>
<?php e(t('Address book')); ?>
</h1>
</div> -->
<?php
$breadcrumbs = [
['label' => t('Address book'), 'path' => 'address-book'],
['label' => $displayName],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-profile-card-container">
<div class="app-profile-card">
<div class="app-profile-banner"></div>
@@ -94,13 +89,8 @@ if ($profileDescription !== '') {
<h2><?php e($displayName); ?></h2>
<?php if ($jobTitle !== ''): ?>
<p><small><?php e($jobTitle); ?></small></p>
<?php endif; ?>
<?php if ($email !== ''): ?>
<p>
<a href="mailto:<?php e($email); ?>">
<small><?php e($email); ?></small>
</a>
</p>
<?php else: ?>
<p><a href="mailto:<?php e($email); ?>"><small><?php e($email); ?></small></a></p>
<?php endif; ?>
</hgroup>
</div>
@@ -207,29 +197,9 @@ if ($profileDescription !== '') {
<?php endforeach; ?>
</tbody>
</table>
<hr>
<?php elseif (!empty($roleLabels)): ?>
<div>
<small><?php e(t('Roles')); ?></small>
<p><?php e(implode(', ', $roleLabels)); ?></p>
</div>
<?php else: ?>
<p>-</p>
<?php endif; ?>
<?php if (!empty($roleLabels)): ?>
<table class="app-profile-table">
<thead>
<tr>
<th><?php e(t('Roles')); ?></th>
</tr>
</thead>
<tbody>
<tr>
<td><?php e(implode(', ', $roleLabels)); ?></td>
</tr>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endif; ?>
<div data-tab-panel="about">

View File

@@ -29,27 +29,54 @@ $dataDisabledAttr = $isReadOnly ? 'data-disabled="true"' : '';
<hr>
<label for="description">
<span><?php e(t('Description')); ?></span>
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
<div class="grid">
<label for="code">
<span><?php e(t('Code')); ?></span>
<input type="text" name="code" id="code" value="<?php e($values['code'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<label for="cost_center">
<span><?php e(t('Cost center')); ?></span>
<input type="text" name="cost_center" id="cost_center" value="<?php e($values['cost_center'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<label for="active">
<span>
<?php e(t('Status')); ?>
</span>
<?php $statusValue = (string) ($values['active'] ?? '1'); ?>
<select name="active" id="active" <?php e($disabledAttr); ?>>
<option value="1" <?php if ($statusValue === '1') { ?>selected
<?php } ?>>
<?php e(t('Active')); ?>
</option>
<option value="0" <?php if ($statusValue === '0') { ?>selected
<?php } ?>>
<?php e(t('Inactive')); ?>
</option>
</select>
</label>
</div>
<?php if ($tenants): ?>
<label>
<span><?php e(t('Assigned tenants')); ?></span>
</label>
<select id="department-tenants" name="tenant_ids" multiple data-multi-select="true" <?php e($dataDisabledAttr); ?>
<?php e($disabledAttr); ?> data-placeholder="<?php e(t('Select tenants')); ?>" data-search="true"
data-search-placeholder="<?php e(t('Search...')); ?>" data-select-all="true"
data-select-all-label="<?php e(t('Select all')); ?>">
<?php foreach ($tenants as $tenant): ?>
<?php
$tenantId = (int) ($tenant['id'] ?? 0);
$selected = in_array($tenantId, $selectedTenantIds ?? [], true) ? 'selected' : '';
?>
<option value="<?php e((string) $tenantId); ?>" <?php e($selected); ?>>
<?php e($tenant['description'] ?? ''); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
</details>
<?php if ($tenants): ?>
<label>
<span><?php e(t('Assigned tenants')); ?></span>
</label>
<select id="department-tenants" name="tenant_ids" multiple data-multi-select="true" <?php e($dataDisabledAttr); ?> <?php e($disabledAttr); ?>
data-placeholder="<?php e(t('Select tenants')); ?>" data-search="true"
data-search-placeholder="<?php e(t('Search...')); ?>" data-select-all="true"
data-select-all-label="<?php e(t('Select all')); ?>">
<?php foreach ($tenants as $tenant): ?>
<?php
$tenantId = (int) ($tenant['id'] ?? 0);
$selected = in_array($tenantId, $selectedTenantIds ?? [], true) ? 'selected' : '';
?>
<option value="<?php e((string) $tenantId); ?>" <?php e($selected); ?>>
<?php e($tenant['description'] ?? ''); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<?php Session::getCsrfInput(); ?>
</form>
</form>

View File

@@ -4,10 +4,10 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\DepartmentService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\Tenant\TenantScopeService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_CREATE);
@@ -20,8 +20,12 @@ if (TenantScopeService::isStrict() && !$allowedTenantIds) {
}
$errors = [];
$warnings = [];
$form = [
'description' => '',
'code' => '',
'cost_center' => '',
'active' => '1',
];
$tenants = TenantService::list();
if ($allowedTenantIds) {
@@ -38,6 +42,7 @@ if (isset($_POST['description'])) {
$result = DepartmentService::createFromAdmin($_POST, $currentUserId);
$form = $result['form'] ?? $form;
$errors = $result['errors'] ?? [];
$warnings = $result['warnings'] ?? [];
$tenantIds = $_POST['tenant_ids'] ?? [];
if (!is_array($tenantIds)) {
$tenantIds = [$tenantIds];
@@ -56,15 +61,24 @@ if (isset($_POST['description'])) {
}
$action = (string) ($_POST['action'] ?? 'create');
if ($action === 'create_close') {
if ($warnings) {
Flash::info(implode(' ', $warnings), 'admin/departments', 'department_warning');
}
Flash::success('Department created', 'admin/departments', 'department_created');
Router::redirect('admin/departments');
} else {
$uuid = (string) ($result['uuid'] ?? '');
if ($uuid !== '') {
$target = "admin/departments/edit/{$uuid}";
if ($warnings) {
Flash::info(implode(' ', $warnings), $target, 'department_warning');
}
Flash::success('Department created', $target, 'department_created');
Router::redirect($target);
} else {
if ($warnings) {
Flash::info(implode(' ', $warnings), 'admin/departments', 'department_warning');
}
Flash::success('Department created', 'admin/departments', 'department_created');
Router::redirect('admin/departments');
}

View File

@@ -8,10 +8,14 @@
?>
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/departments"><?php e(t('Departments')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create department')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments'), 'path' => 'admin/departments'],
['label' => t('Create department')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/departments" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
@@ -35,6 +39,15 @@
</ul>
</div>
<?php endif; ?>
<?php if (!empty($warnings)): ?>
<div class="notice" data-variant="info">
<ul>
<?php foreach ($warnings as $warning): ?>
<li><?php e($warning); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php
$values = $form ?? [];
$detailsOpenAll = true;

View File

@@ -2,10 +2,10 @@
use MintyPHP\Router;
use MintyPHP\Support\Guard;
use MintyPHP\Service\DepartmentService;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantScopeService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
@@ -19,6 +19,7 @@ $search = trim((string) ($_GET['search'] ?? ''));
$order = (string) ($_GET['order'] ?? 'description');
$dir = (string) ($_GET['dir'] ?? 'asc');
$tenant = trim((string) ($_GET['tenant'] ?? ''));
$active = trim((string) ($_GET['active'] ?? ''));
$result = DepartmentService::listPaged([
'limit' => $limit,
@@ -27,6 +28,7 @@ $result = DepartmentService::listPaged([
'order' => $order,
'dir' => $dir,
'tenant' => $tenant,
'active' => $active,
'tenantIds' => $tenantIds,
'tenantUserId' => $currentUserId,
]);
@@ -40,6 +42,9 @@ foreach ($result['rows'] as $row) {
'uuid' => $row['uuid'] ?? '',
'is_default' => $departmentId > 0 && $defaultDepartmentId === $departmentId,
'description' => $row['description'] ?? '',
'code' => $row['code'] ?? '',
'cost_center' => $row['cost_center'] ?? '',
'active' => $row['active'] ?? 1,
'tenants' => $row['tenant_labels'] ?? [],
'created' => dt($row['created'] ?? ''),
'modified' => dt($row['modified'] ?? ''),

View File

@@ -3,10 +3,10 @@
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\AuthService;
use MintyPHP\Service\DepartmentService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Service\Auth\AuthService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantScopeService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_DELETE);

View File

@@ -4,13 +4,13 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\DepartmentService;
use MintyPHP\Repository\TenantDepartmentRepository;
use MintyPHP\Service\TenantService;
use MintyPHP\Service\UserService;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Repository\Tenant\TenantDepartmentRepository;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\User\UserService;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantScopeService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);
@@ -53,6 +53,7 @@ if ($modifierId > 0) {
}
$errors = [];
$warnings = [];
$form = $department;
$tenants = TenantService::list();
$selectedTenantIds = TenantDepartmentRepository::listTenantIdsByDepartmentId($departmentId);
@@ -75,6 +76,7 @@ if (isset($_POST['description'])) {
$result = DepartmentService::updateFromAdmin($departmentId, $_POST, $currentUserId);
$form = $result['form'] ?? $form;
$errors = $result['errors'] ?? [];
$warnings = $result['warnings'] ?? [];
$tenantIds = $_POST['tenant_ids'] ?? [];
if (!is_array($tenantIds)) {
$tenantIds = [$tenantIds];
@@ -99,9 +101,15 @@ if (isset($_POST['description'])) {
Flash::info(t('Department assignments cleaned: %d', $cleaned), "admin/departments/edit/{$uuid}", 'department_assignments_cleaned');
}
if ($action === 'save_close') {
if ($warnings) {
Flash::info(implode(' ', $warnings), 'admin/departments', 'department_warning');
}
Flash::success('Department updated', 'admin/departments', 'department_updated');
Router::redirect('admin/departments');
} else {
if ($warnings) {
Flash::info(implode(' ', $warnings), "admin/departments/edit/{$uuid}", 'department_warning');
}
Flash::success('Department updated', "admin/departments/edit/{$uuid}", 'department_updated');
Router::redirect("admin/departments/edit/{$uuid}");
}

View File

@@ -15,10 +15,14 @@ $isReadOnly = !can('departments.update');
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/departments"><?php e(t('Departments')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Edit department')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments'), 'path' => 'admin/departments'],
['label' => t('Edit department')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/departments" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
@@ -64,6 +68,17 @@ $isReadOnly = !can('departments.update');
</div>
</div>
<?php endif; ?>
<?php if (!empty($warnings)): ?>
<div class="app-details-warnings">
<div class="notice" data-variant="info">
<ul>
<?php foreach ($warnings as $warning): ?>
<li><?php e($warning); ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$detailsOpenAll = false;
@@ -77,18 +92,22 @@ $isReadOnly = !can('departments.update');
<h2><?php e($values['description'] ?? ''); ?></h2>
<p><?php e(t('Department')); ?></p>
</hgroup>
<hr>
<div class="grid">
<div>
<small><?php e(t('ID')); ?></small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['id'] ?? ''); ?>">
<?php e($values['id'] ?? '-'); ?>
</span>
</p>
</div>
<div class="badge-list">
<?php if ((string) ($values['active'] ?? '1') === '0'): ?>
<span class="badge" data-variant="danger"><?php e(t('Inactive')); ?></span>
<?php else: ?>
<span class="badge" data-variant="success"><?php e(t('Active')); ?></span>
<?php endif; ?>
</div>
<?php if (!empty($values['code']) || !empty($values['cost_center'])): ?>
<hr>
<?php if (!empty($values['code'])): ?>
<p><small><?php e(t('Code')); ?></small><br><?php e($values['code']); ?></p>
<?php endif; ?>
<?php if (!empty($values['cost_center'])): ?>
<p><small><?php e(t('Cost center')); ?></small><br><?php e($values['cost_center']); ?></p>
<?php endif; ?>
<?php endif; ?>
<?php
$createdByLabel = $values['created_by_label'] ?? '';
$createdById = $values['created_by'] ?? null;
@@ -97,71 +116,91 @@ $isReadOnly = !can('departments.update');
$modifiedById = $values['modified_by'] ?? null;
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
?>
<div class="grid">
<div>
<small><?php e(t('Created')); ?></small>
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Created by')); ?>
</small>
<p>
<?php if ($createdByLabel !== ''): ?>
<?php if ($createdByUuid): ?>
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
<hr>
<details>
<summary><?php e(t('Audit')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Created')); ?></small>
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Created by')); ?>
</small>
<p>
<?php if ($createdByLabel !== ''): ?>
<?php if ($createdByUuid): ?>
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
<?php else: ?>
<?php e($createdByLabel); ?>
<?php endif; ?>
<?php elseif ($createdById): ?>
<?php e('#' . $createdById); ?>
<?php else: ?>
<?php e($createdByLabel); ?>
-
<?php endif; ?>
<?php elseif ($createdById): ?>
<?php e('#' . $createdById); ?>
<?php else: ?>
-
<?php endif; ?>
</p>
</p>
</div>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Modified')); ?></small>
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Modified by')); ?>
</small>
<p>
<?php if ($modifiedByLabel !== ''): ?>
<?php if ($modifiedByUuid): ?>
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
<div class="grid">
<div>
<small><?php e(t('Modified')); ?></small>
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Modified by')); ?>
</small>
<p>
<?php if ($modifiedByLabel !== ''): ?>
<?php if ($modifiedByUuid): ?>
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
<?php else: ?>
<?php e($modifiedByLabel); ?>
<?php endif; ?>
<?php elseif ($modifiedById): ?>
<?php e('#' . $modifiedById); ?>
<?php else: ?>
<?php e($modifiedByLabel); ?>
-
<?php endif; ?>
<?php elseif ($modifiedById): ?>
<?php e('#' . $modifiedById); ?>
<?php else: ?>
-
<?php endif; ?>
</p>
</p>
</div>
</div>
</div>
<div class="grid">
<div>
<small>
<?php e(t('UUID')); ?>
</small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
<?php
$uuidValue = (string) ($values['uuid'] ?? '');
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
e($uuidDisplay);
?>
</span>
</p>
</details>
<hr>
<details>
<summary><?php e(t('IDs')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('ID')); ?></small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['id'] ?? ''); ?>">
<?php e($values['id'] ?? '-'); ?>
</span>
</p>
</div>
<div>
<small>
<?php e(t('UUID')); ?>
</small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
<?php
$uuidValue = (string) ($values['uuid'] ?? '');
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
e($uuidDisplay);
?>
</span>
</p>
</div>
</div>
</div>
</details>
<hr>
</div>
</aside>
</div>

View File

@@ -3,9 +3,9 @@
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\TenantService;
use MintyPHP\Service\TenantScopeService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\Tenant\TenantScopeService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::DEPARTMENTS_VIEW);

View File

@@ -4,11 +4,16 @@ use MintyPHP\Router;
$canCreateDepartments = can('departments.create');
$canDeleteDepartments = can('departments.delete');
$activeTenant = trim((string) ($_GET['tenant'] ?? ''));
$activeStatus = trim((string) ($_GET['active'] ?? ''));
$showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
?>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Departments')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Departments')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-list-titlebar">
<h1><?php e(t('Departments')); ?></h1>
<div class="app-list-titlebar-actions">
@@ -46,32 +51,51 @@ $showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
<?php endif; ?>
<div class="app-list-toolbar" id="departments-toolbar">
<input type="hidden" id="department-tenant-filter" value="<?php e($activeTenant); ?>">
<input type="search" id="department-search" placeholder="<?php e(t('Search...')); ?>">
<label class="app-field">
<span><?php e(t('Search')); ?></span>
<input type="search" id="department-search" placeholder="<?php e(t('Search...')); ?>">
</label>
<label class="app-field">
<span><?php e(t('Status')); ?></span>
<select id="department-status-filter">
<option value="all" <?php if ($activeStatus === '' || $activeStatus === 'all') { ?>selected<?php } ?>>
<?php e(t('All')); ?>
</option>
<option value="active" <?php if ($activeStatus === 'active' || $activeStatus === '1') { ?>selected<?php } ?>>
<?php e(t('Active')); ?>
</option>
<option value="inactive" <?php if ($activeStatus === 'inactive' || $activeStatus === '0') { ?>selected<?php } ?>>
<?php e(t('Inactive')); ?>
</option>
</select>
</label>
</div>
<div class="app-list-table">
<div id="departments-grid"></div>
</div>
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="module">
import { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
import { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
import { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
import { buildUrl, postAction, badgeHtml, getAppBase } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
initToolbarToggles();
const appBase = "<?php e(localeBase()); ?>";
const appBase = getAppBase();
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
const deleteConfirm = "<?php e(t('Delete this department?')); ?>";
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/departments/${action}/${id}`);
const initDepartmentsGrid = () => {
const formatBadge = (value) => badgeHtml(gridjs, value);
const buildBadgeList = (items) => {
if (!Array.isArray(items) || items.length === 0) return '';
const badges = items.map((label) => `<span class="badge" data-variant="neutral">${label}</span>`).join(' ');
return `<div class="badge-list">${badges}</div>`;
const statusLabels = {
active: "<?php e(t('Active')); ?>",
inactive: "<?php e(t('Inactive')); ?>"
};
const formatStatus = (value) => {
const normalized = String(value ?? '1').toLowerCase();
const isInactive = ['0', 'false', 'inactive'].includes(normalized);
const variant = isInactive ? 'danger' : 'success';
const label = isInactive ? statusLabels.inactive : statusLabels.active;
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
};
const gridConfig = createServerGrid({
gridjs: window.gridjs,
@@ -80,6 +104,13 @@ $showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
appBase,
columns: [
{ name: "<?php e(t('Description')); ?>", sort: true },
{ name: "<?php e(t('Code')); ?>", sort: true },
{ name: "<?php e(t('Cost center')); ?>", sort: true },
{
name: "<?php e(t('Status')); ?>",
sort: false,
formatter: (cell) => formatStatus(cell)
},
{
name: "<?php e(t('Created')); ?>",
sort: true,
@@ -99,11 +130,14 @@ $showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
hidden: true
}
],
sortColumns: ['description', 'created', 'modified', null, null],
sortColumns: ['description', 'code', 'cost_center', null, 'created', 'modified', null, null],
paginationLimit: 10,
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
mapData: (data) => data.data.map((row) => [
row.description,
row.code,
row.cost_center,
row.active,
row.created,
row.modified,
row.uuid,
@@ -112,7 +146,7 @@ $showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
actions: {
enabled: true,
label: "<?php e(t('Actions')); ?>",
getRowId: (row) => row?.cells?.[3]?.data,
getRowId: (row) => row?.cells?.[7]?.data,
items: [
{
key: 'edit',
@@ -149,15 +183,21 @@ $showTenantTabs = isset($tenants) && is_array($tenants) && count($tenants) > 1;
{
input: '#department-tenant-filter',
param: 'tenant'
},
{
input: '#department-status-filter',
param: 'active',
default: 'all',
normalize: (value) => (value === 'all' ? '' : value)
}
],
urlSync: true,
rowDataset: (row) => ({
uuid: row?.cells?.[4]?.data
uuid: row?.cells?.[6]?.data
}),
rowDblClick: {
getUrl: (rowData) => {
const uuid = rowData?.cells?.[3]?.data;
const uuid = rowData?.cells?.[6]?.data;
return uuid ? new URL(`admin/departments/edit/${uuid}`, appBase).toString() : '';
}
}

View File

@@ -11,9 +11,13 @@ if (!empty($notFound)) {
}
?>
<div class="app-breadcrumb">
<a href="/logout">Login</a><i class="bi bi-chevron-right"></i><?php e(t('Home')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Login'), 'path' => 'logout'],
['label' => t('Home')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-dashboard-titlebar">
<h1><?php e(t('Welcome back')); ?> <?php e($first_name )?></h1>
<div class="app-dashboard-titlebar-actions">

View File

@@ -2,8 +2,8 @@
use MintyPHP\Router;
use MintyPHP\Support\Guard;
use MintyPHP\Service\MailLogService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Mail\MailLogService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermissionOrForbidden(PermissionService::MAIL_LOG_VIEW);

View File

@@ -3,7 +3,7 @@
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::MAIL_LOG_VIEW);

View File

@@ -1,9 +1,13 @@
<?php
use MintyPHP\Router;
?>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Mail logs')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Mail logs')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-list-titlebar">
<h1><?php e(t('Mail logs')); ?></h1>
<div class="app-list-titlebar-actions">
@@ -46,16 +50,12 @@ use MintyPHP\Router;
<div id="mail-log-grid"></div>
</div>
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="module">
import { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
import { buildUrl, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
import { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
import { badgeHtml, getAppBase } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
initToolbarToggles();
const appBase = "<?php e(localeBase()); ?>";
const appBase = getAppBase();
const initMailLogGrid = () => {
const formatBadge = (value) => badgeHtml(gridjs, value);
@@ -120,17 +120,7 @@ use MintyPHP\Router;
row.id
]),
actions: {
enabled: true,
label: "<?php e(t('Actions')); ?>",
getRowId: (row) => row?.cells?.[5]?.data,
items: [
{
key: 'view',
type: 'link',
label: "<i class=\"bi bi-eye-fill\"></i>",
href: ({ id }) => buildUrl(appBase, `admin/mail-log/view/${id}`)
}
]
enabled: false
},
search: {
input: '#mail-log-search',

View File

@@ -4,8 +4,8 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\MailLogService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Mail\MailLogService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::MAIL_LOG_VIEW);

View File

@@ -17,10 +17,14 @@ if ($status === 'sent') {
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/mail-log"><?php e(t('Mail logs')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('View')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Mail logs'), 'path' => 'admin/mail-log'],
['label' => t('View')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/mail-log" title="<?php e(t('Back')); ?>"><i class="bi bi-arrow-left"></i></a>
@@ -134,4 +138,4 @@ if ($status === 'sent') {
<?php endif; ?>
</div>
</aside>
</div>
</div>

View File

@@ -15,6 +15,8 @@ $detailsOpenAll = $detailsOpenAll ?? false;
$isReadOnly = $isReadOnly ?? false;
$detailsOpenAttr = $detailsOpenAll ? 'open' : '';
$readonlyAttr = $isReadOnly ? 'readonly' : '';
$disabledAttr = $isReadOnly ? 'disabled' : '';
$keyReadonlyAttr = ($isReadOnly || !empty($values['is_system'])) ? 'readonly' : '';
?>
<form id="<?php e($formId); ?>" method="post">
@@ -29,9 +31,31 @@ $readonlyAttr = $isReadOnly ? 'readonly' : '';
</label>
<label for="permission-key">
<span><?php e(t('Permission key')); ?></span>
<input required type="text" name="key" id="permission-key" value="<?php e($values['key'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
<input required type="text" name="key" id="permission-key" value="<?php e($values['key'] ?? ''); ?>" <?php e($keyReadonlyAttr); ?> />
</label>
</div>
<label for="permission-active">
<span><?php e(t('Status')); ?></span>
<?php $statusValue = (string) ($values['active'] ?? '1'); ?>
<select name="active" id="permission-active" <?php e($disabledAttr); ?>>
<option value="1" <?php if ($statusValue === '1') { ?>selected<?php } ?>>
<?php e(t('Active')); ?>
</option>
<option value="0" <?php if ($statusValue === '0') { ?>selected<?php } ?>>
<?php e(t('Inactive')); ?>
</option>
</select>
</label>
<fieldset>
<legend><small><?php e(t('System permission')); ?></small></legend>
<label class="app-field app-checkbox">
<?php $systemValue = (int) ($values['is_system'] ?? 0); ?>
<input type="checkbox" name="is_system" value="1" <?php if ($systemValue === 1) { ?>checked<?php } ?>
<?php e($disabledAttr); ?> />
<span><?php e(t('System permission')); ?></span>
</label>
<small class="muted"><?php e(t('System permissions are core and should rarely be disabled.')); ?></small>
</fieldset>
</details>
<hr>
<?php Session::getCsrfInput(); ?>

View File

@@ -4,7 +4,7 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::PERMISSIONS_CREATE);
@@ -13,6 +13,8 @@ $errors = [];
$form = [
'key' => '',
'description' => '',
'active' => 1,
'is_system' => 0,
];
if (isset($_POST['key'])) {

View File

@@ -8,10 +8,14 @@
?>
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/permissions"><?php e(t('Permissions')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create permission')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions'), 'path' => 'admin/permissions'],
['label' => t('Create permission')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/permissions" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>

View File

@@ -2,7 +2,7 @@
use MintyPHP\Router;
use MintyPHP\Support\Guard;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
@@ -16,6 +16,7 @@ $offset = (int) ($_GET['offset'] ?? 0);
$search = trim((string) ($_GET['search'] ?? ''));
$order = (string) ($_GET['order'] ?? 'key');
$dir = (string) ($_GET['dir'] ?? 'asc');
$active = (string) ($_GET['active'] ?? '');
$result = PermissionService::listPaged([
'limit' => $limit,
@@ -23,6 +24,7 @@ $result = PermissionService::listPaged([
'search' => $search,
'order' => $order,
'dir' => $dir,
'active' => $active,
]);
$rows = [];
@@ -31,6 +33,8 @@ foreach ($result['data'] ?? [] as $row) {
'id' => $row['id'] ?? null,
'key' => $row['key'] ?? '',
'description' => $row['description'] ?? '',
'active' => (int) ($row['active'] ?? 1),
'is_system' => (int) ($row['is_system'] ?? 0),
'created' => dt($row['created'] ?? ''),
];
}

View File

@@ -3,7 +3,7 @@
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::PERMISSIONS_DELETE);
@@ -15,7 +15,12 @@ if ($id <= 0) {
$result = PermissionService::deleteById($id);
if (!($result['ok'] ?? false)) {
Flash::error('Permission not found', 'admin/permissions', 'permission_not_found');
$error = (string) ($result['error'] ?? '');
if ($error === 'system_permission_protected') {
Flash::error('System permission can not be deleted', 'admin/permissions', 'permission_system_protected');
} else {
Flash::error('Permission not found', 'admin/permissions', 'permission_not_found');
}
Router::redirect('admin/permissions');
}

View File

@@ -4,9 +4,9 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Repository\RolePermissionRepository;
use MintyPHP\Repository\RoleRepository;
use MintyPHP\Service\PermissionService;
use MintyPHP\Repository\Access\RolePermissionRepository;
use MintyPHP\Repository\Access\RoleRepository;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);
@@ -25,7 +25,7 @@ if (!$permission) {
$errors = [];
$form = $permission;
$roles = RoleRepository::list();
$roles = RoleRepository::listActive();
$selectedRoleIds = RolePermissionRepository::listRoleIdsByPermissionId($id);
if (isset($_POST['key'])) {

View File

@@ -9,18 +9,21 @@
?>
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/permissions"><?php e(t('Permissions')); ?></a><i
class="bi bi-chevron-right"></i><?php e(t('Edit permission')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions'), 'path' => 'admin/permissions'],
['label' => t('Edit permission')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/permissions" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
<?php e(t('Edit permission')); ?>
</h1>
<div class="app-details-titlebar-actions">
<?php if (can('permissions.delete')): ?>
<?php if (can('permissions.delete') && empty($permission['is_system'])): ?>
<details class="dropdown">
<summary role="button" class="outline secondary"><i class="bi bi-three-dots"></i></summary>
<ul dir="rtl">
@@ -36,7 +39,6 @@
</ul>
</details>
<?php endif; ?>
<?php if (can('permissions.update')): ?>
<button type="submit" form="permission-form" name="action" value="save" class="secondary outline">
<?php e(t('Save')); ?>
@@ -63,6 +65,11 @@
$isReadOnly = $isReadOnly ?? false;
require __DIR__ . '/_form.phtml';
?>
<?php if (!empty($roles)): ?>
<div>
<?php multiSelectForm('permission-roles', 'role_ids', 'Assigned roles', 'Select roles', $roles, $selectedRoleIds ?? [], $isReadOnly, 'description', 'permission-form'); ?>
</div>
<?php endif; ?>
</section>
<aside id="app-details-aside-section">
<div class="app-details-aside-section">
@@ -70,10 +77,17 @@
<h2><?php e($values['key'] ?? ''); ?></h2>
<p><?php e(t('Permission')); ?></p>
</hgroup>
<div class="badge-list">
<?php if ((string) ($values['active'] ?? '1') === '0'): ?>
<span class="badge" data-variant="danger"><?php e(t('Inactive')); ?></span>
<?php else: ?>
<span class="badge" data-variant="success"><?php e(t('Active')); ?></span>
<?php endif; ?>
<?php if (!empty($values['is_system'])): ?>
<span class="badge" data-variant="neutral"><?php e(t('System')); ?></span>
<?php endif; ?>
</div>
<hr>
<?php if (!empty($roles)): ?>
<?php multiSelectForm('permission-roles', 'role_ids', 'Assigned roles', 'Select roles', $roles, $selectedRoleIds ?? [], $isReadOnly, 'description', 'permission-form'); ?>
<?php endif; ?>
</div>
</aside>
</div>
</div>

View File

@@ -3,7 +3,7 @@
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::PERMISSIONS_VIEW);

View File

@@ -3,22 +3,19 @@ use MintyPHP\Router;
$canCreatePermissions = can('permissions.create');
$canUpdatePermissions = can('permissions.update');
$canDeletePermissions = can('permissions.delete');
?>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Permissions')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Permissions')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-list-titlebar">
<h1><?php e(t('Permissions')); ?></h1>
<div class="app-list-titlebar-actions">
<button
class="outline secondary"
type="button"
data-toolbar-toggle
data-toolbar-target="#permissions-toolbar"
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
>
<button class="outline secondary" type="button" data-toolbar-toggle data-toolbar-target="#permissions-toolbar"
data-toolbar-text-show="<?php e(t('Show filters')); ?>" data-toolbar-text-hide="<?php e(t('Hide filters')); ?>">
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
</button>
<?php if ($canCreatePermissions): ?>
@@ -29,25 +26,34 @@ $canDeletePermissions = can('permissions.delete');
</div>
</div>
<div class="app-list-toolbar" id="permissions-toolbar">
<input type="search" id="permission-search" placeholder="<?php e(t('Search...')); ?>">
<label class="app-field">
<span><?php e(t('Search')); ?></span>
<input type="search" id="permission-search" placeholder="<?php e(t('Search...')); ?>">
</label>
<label class="app-field">
<span><?php e(t('Status')); ?></span>
<select id="permission-status-filter">
<option value="all"><?php e(t('All')); ?></option>
<option value="active"><?php e(t('Active')); ?></option>
<option value="inactive"><?php e(t('Inactive')); ?></option>
</select>
</label>
</div>
<div class="app-list-table">
<div id="permissions-grid"></div>
</div>
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="module">
import { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
import { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
import { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
import { buildUrl, badgeHtml, getAppBase } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
initToolbarToggles();
const appBase = "<?php e(localeBase()); ?>";
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
const deleteConfirm = "<?php e(t('Delete this permission?')); ?>";
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/permissions/${action}/${id}`);
const appBase = getAppBase();
const labels = {
active: "<?php e(t('Active')); ?>",
inactive: "<?php e(t('Inactive')); ?>",
system: "<?php e(t('System')); ?>"
};
const initPermissionsGrid = () => {
const gridConfig = createServerGrid({
@@ -58,55 +64,48 @@ $canDeletePermissions = can('permissions.delete');
columns: [
{ name: "<?php e(t('Description')); ?>", sort: true },
{ name: "<?php e(t('Permission key')); ?>", sort: true },
{
name: "<?php e(t('Status')); ?>",
sort: true,
formatter: (cell) => cell
? gridjs.html(`<span class="badge" data-variant="success">${labels.active}</span>`)
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive}</span>`)
},
{
name: "<?php e(t('System')); ?>",
sort: false,
formatter: (cell) => cell
? gridjs.html(`<span class="badge" data-variant="neutral">${labels.system}</span>`)
: gridjs.html('')
},
{ name: "<?php e(t('Created')); ?>", sort: true, formatter: (cell) => badgeHtml(gridjs, cell) },
{ name: 'ID', hidden: true }
],
sortColumns: ['description', 'key', 'created', null],
sortColumns: ['description', 'key', 'active', null, 'created', null],
paginationLimit: 10,
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
mapData: (data) => data.data.map((row) => [
row.description,
row.key,
row.active,
row.is_system,
row.created,
row.id
]),
actions: {
enabled: true,
label: "<?php e(t('Actions')); ?>",
getRowId: (row) => row?.cells?.[3]?.data,
items: [
{
key: 'edit',
type: 'link',
label: "<i class=\"bi bi-pencil-fill\"></i>",
href: ({ id }) => buildActionUrl('edit', id)
},
<?php if ($canDeletePermissions): ?>
{
key: 'delete',
label: "<i class=\"bi bi-trash3-fill\"></i>",
className: 'danger outline',
confirm: deleteConfirm
}
<?php endif; ?>
],
onAction: async ({ action, id, grid }) => {
if (action !== 'delete') return;
const url = buildActionUrl('delete', id);
const response = await postAction(url, csrf);
if (response.ok) {
grid.forceRender();
} else {
window.location.href = new URL('admin/permissions', appBase).toString();
}
}
},
rowDblClick: {
getUrl: (row) => {
const id = row?.cells?.[3]?.data;
return id ? buildActionUrl('edit', id) : '';
const id = row?.cells?.[5]?.data;
return id ? new URL(`admin/permissions/edit/${id}`, appBase).toString() : '';
}
},
filters: [
{
input: '#permission-status-filter',
param: 'active',
default: 'all',
normalize: (value) => (value && value !== 'all' ? value : '')
}
],
search: {
input: '#permission-search',
param: 'search',

View File

@@ -15,6 +15,7 @@ $detailsOpenAll = $detailsOpenAll ?? false;
$isReadOnly = $isReadOnly ?? false;
$detailsOpenAttr = $detailsOpenAll ? 'open' : '';
$readonlyAttr = $isReadOnly ? 'readonly' : '';
$disabledAttr = $isReadOnly ? 'disabled' : '';
?>
<form id="<?php e($formId); ?>" method="post">
@@ -25,6 +26,24 @@ $readonlyAttr = $isReadOnly ? 'readonly' : '';
<span><?php e(t('Description')); ?></span>
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<div class="grid">
<label for="code">
<span><?php e(t('Code')); ?></span>
<input type="text" name="code" id="code" value="<?php e($values['code'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
</label>
<label for="active">
<span><?php e(t('Status')); ?></span>
<?php $statusValue = (string) ($values['active'] ?? '1'); ?>
<select name="active" id="active" <?php e($disabledAttr); ?>>
<option value="1" <?php if ($statusValue === '1') { ?>selected<?php } ?>>
<?php e(t('Active')); ?>
</option>
<option value="0" <?php if ($statusValue === '0') { ?>selected<?php } ?>>
<?php e(t('Inactive')); ?>
</option>
</select>
</label>
</div>
</details>
<hr>
<?php if (!empty($permissions)): ?>

View File

@@ -4,10 +4,10 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Repository\PermissionRepository;
use MintyPHP\Repository\RolePermissionRepository;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Repository\Access\PermissionRepository;
use MintyPHP\Repository\Access\RolePermissionRepository;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_CREATE);
@@ -15,8 +15,10 @@ Guard::requirePermission(PermissionService::ROLES_CREATE);
$errors = [];
$form = [
'description' => '',
'code' => '',
'active' => 1,
];
$permissions = PermissionRepository::list();
$permissions = PermissionRepository::listActive();
$selectedPermissionIds = [];
if (isset($_POST['description'])) {

View File

@@ -8,10 +8,14 @@
?>
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/roles"><?php e(t('Roles')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Create role')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles'), 'path' => 'admin/roles'],
['label' => t('Create role')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/roles" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>

View File

@@ -2,9 +2,9 @@
use MintyPHP\Router;
use MintyPHP\Support\Guard;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_VIEW);
@@ -14,6 +14,7 @@ $offset = (int) ($_GET['offset'] ?? 0);
$search = trim((string) ($_GET['search'] ?? ''));
$order = (string) ($_GET['order'] ?? 'description');
$dir = (string) ($_GET['dir'] ?? 'asc');
$active = (string) ($_GET['active'] ?? '');
$result = RoleService::listPaged([
'limit' => $limit,
@@ -21,6 +22,7 @@ $result = RoleService::listPaged([
'search' => $search,
'order' => $order,
'dir' => $dir,
'active' => $active,
]);
$defaultRoleId = SettingService::getDefaultRoleId();
@@ -32,6 +34,8 @@ foreach ($result['rows'] as $row) {
'uuid' => $row['uuid'] ?? '',
'is_default' => $roleId > 0 && $defaultRoleId === $roleId,
'description' => $row['description'] ?? '',
'code' => $row['code'] ?? '',
'active' => (int) ($row['active'] ?? 1),
'created' => dt($row['created'] ?? ''),
'modified' => dt($row['modified'] ?? ''),
];

View File

@@ -3,8 +3,8 @@
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_DELETE);

View File

@@ -4,12 +4,12 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Repository\PermissionRepository;
use MintyPHP\Repository\RolePermissionRepository;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\UserService;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Repository\Access\PermissionRepository;
use MintyPHP\Repository\Access\RolePermissionRepository;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\User\UserService;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_VIEW);
@@ -48,7 +48,7 @@ if ($modifierId > 0) {
$errors = [];
$form = $role;
$permissions = PermissionRepository::list();
$permissions = PermissionRepository::listActive();
$selectedPermissionIds = RolePermissionRepository::listPermissionIdsByRoleId($roleId);
if (isset($_POST['description'])) {

View File

@@ -15,10 +15,14 @@ $isReadOnly = !can('roles.update');
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><a
href="/admin/roles"><?php e(t('Roles')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Edit role')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles'), 'path' => 'admin/roles'],
['label' => t('Edit role')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<a href="admin/roles" title="<?php e(t('Cancel')); ?>"><i class="bi bi-arrow-left"></i></a>
@@ -77,18 +81,17 @@ $isReadOnly = !can('roles.update');
<h2><?php e($values['description'] ?? ''); ?></h2>
<p><?php e(t('Role')); ?></p>
</hgroup>
<hr>
<div class="grid">
<div>
<small><?php e(t('ID')); ?></small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['id'] ?? ''); ?>">
<?php e($values['id'] ?? '-'); ?>
</span>
</p>
</div>
<div class="badge-list">
<?php if ((string) ($values['active'] ?? '1') === '0'): ?>
<span class="badge" data-variant="danger"><?php e(t('Inactive')); ?></span>
<?php else: ?>
<span class="badge" data-variant="success"><?php e(t('Active')); ?></span>
<?php endif; ?>
</div>
<?php if (!empty($values['code'])): ?>
<hr>
<p><small><?php e(t('Code')); ?></small><br><?php e($values['code']); ?></p>
<?php endif; ?>
<?php
$createdByLabel = $values['created_by_label'] ?? '';
$createdById = $values['created_by'] ?? null;
@@ -97,71 +100,91 @@ $isReadOnly = !can('roles.update');
$modifiedById = $values['modified_by'] ?? null;
$modifiedByUuid = $values['modified_by_uuid'] ?? null;
?>
<div class="grid">
<div>
<small><?php e(t('Created')); ?></small>
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Created by')); ?>
</small>
<p>
<?php if ($createdByLabel !== ''): ?>
<?php if ($createdByUuid): ?>
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
<hr>
<details>
<summary><?php e(t('Audit')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('Created')); ?></small>
<p><?php e(dt($values['created'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Created by')); ?>
</small>
<p>
<?php if ($createdByLabel !== ''): ?>
<?php if ($createdByUuid): ?>
<a href="admin/users/edit/<?php e($createdByUuid); ?>"><?php e($createdByLabel); ?></a>
<?php else: ?>
<?php e($createdByLabel); ?>
<?php endif; ?>
<?php elseif ($createdById): ?>
<?php e('#' . $createdById); ?>
<?php else: ?>
<?php e($createdByLabel); ?>
-
<?php endif; ?>
<?php elseif ($createdById): ?>
<?php e('#' . $createdById); ?>
<?php else: ?>
-
<?php endif; ?>
</p>
</p>
</div>
</div>
</div>
<div class="grid">
<div>
<small><?php e(t('Modified')); ?></small>
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Modified by')); ?>
</small>
<p>
<?php if ($modifiedByLabel !== ''): ?>
<?php if ($modifiedByUuid): ?>
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
<div class="grid">
<div>
<small><?php e(t('Modified')); ?></small>
<p><?php e(dt($values['modified'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small>
<?php e(t('Modified by')); ?>
</small>
<p>
<?php if ($modifiedByLabel !== ''): ?>
<?php if ($modifiedByUuid): ?>
<a href="admin/users/edit/<?php e($modifiedByUuid); ?>"><?php e($modifiedByLabel); ?></a>
<?php else: ?>
<?php e($modifiedByLabel); ?>
<?php endif; ?>
<?php elseif ($modifiedById): ?>
<?php e('#' . $modifiedById); ?>
<?php else: ?>
<?php e($modifiedByLabel); ?>
-
<?php endif; ?>
<?php elseif ($modifiedById): ?>
<?php e('#' . $modifiedById); ?>
<?php else: ?>
-
<?php endif; ?>
</p>
</p>
</div>
</div>
</div>
<div class="grid">
<div>
<small>
<?php e(t('UUID')); ?>
</small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
<?php
$uuidValue = (string) ($values['uuid'] ?? '');
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
e($uuidDisplay);
?>
</span>
</p>
</details>
<hr>
<details>
<summary><?php e(t('IDs')); ?></summary>
<hr>
<div class="grid">
<div>
<small><?php e(t('ID')); ?></small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['id'] ?? ''); ?>">
<?php e($values['id'] ?? '-'); ?>
</span>
</p>
</div>
<div>
<small>
<?php e(t('UUID')); ?>
</small>
<p>
<span class="badge" data-variant="neutral" data-copy="true"
data-copy-value="<?php e($values['uuid'] ?? ''); ?>">
<?php
$uuidValue = (string) ($values['uuid'] ?? '');
$uuidDisplay = $uuidValue !== '' ? substr($uuidValue, 0, 10) : '-';
e($uuidDisplay);
?>
</span>
</p>
</div>
</div>
</div>
</details>
<hr>
</div>
</aside>
</div>

View File

@@ -3,7 +3,7 @@
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Session;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::ROLES_VIEW);

View File

@@ -4,20 +4,18 @@ use MintyPHP\Router;
$canCreateRoles = can('roles.create');
$canDeleteRoles = can('roles.delete');
?>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Roles')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Roles')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-list-titlebar">
<h1><?php e(t('Roles')); ?></h1>
<div class="app-list-titlebar-actions">
<button
class="outline secondary"
type="button"
data-toolbar-toggle
data-toolbar-target="#roles-toolbar"
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
>
<button class="outline secondary" type="button" data-toolbar-toggle data-toolbar-target="#roles-toolbar"
data-toolbar-text-show="<?php e(t('Show filters')); ?>" data-toolbar-text-hide="<?php e(t('Hide filters')); ?>">
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
</button>
<?php if ($canCreateRoles): ?>
@@ -28,24 +26,35 @@ $canDeleteRoles = can('roles.delete');
</div>
</div>
<div class="app-list-toolbar" id="roles-toolbar">
<input type="search" id="role-search" placeholder="<?php e(t('Search...')); ?>">
<label class="app-field">
<span>Suche</span>
<input type="search" id="role-search" placeholder="<?php e(t('Search...')); ?>">
</label>
<label class="app-field">
<span><?php e(t('Status')); ?></span>
<select id="role-status-filter">
<option value="all"><?php e(t('All')); ?></option>
<option value="active"><?php e(t('Active')); ?></option>
<option value="inactive"><?php e(t('Inactive')); ?></option>
</select>
</label>
</div>
<div class="app-list-table">
<div id="roles-grid"></div>
</div>
<?php $gridFactoryVersion = @filemtime('web/js/core/app-grid-factory.js') ?: time(); ?>
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
<script type="module">
import { initToolbarToggles } from "<?php e(asset('js/components/app-toggle-toolbar.js')); ?>";
import { createServerGrid } from "<?php e(asset('js/core/app-grid-factory.js?v=' . $gridFactoryVersion)); ?>";
import { buildUrl, postAction, badgeHtml } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
import { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
import { buildUrl, postAction, badgeHtml, getAppBase } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
initToolbarToggles();
const appBase = "<?php e(localeBase()); ?>";
const appBase = getAppBase();
const csrf = <?php MintyPHP\Buffer::get('grid_csrf'); ?>;
const deleteConfirm = "<?php e(t('Delete this role?')); ?>";
const labels = {
active: "<?php e(t('Active')); ?>",
inactive: "<?php e(t('Inactive')); ?>"
};
const buildActionUrl = (action, id) => buildUrl(appBase, `admin/roles/${action}/${id}`);
const initRolesGrid = () => {
@@ -67,6 +76,18 @@ $canDeleteRoles = can('roles.delete');
return gridjs.html(`${label}`);
}
},
{
name: "<?php e(t('Code')); ?>",
sort: true,
formatter: (cell) => gridjs.html(String(cell ?? ''))
},
{
name: "<?php e(t('Status')); ?>",
sort: true,
formatter: (cell) => cell
? gridjs.html(`<span class="badge" data-variant="success">${labels.active}</span>`)
: gridjs.html(`<span class="badge" data-variant="danger">${labels.inactive}</span>`)
},
{
name: "<?php e(t('Created')); ?>",
sort: true,
@@ -86,7 +107,7 @@ $canDeleteRoles = can('roles.delete');
hidden: true
}
],
sortColumns: ['description', 'created', 'modified', null, null],
sortColumns: ['description', 'code', 'active', 'created', 'modified', null, null],
paginationLimit: 10,
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
mapData: (data) => data.data.map((row) => [
@@ -94,6 +115,8 @@ $canDeleteRoles = can('roles.delete');
label: row.description,
is_default: row.is_default ? 1 : 0
},
row.code,
row.active,
row.created,
row.modified,
row.uuid,
@@ -102,7 +125,7 @@ $canDeleteRoles = can('roles.delete');
actions: {
enabled: true,
label: "<?php e(t('Actions')); ?>",
getRowId: (row) => row?.cells?.[3]?.data,
getRowId: (row) => row?.cells?.[5]?.data,
items: [
{
key: 'edit',
@@ -111,7 +134,7 @@ $canDeleteRoles = can('roles.delete');
href: ({ id }) => buildActionUrl('edit', id)
},
<?php if ($canDeleteRoles): ?>
{
{
key: 'delete',
label: "<i class=\"bi bi-trash3-fill\"></i>",
className: 'danger outline',
@@ -135,13 +158,21 @@ $canDeleteRoles = can('roles.delete');
param: 'search',
debounce: 250
},
filters: [
{
input: '#role-status-filter',
param: 'active',
default: 'all',
normalize: (value) => (value && value !== 'all' ? value : '')
}
],
urlSync: true,
rowDataset: (row) => ({
uuid: row?.cells?.[3]?.data
uuid: row?.cells?.[5]?.data
}),
rowDblClick: {
getUrl: (rowData) => {
const uuid = rowData?.cells?.[3]?.data;
const uuid = rowData?.cells?.[5]?.data;
return uuid ? new URL(`admin/roles/edit/${uuid}`, appBase).toString() : '';
}
}

View File

@@ -3,10 +3,10 @@
use MintyPHP\Router;
use MintyPHP\Support\Guard;
use MintyPHP\Support\SearchConfig;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\DB;
use MintyPHP\I18n;
use MintyPHP\Repository\UserTenantRepository;
use MintyPHP\Repository\Tenant\UserTenantRepository;
Guard::requireLogin();

View File

@@ -0,0 +1,18 @@
<?php
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Service\Auth\RememberMeService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
Router::redirect('admin/settings');
}
$count = RememberMeService::expireAllTokensByAdmin();
Flash::success(sprintf(t('%d login tokens expired'), $count), 'admin/settings', 'login_tokens_expired');
Router::redirect('admin/settings');

View File

@@ -1,10 +1,10 @@
<?php
use MintyPHP\Router;
use MintyPHP\Service\BrandingFaviconService;
use MintyPHP\Service\Branding\BrandingFaviconService;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);

View File

@@ -1,10 +1,10 @@
<?php
use MintyPHP\Router;
use MintyPHP\Service\BrandingFaviconService;
use MintyPHP\Service\Branding\BrandingFaviconService;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);

View File

@@ -4,17 +4,18 @@ use MintyPHP\Buffer;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Router;
use MintyPHP\Service\SettingService;
use MintyPHP\Service\TenantService;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\DepartmentService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Settings\SettingService;
use MintyPHP\Service\Tenant\TenantService;
use MintyPHP\Service\Access\RoleService;
use MintyPHP\Service\Org\DepartmentService;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\Repository\Auth\RememberTokenRepository;
Guard::requireLogin();
Guard::requirePermission(PermissionService::SETTINGS_VIEW);
$tenants = TenantService::list();
$roles = RoleService::list();
$roles = RoleService::listActive();
$departments = DepartmentService::list();
$sortByDescription = static function ($a, $b) {
@@ -32,6 +33,7 @@ $values = [
'app_locale' => SettingService::getAppLocale(),
'app_theme' => SettingService::getAppTheme(),
'app_theme_user' => SettingService::isUserThemeAllowed(),
'app_registration' => SettingService::isRegistrationEnabled(),
'app_primary_color' => SettingService::getAppPrimaryColor(),
'smtp_host' => SettingService::getSmtpHost(),
'smtp_port' => SettingService::getSmtpPort(),
@@ -40,6 +42,7 @@ $values = [
'smtp_from' => SettingService::getSmtpFrom(),
'smtp_from_name' => SettingService::getSmtpFromName(),
];
$activeLoginTokens = RememberTokenRepository::countActive();
$settings = [
'default_tenant' => SettingService::get(SettingService::DEFAULT_TENANT_KEY),
'default_role' => SettingService::get(SettingService::DEFAULT_ROLE_KEY),
@@ -48,6 +51,7 @@ $settings = [
'app_locale' => SettingService::get(SettingService::APP_LOCALE_KEY),
'app_theme' => SettingService::get(SettingService::APP_THEME_KEY),
'app_theme_user' => SettingService::get(SettingService::APP_THEME_USER_KEY),
'app_registration' => SettingService::get(SettingService::APP_REGISTRATION_KEY),
'app_primary_color' => SettingService::get(SettingService::APP_PRIMARY_COLOR_KEY),
'smtp_host' => SettingService::get(SettingService::SMTP_HOST_KEY),
'smtp_port' => SettingService::get(SettingService::SMTP_PORT_KEY),
@@ -70,6 +74,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$appLocale = trim((string) ($_POST['app_locale'] ?? ''));
$appTheme = trim((string) ($_POST['app_theme'] ?? ''));
$appThemeUser = isset($_POST['app_theme_user']);
$appRegistration = isset($_POST['app_registration']);
$rawPrimaryColor = $_POST['app_primary_color'] ?? '';
if (is_array($rawPrimaryColor)) {
$rawPrimaryColor = '';
@@ -93,6 +98,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
SettingService::setAppLocale($appLocale);
SettingService::setAppTheme($appTheme);
SettingService::setUserThemeAllowed($appThemeUser);
SettingService::setRegistrationEnabled($appRegistration);
$primaryOk = SettingService::setAppPrimaryColor($appPrimaryColor);
if (!$primaryOk) {
$appPrimaryColor = '';
@@ -120,6 +126,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$cacheData['app_locale'] = $appLocale !== '' ? $appLocale : null;
$cacheData['app_theme'] = $appTheme !== '' ? $appTheme : null;
$cacheData['app_theme_user'] = $appThemeUser ? '1' : '0';
$cacheData['app_registration'] = $appRegistration ? '1' : '0';
$cacheData['app_primary_color'] = $appPrimaryColor !== '' ? $appPrimaryColor : null;
file_put_contents($cacheFile, "<?php\nreturn " . var_export($cacheData, true) . ";\n");

View File

@@ -8,8 +8,8 @@
*/
use MintyPHP\Session;
use MintyPHP\Service\BrandingLogoService;
use MintyPHP\Service\BrandingFaviconService;
use MintyPHP\Service\Branding\BrandingLogoService;
use MintyPHP\Service\Branding\BrandingFaviconService;
$values = $values ?? [];
$settings = $settings ?? [];
@@ -20,13 +20,16 @@ $appTitle = (string) ($values['app_title'] ?? '');
$appLocale = (string) ($values['app_locale'] ?? '');
$appTheme = (string) ($values['app_theme'] ?? '');
$appThemeUser = !empty($values['app_theme_user']);
$appRegistration = !empty($values['app_registration']);
$appPrimaryColor = (string) ($values['app_primary_color'] ?? '');
$themes = appThemes();
$smtpHost = (string) ($values['smtp_host'] ?? '');
$smtpPort = (string) ($values['smtp_port'] ?? '');
$smtpUser = (string) ($values['smtp_user'] ?? '');
$smtpSecure = (string) ($values['smtp_secure'] ?? '');
$smtpFrom = (string) ($values['smtp_from'] ?? '');
$smtpFromName = (string) ($values['smtp_from_name'] ?? '');
$activeLoginTokens = (int) ($activeLoginTokens ?? 0);
$defaultTenantDesc = $settings['default_tenant']['description'] ?? 'setting.default_tenant';
$defaultRoleDesc = $settings['default_role']['description'] ?? 'setting.default_role';
$defaultDepartmentDesc = $settings['default_department']['description'] ?? 'setting.default_department';
@@ -34,6 +37,7 @@ $appTitleDesc = $settings['app_title']['description'] ?? 'setting.app_title';
$appLocaleDesc = $settings['app_locale']['description'] ?? 'setting.app_locale';
$appThemeDesc = $settings['app_theme']['description'] ?? 'setting.app_theme';
$appThemeUserDesc = $settings['app_theme_user']['description'] ?? 'setting.app_theme_user';
$appRegistrationDesc = $settings['app_registration']['description'] ?? 'setting.app_registration';
$appPrimaryColorDesc = $settings['app_primary_color']['description'] ?? 'setting.app_primary_color';
$smtpHostDesc = $settings['smtp_host']['description'] ?? 'setting.smtp_host';
$smtpPortDesc = $settings['smtp_port']['description'] ?? 'setting.smtp_port';
@@ -53,9 +57,13 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
<div class="app-details-container">
<section>
<div class="app-breadcrumb">
<a href="/admin">Startseite</a><i class="bi bi-chevron-right"></i><?php e(t('Settings')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Settings')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-details-titlebar">
<h1>
<?php e(t('Settings')); ?>
@@ -79,6 +87,18 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
<input type="text" name="app_title" value="<?php e($appTitle); ?>" placeholder="<?php e(APP_NAME); ?>" <?php e($readonlyAttr); ?>>
<small><?php e(t($appTitleDesc)); ?></small>
</label>
<fieldset>
<legend>
<small>
<?php e(t($appRegistrationDesc)); ?>
</small>
</legend>
<label class="app-field">
<input type="checkbox" name="app_registration" value="1" <?php e($appRegistration ? 'checked' : ''); ?>
<?php e($disabledAttr); ?>>
<span><?php e(t('Allow registration')); ?></span>
</label>
</fieldset>
</details>
<hr>
<details name="appearance">
@@ -89,12 +109,11 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
<legend><small><?php e(t('Default theme')); ?></small></legend>
<select name="app_theme" <?php e($disabledAttr); ?>>
<option value=""><?php e(t('None')); ?></option>
<option value="light" <?php e($appTheme === 'light' ? 'selected' : ''); ?>>
<?php e(t('Light')); ?>
</option>
<option value="dark" <?php e($appTheme === 'dark' ? 'selected' : ''); ?>>
<?php e(t('Dark')); ?>
</option>
<?php foreach ($themes as $key => $label): ?>
<option value="<?php e($key); ?>" <?php e($appTheme === $key ? 'selected' : ''); ?>>
<?php e(t($label)); ?>
</option>
<?php endforeach; ?>
</select>
<small><?php e(t($appThemeDesc)); ?></small>
</fieldset>
@@ -271,6 +290,31 @@ $disabledAttr = $isReadOnly ? 'disabled' : '';
<hr>
<?php Session::getCsrfInput(); ?>
</form>
<?php if ($canUpdateSettings): ?>
<div>
<details name="danger-zone">
<summary><?php e(t('Danger zone')); ?></summary>
<hr>
<fieldset>
<legend><small><?php e(t('Login tokens')); ?></small></legend>
<p class="muted">
<?php e(t('This will mark all remember-me tokens as expired. Users will need to sign in again.')); ?>
</p>
<p class="muted">
<?php e(sprintf(t('%d active login tokens'), $activeLoginTokens)); ?>
</p>
<form method="post" action="admin/settings/expire-remember-tokens"
onsubmit="return confirm('<?php e(t('Expire all login tokens?')); ?>');">
<button type="submit" class="danger">
<?php e(t('Expire all login tokens')); ?>
</button>
<?php Session::getCsrfInput(); ?>
</form>
</fieldset>
</details>
<hr>
</div>
<?php endif; ?>
</section>
<aside id="app-details-aside-section">
<div class="app-details-aside-section">

View File

@@ -1,10 +1,10 @@
<?php
use MintyPHP\Router;
use MintyPHP\Service\BrandingLogoService;
use MintyPHP\Service\Branding\BrandingLogoService;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);

View File

@@ -1,10 +1,10 @@
<?php
use MintyPHP\Router;
use MintyPHP\Service\BrandingLogoService;
use MintyPHP\Service\Branding\BrandingLogoService;
use MintyPHP\Support\Flash;
use MintyPHP\Support\Guard;
use MintyPHP\Service\PermissionService;
use MintyPHP\Service\Access\PermissionService;
Guard::requireLogin();
Guard::requirePermission(PermissionService::SETTINGS_UPDATE);

View File

@@ -2,165 +2,66 @@
use MintyPHP\Buffer;
use MintyPHP\Support\Guard;
use MintyPHP\Service\TenantService;
use MintyPHP\Service\DepartmentService;
use MintyPHP\Service\RoleService;
use MintyPHP\Service\UserService;
use MintyPHP\Service\PermissionService;
use MintyPHP\Repository\PermissionRepository;
use MintyPHP\Repository\MailLogRepository;
use MintyPHP\Service\Access\PermissionService;
use MintyPHP\DB;
Guard::requirePermissionOrForbidden(PermissionService::STATS_VIEW);
// Basic counts
$tenants = TenantService::list();
$tenantCount = count($tenants);
$activeUserCount = (int) (DB::selectValue('select count(*) from users where active = 1') ?? 0);
$inactiveUserCount = (int) (DB::selectValue('select count(*) from users where active = 0') ?? 0);
$activeTenantCount = (int) (DB::selectValue("select count(*) from tenants where status = 'active'") ?? 0);
$inactiveTenantCount = (int) (DB::selectValue("select count(*) from tenants where status = 'inactive'") ?? 0);
$activeDepartmentCount = (int) (DB::selectValue('select count(*) from departments where active = 1') ?? 0);
$inactiveDepartmentCount = (int) (DB::selectValue('select count(*) from departments where active = 0') ?? 0);
$activeRoleCount = (int) (DB::selectValue('select count(*) from roles where active = 1') ?? 0);
$inactiveRoleCount = (int) (DB::selectValue('select count(*) from roles where active = 0') ?? 0);
$departments = DepartmentService::list();
$departmentCount = count($departments);
$rolesWithoutPermissionsCount = (int) (DB::selectValue(
'select count(*) from roles r left join role_permissions rp on rp.role_id = r.id where r.active = 1 and rp.role_id is null'
) ?? 0);
$permissionsWithoutRolesCount = (int) (DB::selectValue(
'select count(*) from permissions p left join role_permissions rp on rp.permission_id = p.id where p.active = 1 and rp.permission_id is null'
) ?? 0);
$usersWithoutRolesCount = (int) (DB::selectValue(
'select count(*) from users u left join user_roles ur on ur.user_id = u.id where u.active = 1 and ur.user_id is null'
) ?? 0);
$usersWithoutTenantsCount = (int) (DB::selectValue(
'select count(*) from users u left join user_tenants ut on ut.user_id = u.id where u.active = 1 and ut.user_id is null'
) ?? 0);
$departmentsWithoutTenantsCount = (int) (DB::selectValue(
'select count(*) from departments d left join tenant_departments td on td.department_id = d.id where d.active = 1 and td.department_id is null'
) ?? 0);
$roles = RoleService::list();
$roleCount = count($roles);
$usersUnverifiedCount = (int) (DB::selectValue(
'select count(*) from users where active = 1 and email_verified_at is null'
) ?? 0);
$usersNeverLoggedCount = (int) (DB::selectValue(
'select count(*) from users where active = 1 and last_login_at is null'
) ?? 0);
$recentLogins = DB::select(
'select id, uuid, first_name, last_name, email, last_login_at from users where active = 1 and last_login_at is not null order by last_login_at desc limit 10'
);
$recentLogins = is_array($recentLogins) ? array_map(
static fn ($row) => $row['users'] ?? $row,
$recentLogins
) : [];
$neverLoggedUsers = DB::select(
'select id, uuid, first_name, last_name, email, created from users where active = 1 and last_login_at is null order by created desc limit 10'
);
$neverLoggedUsers = is_array($neverLoggedUsers) ? array_map(
static fn ($row) => $row['users'] ?? $row,
$neverLoggedUsers
) : [];
$users = UserService::list();
$userCount = count($users);
$permissions = PermissionRepository::list();
$permissionCount = count($permissions);
// User stats (active/inactive)
$activeUserCount = 0;
$inactiveUserCount = 0;
foreach ($users as $user) {
$isActive = (int) ($user['active'] ?? 1);
if ($isActive === 1) {
$activeUserCount++;
} else {
$inactiveUserCount++;
}
}
// Organization breakdown (per tenant)
$tenantBreakdown = [];
// Load user-tenant assignments
$userTenantAssignments = [];
$userTenantRows = DB::select('select user_id, tenant_id from user_tenants');
if (is_array($userTenantRows)) {
foreach ($userTenantRows as $row) {
$data = $row['user_tenants'] ?? $row;
$userId = (int) ($data['user_id'] ?? 0);
$tenantId = (int) ($data['tenant_id'] ?? 0);
if ($userId > 0 && $tenantId > 0) {
if (!isset($userTenantAssignments[$userId])) {
$userTenantAssignments[$userId] = [];
}
$userTenantAssignments[$userId][] = $tenantId;
}
}
}
// Load tenant-department assignments
$tenantDepartmentAssignments = [];
$tenantDepartmentRows = DB::select('select tenant_id, department_id from tenant_departments');
if (is_array($tenantDepartmentRows)) {
foreach ($tenantDepartmentRows as $row) {
$data = $row['tenant_departments'] ?? $row;
$tenantId = (int) ($data['tenant_id'] ?? 0);
$departmentId = (int) ($data['department_id'] ?? 0);
if ($tenantId > 0 && $departmentId > 0) {
if (!isset($tenantDepartmentAssignments[$tenantId])) {
$tenantDepartmentAssignments[$tenantId] = [];
}
$tenantDepartmentAssignments[$tenantId][] = $departmentId;
}
}
}
foreach ($tenants as $tenant) {
$tenantId = (int) ($tenant['id'] ?? 0);
if ($tenantId === 0) {
continue;
}
// Count departments for this tenant
$deptCount = 0;
$tenantDepartmentIds = $tenantDepartmentAssignments[$tenantId] ?? [];
foreach ($departments as $dept) {
$deptId = (int) ($dept['id'] ?? 0);
if ($deptId > 0 && in_array($deptId, $tenantDepartmentIds, true)) {
$deptCount++;
}
}
// Count users for this tenant
$totalUsers = 0;
$activeUsers = 0;
$inactiveUsers = 0;
foreach ($users as $user) {
$userId = (int) ($user['id'] ?? 0);
if ($userId === 0) {
continue;
}
// Check if user is assigned to this tenant
$userTenantIds = $userTenantAssignments[$userId] ?? [];
if (in_array($tenantId, $userTenantIds, true)) {
$totalUsers++;
$isActive = (int) ($user['active'] ?? 1);
if ($isActive === 1) {
$activeUsers++;
} else {
$inactiveUsers++;
}
}
}
$tenantBreakdown[] = [
'id' => $tenantId,
'uuid' => $tenant['uuid'] ?? '',
'description' => $tenant['description'] ?? '',
'departments' => $deptCount,
'total_users' => $totalUsers,
'active_users' => $activeUsers,
'inactive_users' => $inactiveUsers,
];
}
// Sort by description
usort($tenantBreakdown, function ($a, $b) {
return strcmp((string) ($a['description'] ?? ''), (string) ($b['description'] ?? ''));
});
// Mail log stats
$mailLogTotalCount = DB::selectValue('select count(*) from mail_log');
$mailLogTotalCount = $mailLogTotalCount ? (int) $mailLogTotalCount : 0;
$mailLogSentCount = DB::selectValue("select count(*) from mail_log where status = 'sent'");
$mailLogSentCount = $mailLogSentCount ? (int) $mailLogSentCount : 0;
$mailLogFailedCount = DB::selectValue("select count(*) from mail_log where status = 'failed'");
$mailLogFailedCount = $mailLogFailedCount ? (int) $mailLogFailedCount : 0;
$mailLogQueuedCount = DB::selectValue("select count(*) from mail_log where status = 'queued'");
$mailLogQueuedCount = $mailLogQueuedCount ? (int) $mailLogQueuedCount : 0;
// Mail log stats (last 24h)
$mailLogTotal24hCount = DB::selectValue("select count(*) from mail_log where created_at >= NOW() - INTERVAL 24 HOUR");
$mailLogTotal24hCount = $mailLogTotal24hCount ? (int) $mailLogTotal24hCount : 0;
$mailLogSent24hCount = DB::selectValue("select count(*) from mail_log where status = 'sent' and created_at >= NOW() - INTERVAL 24 HOUR");
$mailLogSent24hCount = $mailLogSent24hCount ? (int) $mailLogSent24hCount : 0;
$mailLogFailed24hCount = DB::selectValue("select count(*) from mail_log where status = 'failed' and created_at >= NOW() - INTERVAL 24 HOUR");
$mailLogFailed24hCount = $mailLogFailed24hCount ? (int) $mailLogFailed24hCount : 0;
$mailLogQueued24hCount = DB::selectValue("select count(*) from mail_log where status = 'queued' and created_at >= NOW() - INTERVAL 24 HOUR");
$mailLogQueued24hCount = $mailLogQueued24hCount ? (int) $mailLogQueued24hCount : 0;
// Calculate date range for last 24 hours
$date24hAgo = gmdate('Y-m-d', strtotime('-24 hours'));
$dateToday = gmdate('Y-m-d');
$mailLogFailedCount = (int) (DB::selectValue("select count(*) from mail_log where status = 'failed'") ?? 0);
$mailLogSentCount = (int) (DB::selectValue("select count(*) from mail_log where status = 'sent'") ?? 0);
$mailLogLastSentAt = (string) (DB::selectValue("select max(sent_at) from mail_log where status = 'sent'") ?? '');
$mailLogRecentFailed = DB::select(
"select id, to_email, subject, error_message, created_at from mail_log where status = 'failed' order by created_at desc limit 5"
);
$mailLogRecentFailed = is_array($mailLogRecentFailed) ? array_map(
static fn ($row) => $row['mail_log'] ?? $row,
$mailLogRecentFailed
) : [];
Buffer::set('title', t('Statistics'));

View File

@@ -1,28 +1,33 @@
<?php
/**
* @var int $tenantCount
* @var int $departmentCount
* @var int $roleCount
* @var int $userCount
* @var int $permissionCount
* @var int $activeUserCount
* @var int $inactiveUserCount
* @var array $tenantBreakdown
* @var int $mailLogTotalCount
* @var int $activeTenantCount
* @var int $inactiveTenantCount
* @var int $activeDepartmentCount
* @var int $inactiveDepartmentCount
* @var int $activeRoleCount
* @var int $inactiveRoleCount
* @var int $rolesWithoutPermissionsCount
* @var int $permissionsWithoutRolesCount
* @var int $usersWithoutRolesCount
* @var int $usersWithoutTenantsCount
* @var int $departmentsWithoutTenantsCount
* @var int $mailLogSentCount
* @var int $mailLogFailedCount
* @var int $mailLogQueuedCount
* @var int $mailLogTotal24hCount
* @var int $mailLogSent24hCount
* @var int $mailLogFailed24hCount
* @var int $mailLogQueued24hCount
* @var string $mailLogLastSentAt
* @var array $mailLogRecentFailed
*/
?>
<div class="app-breadcrumb">
<a href="/admin"><?php e(t('Home')); ?></a><i class="bi bi-chevron-right"></i><?php e(t('Statistics')); ?>
</div>
<?php
$breadcrumbs = [
['label' => t('Home'), 'path' => 'admin'],
['label' => t('Statistics')],
];
require templatePath('partials/app-breadcrumb.phtml');
?>
<div class="app-dashboard-titlebar">
<h1><?php e(t('Statistics')); ?></h1>
<div class="app-dashboard-titlebar-actions">
@@ -34,250 +39,370 @@
<div class="app-tabs" data-tabs id="stats-tabs">
<!-- Tab Navigation -->
<div class="app-tabs-nav">
<button data-tab="organization" data-tab-default class="transparent tab-button">
<?php e(t('Organization')); ?>
<button data-tab="users" data-tab-default class="transparent tab-button">
<?php e(t('Users')); ?>
</button>
<button data-tab="users-roles" class="transparent tab-button">
<?php e(t('Users & roles')); ?>
<button data-tab="tenants" class="transparent tab-button">
<?php e(t('Tenants')); ?>
</button>
<button data-tab="departments" class="transparent tab-button">
<?php e(t('Departments')); ?>
</button>
<button data-tab="roles-permissions" class="transparent tab-button">
<?php e(t('Roles & permissions')); ?>
</button>
<button data-tab="security" class="transparent tab-button">
<?php e(t('Security')); ?>
</button>
<?php if (can('mail_log.view')): ?>
<button data-tab="system" class="transparent tab-button">
<?php e(t('System')); ?>
<button data-tab="email-security" class="transparent tab-button">
<?php e(t('Email security')); ?>
</button>
<?php endif; ?>
</div>
<!-- Tab Panel: Organization -->
<div data-tab-panel="organization">
<!-- Tab Panel: Users -->
<div data-tab-panel="users">
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users?active=active',
'label' => t('Active users'),
'count' => (string) $activeUserCount,
'icon' => 'bi bi-person-check-fill',
'iconBg' => '#d9f2e6',
'iconColor' => '#1f6a3a',
'tooltip' => t('Active users'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users?active=inactive',
'label' => t('Inactive users'),
'count' => (string) $inactiveUserCount,
'icon' => 'bi bi-person-x-fill',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Inactive users'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users?login_status=never',
'label' => t('Never logged in'),
'count' => (string) $usersNeverLoggedCount,
'icon' => 'bi bi-person-slash',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
'tooltip' => t('Never logged in'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users?email_verified=unverified',
'label' => t('Email unverified'),
'count' => (string) $usersUnverifiedCount,
'icon' => 'bi bi-shield-exclamation',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
'tooltip' => t('Email unverified'),
]);
?>
</div>
<?php $canViewUsers = can('users.view'); ?>
<div class="app-stats-table">
<div class="app-stats-table-header">
<small><?php e(t('Recent logins')); ?></small>
</div>
<?php if (!empty($recentLogins)): ?>
<table>
<thead>
<tr>
<th><?php e(t('User')); ?></th>
<th><?php e(t('Email')); ?></th>
<th><?php e(t('Last login')); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($recentLogins as $row): ?>
<tr>
<td>
<?php $name = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? '')); ?>
<?php if ($canViewUsers && !empty($row['uuid'])): ?>
<a href="admin/users/edit/<?php e($row['uuid']); ?>"><?php e($name); ?></a>
<?php else: ?>
<?php e($name); ?>
<?php endif; ?>
</td>
<td><?php e($row['email'] ?? ''); ?></td>
<td><?php e(dt($row['last_login_at'] ?? '')); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php endif; ?>
</div>
<div class="app-stats-table">
<div class="app-stats-table-header">
<small><?php e(t('Never logged in')); ?></small>
</div>
<?php if (!empty($neverLoggedUsers)): ?>
<table>
<thead>
<tr>
<th><?php e(t('User')); ?></th>
<th><?php e(t('Email')); ?></th>
<th><?php e(t('Created')); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($neverLoggedUsers as $row): ?>
<tr>
<td>
<?php $name = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? '')); ?>
<?php if ($canViewUsers && !empty($row['uuid'])): ?>
<a href="admin/users/edit/<?php e($row['uuid']); ?>"><?php e($name); ?></a>
<?php else: ?>
<?php e($name); ?>
<?php endif; ?>
</td>
<td><?php e($row['email'] ?? ''); ?></td>
<td><?php e(dt($row['created'] ?? '')); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<small class="muted"><?php e(t('No entries found')); ?></small>
<?php endif; ?>
</div>
</div>
<!-- Tab Panel: Tenants -->
<div data-tab-panel="tenants">
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/tenants',
'label' => t('Tenants'),
'count' => (string) $tenantCount,
'icon' => 'bi bi-building',
'label' => t('Active tenants'),
'count' => (string) $activeTenantCount,
'icon' => 'bi bi-buildings',
'iconBg' => '#e9f0ff',
'iconColor' => '#264db3',
'tooltip' => t('Active tenants'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/tenants',
'label' => t('Inactive tenants'),
'count' => (string) $inactiveTenantCount,
'icon' => 'bi bi-building-x',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Inactive tenants'),
]);
?>
</div>
</div>
<!-- Tab Panel: Departments -->
<div data-tab-panel="departments">
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/departments?active=active',
'label' => t('Active departments'),
'count' => (string) $activeDepartmentCount,
'icon' => 'bi bi-diagram-3-fill',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
'tooltip' => t('Active departments'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/departments?active=inactive',
'label' => t('Inactive departments'),
'count' => (string) $inactiveDepartmentCount,
'icon' => 'bi bi-diagram-3',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Inactive departments'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/departments',
'label' => t('Departments'),
'count' => (string) $departmentCount,
'icon' => 'bi bi-diagram-3-fill',
'label' => t('Departments without tenants'),
'count' => (string) $departmentsWithoutTenantsCount,
'icon' => 'bi bi-diagram-3',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
'tooltip' => t('Departments without tenants'),
]);
?>
</div>
</div>
<!-- Tab Panel: Roles & permissions -->
<div data-tab-panel="roles-permissions">
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/roles?active=active',
'label' => t('Active roles'),
'count' => (string) $activeRoleCount,
'icon' => 'bi bi-shield-check',
'iconBg' => '#d9f2e6',
'iconColor' => '#1f6a3a',
'tooltip' => t('Active roles'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/roles?active=inactive',
'label' => t('Inactive roles'),
'count' => (string) $inactiveRoleCount,
'icon' => 'bi bi-shield-x',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Inactive roles'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/roles',
'label' => t('Roles without permissions'),
'count' => (string) $rolesWithoutPermissionsCount,
'icon' => 'bi bi-shield-exclamation',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
'tooltip' => t('Roles without permissions'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/permissions',
'label' => t('Permissions without roles'),
'count' => (string) $permissionsWithoutRolesCount,
'icon' => 'bi bi-key-fill',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Permissions without roles'),
]);
?>
</div>
</div>
<!-- Tab Panel: Security -->
<div data-tab-panel="security">
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users?active=inactive',
'label' => t('Inactive users'),
'count' => (string) $inactiveUserCount,
'icon' => 'bi bi-person-x-fill',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Inactive users'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users',
'label' => t('Users'),
'count' => (string) $userCount,
'icon' => 'bi bi-people-fill',
'iconBg' => '#d9f2e6',
'iconColor' => '#1f6a3a',
'label' => t('Users without roles'),
'count' => (string) $usersWithoutRolesCount,
'icon' => 'bi bi-person-dash',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
'tooltip' => t('Users without roles'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users',
'label' => t('Users without tenants'),
'count' => (string) $usersWithoutTenantsCount,
'icon' => 'bi bi-person-slash',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Users without tenants'),
]);
?>
</div>
<?php if (!empty($tenantBreakdown)): ?>
<div class="app-stats-table">
<table>
<thead>
<tr>
<th>
<?php e(t('Tenant')); ?>
</th>
<th>
<?php e(t('Departments')); ?>
</th>
<th>
<?php e(t('Assigned users (Active/Inactive)')); ?>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($tenantBreakdown as $item): ?>
<tr>
<td>
<?php if (can('tenants.view') && !empty($item['uuid'])): ?>
<a href="admin/tenants/edit/<?php e($item['uuid']); ?>">
<?php e($item['description']); ?>
</a>
<?php else: ?>
<?php e($item['description']); ?>
<?php endif; ?>
</td>
<td>
<?php e($item['departments']); ?>
</td>
<td>
<?php e($item['total_users']); ?> <small>(
<?php e($item['active_users']); ?> /
<?php e($item['inactive_users']); ?>)
</small>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Tab Panel: Users & Roles -->
<div data-tab-panel="users-roles">
<div class="grid">
<div>
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users',
'label' => t('Active users'),
'count' => (string) $activeUserCount,
'icon' => 'bi bi-person-check-fill',
'iconBg' => '#d9f2e6',
'iconColor' => '#1f6a3a',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/users',
'label' => t('Inactive users'),
'count' => (string) $inactiveUserCount,
'icon' => 'bi bi-person-x-fill',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
]);
?>
</div>
</div>
<div>
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/roles',
'label' => t('Roles'),
'count' => (string) $roleCount,
'icon' => 'bi bi-shield-lock-fill',
'iconBg' => '#f3e9ff',
'iconColor' => '#5b2c83',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/permissions',
'label' => t('Permissions'),
'count' => (string) $permissionCount,
'icon' => 'bi bi-key-fill',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
]);
?>
</div>
</div>
</div>
</div>
<!-- Tab Panel: System -->
<!-- Tab Panel: Email security -->
<?php if (can('mail_log.view')): ?>
<div data-tab-panel="system">
<div class="grid">
<div>
<h4><?php e(t('Mail logs')); ?></h4>
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log',
'label' => t('Total emails'),
'count' => (string) $mailLogTotalCount,
'icon' => 'bi bi-envelope-fill',
'iconBg' => '#e9f0ff',
'iconColor' => '#264db3',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=sent',
'label' => t('Sent emails'),
'count' => (string) $mailLogSentCount,
'icon' => 'bi bi-envelope-check-fill',
'iconBg' => '#d9f2e6',
'iconColor' => '#1f6a3a',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=failed',
'label' => t('Failed emails'),
'count' => (string) $mailLogFailedCount,
'icon' => 'bi bi-envelope-x-fill',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log',
'label' => t('Queued emails'),
'count' => (string) $mailLogQueuedCount,
'icon' => 'bi bi-envelope-paper-fill',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
]);
?>
</div>
</div>
<div>
<h4>
<?php e(t('Last 24 hours')); ?>
</h4>
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
'label' => t('Total (24h)'),
'count' => (string) $mailLogTotal24hCount,
'icon' => 'bi bi-envelope-fill',
'iconBg' => '#e9f0ff',
'iconColor' => '#264db3',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=sent&created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
'label' => t('Sent (24h)'),
'count' => (string) $mailLogSent24hCount,
'icon' => 'bi bi-envelope-check-fill',
'iconBg' => '#d9f2e6',
'iconColor' => '#1f6a3a',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=failed&created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
'label' => t('Failed (24h)'),
'count' => (string) $mailLogFailed24hCount,
'icon' => 'bi bi-envelope-x-fill',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=queued&created_from=' . urlencode($date24hAgo ?? '') . '&created_to=' . urlencode($dateToday ?? ''),
'label' => t('Queued (24h)'),
'count' => (string) $mailLogQueued24hCount,
'icon' => 'bi bi-envelope-paper-fill',
'iconBg' => '#fff2d9',
'iconColor' => '#9a5a00',
]);
?>
</div>
</div>
<div data-tab-panel="email-security">
<div class="app-tiles">
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=failed',
'label' => t('Mail failures'),
'count' => (string) $mailLogFailedCount,
'icon' => 'bi bi-envelope-x-fill',
'iconBg' => '#ffe9e9',
'iconColor' => '#a32e2e',
'tooltip' => t('Mail failures'),
]);
?>
<?php
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=sent',
'label' => t('Sent emails'),
'count' => (string) $mailLogSentCount,
'icon' => 'bi bi-envelope-check-fill',
'iconBg' => '#d9f2e6',
'iconColor' => '#1f6a3a',
'tooltip' => t('Sent emails'),
]);
?>
<?php
$lastSentLabel = $mailLogLastSentAt ? dt($mailLogLastSentAt) : t('Never');
MintyPHP\Support\Tile::render([
'href' => 'admin/mail-log?status=sent',
'label' => t('Last email sent'),
'count' => $lastSentLabel,
'icon' => 'bi bi-clock-history',
'iconBg' => '#e9f0ff',
'iconColor' => '#264db3',
'tooltip' => t('Last email sent'),
]);
?>
</div>
<?php if (!empty($mailLogRecentFailed)): ?>
<div class="app-stats-table">
<div class="app-stats-table-header">
<small><?php e(t('Mail failures')); ?></small>
</div>
<table>
<thead>
<tr>
<th><?php e(t('Recipient')); ?></th>
<th><?php e(t('Subject')); ?></th>
<th><?php e(t('Error')); ?></th>
<th><?php e(t('Created')); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($mailLogRecentFailed as $row): ?>
<tr>
<td><?php e($row['to_email'] ?? ''); ?></td>
<td><?php e($row['subject'] ?? ''); ?></td>
<td><?php e($row['error_message'] ?? ''); ?></td>
<td><?php e(dt($row['created_at'] ?? '')); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>

View File

@@ -24,6 +24,7 @@ $useDefaultPrimaryColor = $primaryColor === '';
<div class="app-tabs" data-tabs data-tabs-param="tab" data-tabs-storage-key="admin-tenant-form">
<div class="app-tabs-nav">
<button type="button" data-tab="basic" data-tab-default><?php e(t('Master data')); ?></button>
<button type="button" data-tab="status"><?php e(t('Status')); ?></button>
<button type="button" data-tab="accounting"><?php e(t('Accounting info')); ?></button>
<button type="button" data-tab="communication"><?php e(t('Communication')); ?></button>
<button type="button" data-tab="support"><?php e(t('Support')); ?></button>
@@ -32,23 +33,13 @@ $useDefaultPrimaryColor = $primaryColor === '';
</div>
<div data-tab-panel="basic">
<label for="description">
<span><?php e(t('Description')); ?></span>
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
<label for="status">
<span><?php e(t('Status')); ?></span>
<?php $statusValue = $values['status'] ?? 'active'; ?>
<select name="status" id="status" <?php e($disabledAttr); ?>>
<option value="active" <?php if ($statusValue === 'active') { ?>selected<?php } ?>>
<?php e(t('Active')); ?>
</option>
<option value="inactive" <?php if ($statusValue === 'inactive') { ?>selected<?php } ?>>
<?php e(t('Inactive')); ?>
</option>
</select>
</label>
<div class="grid">
<label for="description">
<span><?php e(t('Description')); ?></span>
<input required type="text" name="description" id="description" value="<?php e($values['description'] ?? ''); ?>"
<?php e($readonlyAttr); ?> />
</label>
</div>
<label for="address">
<span><?php e(t('Address')); ?></span>
<input type="text" name="address" id="address" value="<?php e($values['address'] ?? ''); ?>" <?php e($readonlyAttr); ?> />
@@ -75,6 +66,24 @@ $useDefaultPrimaryColor = $primaryColor === '';
</div>
</div>
<div data-tab-panel="status">
<blockquote data-variant="warning">
<?php e(t('Inactive tenants are excluded from user access and tenant-scoped data.')); ?>
</blockquote>
<label for="status">
<span><?php e(t('Status')); ?></span>
<?php $statusValue = $values['status'] ?? 'active'; ?>
<select name="status" id="status" <?php e($disabledAttr); ?>>
<option value="active" <?php if ($statusValue === 'active') { ?>selected<?php } ?>>
<?php e(t('Active')); ?>
</option>
<option value="inactive" <?php if ($statusValue === 'inactive') { ?>selected<?php } ?>>
<?php e(t('Inactive')); ?>
</option>
</select>
</label>
</div>
<div data-tab-panel="accounting">
<div class="grid">
<label for="vat_id">

Some files were not shown because too many files have changed in this diff Show More