Files
breadcrumb-the-shire/pages/admin/tenants/edit(default).phtml
fs 6e3fc63c1d feat(tenant): per-theme logos + file-upload + button UI polish
Replace the single tenant avatar with a pair of theme-scoped brand logos.
Render only the theme-matching <img> server-side and swap src on theme
toggle via a JS hook — no reload, no double request, no CSS tricks.

Tenant logos
- TenantLogoService (ImageUploadTrait) with theme whitelist and per-theme
  storage storage/tenants/{uuid}/logo/{light|dark}/, SIZES 128/256/512
- Public serving endpoint auth/tenant-logo-file so login can show the
  logo pre-auth; matching authenticated admin preview endpoint
- appTenantLogoUrl(?size, ?theme) with 4-step fallback cascade; PDF +
  mail always request 'light'
- Admin tenant edit: avatar block replaced by "Tenant logos" details
  block inside the Master-data tab, two side-by-side slots via Pico
  .grid with the core app-file-upload partial
- Policy rename ABILITY_ADMIN_TENANTS_AVATAR_VIEW -> LOGO_VIEW, action
  routes logo / logo-delete / logo-file with theme body/query param
- API endpoint path kept (backward compat), internals on new service
- CLI tenant:logo-migrate-avatars moves legacy avatar/ -> logo/light/
  idempotently (--dry-run, --yes, --cleanup)
- i18n "Tenant image" removed, 12 new keys synced across de/en

File upload component
- Full-width preview + filename/actions below (3D stack layout)
- Fixed 16:9 aspect ratio with 1rem inner padding for consistent
  preview size across any logo aspect
- Transparency checker pattern as background so black logos stay
  visible on dark mode and white logos on light mode
- form="" + deleteFormId support so the partial works with barrier
  forms inside another form

Buttons
- width:100% dropped from button[type="submit"]; scoped back via
  .login-main for the auth-flow primary CTA
- .outline base rule now tints background via color-mix of --app-color
  so secondary/primary/danger outlines all gain a subtle surface
- .outline.secondary restyled Stripe-style in both themes: solid white
  chip with soft shadow in light, solid elevated dark chip with white
  text in dark; neutral border replaces role-colored border
- .app-action-success/.app-action-danger outlines get color-mix bg +
  theme-aware outline-text tokens for stronger contrast
- Filled .primary/.app-action-success/.app-action-danger get raised
  box-shadow (inset highlight + drop) — opt-in via class so chrome
  buttons stay flat
- Dropped the legacy .secondary utility that was clobbering the
  custom-property cascade with a hardcoded muted color

Theme swap
- Logo img carries data-src-light + data-src-dark; theme-toggle JS
  swaps src when data-theme changes, keeping the topbar/login logo in
  sync without a page reload

Quality gates: PHPUnit (2045), PHPStan L5, CS-Fixer, docs link/drift,
codex skills sync — all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:25:53 +02:00

313 lines
12 KiB
PHTML

<?php
/**
* @var array<int, string> $errors
* @var array $form
* @var array $tenant
*/
use MintyPHP\Session;
$values = $form ?? $tenant ?? [];
$pageAuth = is_array($viewAuth['page'] ?? null) ? $viewAuth['page'] : [];
$canUpdateTenant = (bool) ($pageAuth['can_update_tenant'] ?? false);
$canDeleteTenant = (bool) ($pageAuth['can_delete_tenant'] ?? false);
$canManageCustomFields = (bool) ($pageAuth['can_manage_custom_fields'] ?? false);
$canManageSso = (bool) ($pageAuth['can_manage_sso'] ?? false);
$isReadOnly = !$canUpdateTenant;
$titleText = $isReadOnly ? t('View tenant') : t('Edit tenant');
$tenantUuid = (string) ($values['uuid'] ?? '');
$tenantLogoService = app(\MintyPHP\Service\Tenant\TenantLogoService::class);
$tenantFaviconService = app(\MintyPHP\Service\Tenant\TenantFaviconService::class);
$hasLogoLight = $tenantUuid !== '' && $tenantLogoService->hasLogo($tenantUuid, \MintyPHP\Service\Tenant\TenantLogoService::THEME_LIGHT);
$hasLogoDark = $tenantUuid !== '' && $tenantLogoService->hasLogo($tenantUuid, \MintyPHP\Service\Tenant\TenantLogoService::THEME_DARK);
$hasFavicon = $tenantUuid !== '' && $tenantFaviconService->hasFavicon($tenantUuid);
?>
<div class="app-details-container">
<section>
<?php
$titlebar = [
'title' => $titleText,
'backHref' => 'admin/tenants',
'backTitle' => t('Cancel'),
'actions' => $canUpdateTenant ? [
[
'form' => 'tenant-form',
'name' => 'action',
'value' => 'save',
'class' => 'secondary outline',
'label' => t('Save'),
],
[
'form' => 'tenant-form',
'name' => 'action',
'value' => 'save_close',
'class' => 'primary',
'label' => t('Save & close'),
],
] : [],
];
require templatePath('partials/app-details-titlebar.phtml');
?>
<?php
$validationSummaryErrors = $validationSummaryErrors ?? ($errors ?? []);
require templatePath('partials/app-details-validation-summary.phtml');
?>
<?php if ($canUpdateTenant && $tenantUuid !== ''): ?>
<form id="tenant-logo-light-form" method="post"
action="admin/tenants/logo/<?php e($tenantUuid); ?>" enctype="multipart/form-data" hidden>
<input type="hidden" name="theme" value="light">
<?php Session::getCsrfInput(); ?>
</form>
<form id="tenant-logo-dark-form" method="post"
action="admin/tenants/logo/<?php e($tenantUuid); ?>" enctype="multipart/form-data" hidden>
<input type="hidden" name="theme" value="dark">
<?php Session::getCsrfInput(); ?>
</form>
<form id="tenant-logo-light-delete-form" method="post"
action="admin/tenants/logo-delete/<?php e($tenantUuid); ?>" hidden>
<input type="hidden" name="theme" value="light">
<?php Session::getCsrfInput(); ?>
</form>
<form id="tenant-logo-dark-delete-form" method="post"
action="admin/tenants/logo-delete/<?php e($tenantUuid); ?>" hidden>
<input type="hidden" name="theme" value="dark">
<?php Session::getCsrfInput(); ?>
</form>
<?php endif; ?>
<?php
$detailsOpenAll = false;
$isReadOnly = $isReadOnly ?? false;
$showDangerZone = $canDeleteTenant;
$dangerZoneDeleteFormId = $showDangerZone ? 'tenant-delete-form' : '';
$dangerZoneWarning = t('This will permanently delete this tenant.');
$dangerZoneActionLabel = t('Delete tenant');
$showCustomFieldsTab = $canManageCustomFields;
$showSsoTab = $canManageSso;
$customFieldTenantUuid = (string) ($values['uuid'] ?? '');
$customFieldDefinitions = $customFieldDefinitions ?? [];
require __DIR__ . '/_form.phtml';
?>
<?php if ($showDangerZone): ?>
<form
id="tenant-delete-form"
method="post"
action="admin/tenants/delete/<?php e($values['uuid'] ?? ''); ?>"
hidden
data-detail-confirm-message="<?php e(t('Delete this tenant?')); ?>"
data-detail-action-kind="delete"
>
<?php Session::getCsrfInput(); ?>
</form>
<?php endif; ?>
</section>
<aside id="app-details-aside-section">
<div class="app-details-aside-section">
<hgroup>
<h2><?php e($values['description'] ?? ''); ?></h2>
<p><?php e(t('Tenant')); ?></p>
</hgroup>
<hr>
<?php if ($canUpdateTenant): ?>
<details name="tenant-favicon">
<summary>
<?php e(t('Upload favicon')); ?>
</summary>
<hr>
<small><?php e(t('Square images are recommended (icons are center-cropped).')); ?></small>
<hr>
<form class="user-avatar-form" method="post" action="admin/tenants/favicon/<?php e($tenantUuid); ?>"
enctype="multipart/form-data">
<?php
$fileUpload = [
'name' => 'favicon',
'accept' => 'image/png',
'hint' => t('Allowed file types: PNG'),
'currentSrc' => $hasFavicon ? asset('favicon/tenants/' . $tenantUuid . '/favicon/favicon-32x32.png') : '',
'currentLabel' => t('Favicon'),
'deleteAction' => $hasFavicon ? 'admin/tenants/favicon-delete/' . $tenantUuid : '',
];
require templatePath('partials/app-file-upload.phtml');
?>
<button type="submit" class="app-action-success">
<?php e(t('Save')); ?>
</button>
<?php Session::getCsrfInput(); ?>
</form>
</details>
<?php endif; ?>
<hr>
<?php
$phone = trim((string) ($values['phone'] ?? ''));
$fax = trim((string) ($values['fax'] ?? ''));
$email = trim((string) ($values['email'] ?? ''));
$supportEmail = trim((string) ($values['support_email'] ?? ''));
$supportPhone = trim((string) ($values['support_phone'] ?? ''));
$billingEmail = trim((string) ($values['billing_email'] ?? ''));
$website = trim((string) ($values['website'] ?? ''));
$privacyUrl = trim((string) ($values['privacy_url'] ?? ''));
$imprintUrl = trim((string) ($values['imprint_url'] ?? ''));
$statusValue = (string) ($values['status'] ?? 'active');
?>
<details name="tenant-contact">
<summary><?php e(t('Contact')); ?></summary>
<hr>
<?php if ($supportEmail !== '' || $supportPhone !== '' || $billingEmail !== '' || $email !== '' || $phone !== '' || $fax !== '' || $website !== ''): ?>
<?php if ($supportEmail !== ''): ?>
<div>
<small><?php e(t('Support email')); ?></small>
<p><a href="mailto:<?php e($supportEmail); ?>"><?php e($supportEmail); ?></a></p>
</div>
<?php endif; ?>
<?php if ($supportPhone !== ''): ?>
<div>
<small><?php e(t('Support phone')); ?></small>
<p><a href="tel:<?php e($supportPhone); ?>"><?php e($supportPhone); ?></a></p>
</div>
<?php endif; ?>
<?php if ($billingEmail !== ''): ?>
<div>
<small><?php e(t('Billing email')); ?></small>
<p><a href="mailto:<?php e($billingEmail); ?>"><?php e($billingEmail); ?></a></p>
</div>
<?php endif; ?>
<?php if ($email !== ''): ?>
<div>
<small><?php e(t('Email')); ?></small>
<p><a href="mailto:<?php e($email); ?>"><?php e($email); ?></a></p>
</div>
<?php endif; ?>
<?php if ($phone !== ''): ?>
<div>
<small><?php e(t('Phone')); ?></small>
<p><a href="tel:<?php e($phone); ?>"><?php e($phone); ?></a></p>
</div>
<?php endif; ?>
<?php if ($fax !== ''): ?>
<div>
<small><?php e(t('Fax')); ?></small>
<div><?php e($fax); ?></div>
</div>
<?php endif; ?>
<?php if ($website !== ''): ?>
<div>
<small><?php e(t('Website')); ?></small>
<div>
<a href="<?php e($website); ?>" target="_blank" rel="noopener noreferrer">
<?php e($website); ?>
</a>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<?php
$emptyState = [
'message' => t('No contact details available'),
'size' => 'small',
'align' => 'left',
'icon' => false,
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php endif; ?>
</details>
<hr>
<details name="tenant-legal">
<summary><?php e(t('Legal')); ?></summary>
<hr>
<?php if ($privacyUrl !== '' || $imprintUrl !== ''): ?>
<?php if ($privacyUrl !== ''): ?>
<div>
<small><?php e(t('Privacy URL')); ?></small>
<p>
<a href="<?php e($privacyUrl); ?>" target="_blank" rel="noopener noreferrer">
<?php e($privacyUrl); ?>
</a>
</p>
</div>
<?php endif; ?>
<?php if ($imprintUrl !== ''): ?>
<div>
<small><?php e(t('Imprint URL')); ?></small>
<p>
<a href="<?php e($imprintUrl); ?>" target="_blank" rel="noopener noreferrer">
<?php e($imprintUrl); ?>
</a>
</p>
</div>
<?php endif; ?>
<?php else: ?>
<?php
$emptyState = [
'message' => t('No legal links configured'),
'size' => 'small',
'align' => 'left',
'icon' => false,
];
require templatePath('partials/app-empty-state.phtml');
?>
<?php endif; ?>
</details>
<hr>
<?php
$statusChangedByLabel = $values['status_changed_by_label'] ?? '';
$statusChangedById = $values['status_changed_by'] ?? null;
$statusChangedByUuid = $values['status_changed_by_uuid'] ?? null;
?>
<details name="tenant-status">
<summary><?php e(t('Status')); ?></summary>
<hr>
<div>
<small><?php e(t('Status')); ?></small>
<p><?php e($statusValue !== '' ? t(ucfirst($statusValue)) : '-'); ?></p>
</div>
<div>
<small><?php e(t('Status changed')); ?></small>
<p><?php e(dt($values['status_changed_at'] ?? '') ?: '-'); ?></p>
</div>
<div>
<small><?php e(t('Status changed by')); ?></small>
<p>
<?php if ($statusChangedByLabel !== ''): ?>
<?php if ($statusChangedByUuid): ?>
<a href="admin/users/edit/<?php e($statusChangedByUuid); ?>"><?php e($statusChangedByLabel); ?></a>
<?php else: ?>
<?php e($statusChangedByLabel); ?>
<?php endif; ?>
<?php elseif ($statusChangedById): ?>
<?php e('#' . $statusChangedById); ?>
<?php else: ?>
-
<?php endif; ?>
</p>
</div>
</details>
<hr>
<?php
$asideAuditDetailsName = 'tenant-audit';
$asideAuditCreated = (string) ($values['created'] ?? '');
$asideAuditCreatedByLabel = (string) ($values['created_by_label'] ?? '');
$asideAuditCreatedById = $values['created_by'] ?? null;
$asideAuditCreatedByUuid = (string) ($values['created_by_uuid'] ?? '');
$asideAuditModified = (string) ($values['modified'] ?? '');
$asideAuditModifiedByLabel = (string) ($values['modified_by_label'] ?? '');
$asideAuditModifiedById = $values['modified_by'] ?? null;
$asideAuditModifiedByUuid = (string) ($values['modified_by_uuid'] ?? '');
require templatePath('partials/app-details-aside-audit.phtml');
?>
<hr>
<?php
$asideIdsDetailsName = 'tenant-ids';
$asideIdsUuidFirst = true;
$asideIdValue = $values['id'] ?? '';
$asideUuidValue = (string) ($values['uuid'] ?? '');
require templatePath('partials/app-details-aside-ids.phtml');
?>
</div>
</aside>
</div>