forked from fa/breadcrumb-the-shire
big restructure
This commit is contained in:
@@ -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
7
config/themes.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'light' => 'Light',
|
||||
'dark' => 'Dark',
|
||||
'dark-green' => 'Dark green',
|
||||
];
|
||||
@@ -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',
|
||||
|
||||
@@ -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
24
docs/todo-2fa.md
Normal 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.
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
140
lib/Repository/Access/PermissionRepository.php
Normal file
140
lib/Repository/Access/PermissionRepository.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
@@ -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'];
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository;
|
||||
namespace MintyPHP\Repository\Access;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository;
|
||||
namespace MintyPHP\Repository\Auth;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
102
lib/Repository/Auth/RememberTokenRepository.php
Normal file
102
lib/Repository/Auth/RememberTokenRepository.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository;
|
||||
namespace MintyPHP\Repository\Content;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository;
|
||||
namespace MintyPHP\Repository\Content;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
@@ -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';
|
||||
@@ -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'];
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository;
|
||||
namespace MintyPHP\Repository\Org;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository;
|
||||
namespace MintyPHP\Repository\Settings;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
120
lib/Repository/Support/RepoQuery.php
Normal file
120
lib/Repository/Support/RepoQuery.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Repository;
|
||||
namespace MintyPHP\Repository\Tenant;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service;
|
||||
namespace MintyPHP\Service\Mail;
|
||||
|
||||
use MintyPHP\Repository\MailLogRepository;
|
||||
use MintyPHP\Repository\Mail\MailLogRepository;
|
||||
|
||||
class MailLogService
|
||||
{
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -14,6 +14,8 @@ class Tile
|
||||
'iconBg' => null,
|
||||
'iconColor' => null,
|
||||
'class' => '',
|
||||
'tooltip' => '',
|
||||
'tooltipPos' => 'top',
|
||||
];
|
||||
|
||||
$data = array_merge($defaults, $options);
|
||||
|
||||
@@ -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, '/'));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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] ?? '';
|
||||
|
||||
@@ -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) => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[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>
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'] ?? ''),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(); ?>
|
||||
|
||||
@@ -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'])) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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'])) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)): ?>
|
||||
|
||||
@@ -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'])) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'] ?? ''),
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'])) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
18
pages/admin/settings/expire-remember-tokens().php
Normal file
18
pages/admin/settings/expire-remember-tokens().php
Normal 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');
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user