瀏覽代碼

update accounts / users

master
FlorianEisenmenger 1 周之前
父節點
當前提交
039f24d3a6
共有 14 個文件被更改,包括 428 次插入136 次删除
  1. +115
    -0
      httpdocs/assets/scripts/account.js
  2. +10
    -1
      httpdocs/assets/styles/components/_account.scss
  3. +1
    -0
      httpdocs/config/packages/security.yaml
  4. +35
    -0
      httpdocs/migrations/central/Version20260523231929.php
  5. +51
    -1
      httpdocs/src/Controller/AccountController.php
  6. +8
    -0
      httpdocs/src/Controller/TeamController.php
  7. +19
    -5
      httpdocs/src/Entity/Central/Account.php
  8. +73
    -0
      httpdocs/src/EventSubscriber/ArchivedUserSubscriber.php
  9. +36
    -0
      httpdocs/src/Security/ArchivedUserChecker.php
  10. +1
    -0
      httpdocs/src/Service/RegistrationService.php
  11. +37
    -37
      httpdocs/templates/_nav.html.twig
  12. +40
    -91
      httpdocs/templates/account/index.html.twig
  13. +1
    -1
      httpdocs/templates/team/index.html.twig
  14. +1
    -0
      httpdocs/webpack.config.js

+ 115
- 0
httpdocs/assets/scripts/account.js 查看文件

@@ -0,0 +1,115 @@
// account.js
document.addEventListener('DOMContentLoaded', () => {

const toast = document.getElementById('account-toast');

function showToast(msg, isError = false) {
toast.textContent = msg;
toast.classList.toggle('account-toast--error', isError);
toast.classList.add('account-toast--visible');
setTimeout(() => toast.classList.remove('account-toast--visible'), 3000);
}

async function patchJson(url, data) {
const res = await fetch(url, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error ?? 'Fehler');
return json;
}

// ── Account-Formular ──────────────────────────────────────────────────────
const btnAccountSave = document.getElementById('btn-account-save');
if (btnAccountSave) {
btnAccountSave.addEventListener('click', async () => {
try {
await patchJson('/api/account', {
name: document.getElementById('account-name').value.trim(),
trackingInterval: parseInt(document.getElementById('account-interval').value, 10),
});
showToast('Gespeichert.');
} catch (e) {
showToast(e.message, true);
}
});
}

// ── Besitzer des Accounts ─────────────────────────────────────────────────
const superadminSelect = document.getElementById('superadmin-select');
if (superadminSelect && !superadminSelect.disabled) {
superadminSelect.addEventListener('change', async () => {
const selectedName = superadminSelect.options[superadminSelect.selectedIndex].text;
if (!confirm(`${selectedName} zum neuen Kontoinhaber machen?`)) {
// Auswahl zurücksetzen
superadminSelect.value = superadminSelect.dataset.original;
return;
}

try {
await patchJson('/api/account/superadmin', {
userId: parseInt(superadminSelect.value, 10),
});
showToast('Kontoinhaber geändert. Seite wird neu geladen…');
setTimeout(() => window.location.reload(), 1500);
} catch (e) {
showToast(e.message, true);
superadminSelect.value = superadminSelect.dataset.original;
}
});

// Original-Wert merken für Rollback
superadminSelect.dataset.original = superadminSelect.value;
}

// ── Passwort-Toggle ───────────────────────────────────────────────────────
const btnPwToggle = document.getElementById('btn-pw-toggle');
const pwSection = document.getElementById('pw-section');
if (btnPwToggle && pwSection) {
btnPwToggle.addEventListener('click', (e) => {
e.preventDefault();
const open = !pwSection.hidden;
pwSection.hidden = open;
btnPwToggle.textContent = open ? 'ändern' : 'abbrechen';
});
}

// ── Benutzer-Formular ─────────────────────────────────────────────────────
const btnUserSave = document.getElementById('btn-user-save');
if (btnUserSave) {
btnUserSave.addEventListener('click', async () => {
const data = {
firstName: document.getElementById('user-firstname').value.trim(),
lastName: document.getElementById('user-lastname').value.trim(),
email: document.getElementById('user-email').value.trim(),
};

if (pwSection && !pwSection.hidden) {
const pwNew = document.getElementById('user-pw-new').value;
const pwRepeat = document.getElementById('user-pw-repeat').value;
if (pwNew !== pwRepeat) {
showToast('Die Passwörter stimmen nicht überein.', true);
return;
}
data.currentPassword = document.getElementById('user-pw-current').value;
data.newPassword = pwNew;
}

try {
await patchJson('/api/account/user', data);
showToast('Gespeichert.');
if (pwSection) {
pwSection.hidden = true;
document.getElementById('btn-pw-toggle').textContent = 'ändern';
['user-pw-current', 'user-pw-new', 'user-pw-repeat'].forEach(id => {
document.getElementById(id).value = '';
});
}
} catch (e) {
showToast(e.message, true);
}
});
}
});

+ 10
- 1
httpdocs/assets/styles/components/_account.scss 查看文件

@@ -67,6 +67,9 @@
width: 100%;
margin: $space-8 auto;
padding: 0 $space-6;
display: flex;
flex-direction: column;
gap: $space-6;
}

// ─── Karte ───────────────────────────────────────────────────────────────────
@@ -101,6 +104,12 @@
.account-form__hint {
font-size: $font-size-xs;
color: $color-text-muted;

&--owner {
font-size: $font-size-sm;
line-height: 1.55;
margin-top: $space-1;
}
}

.account-form__link {
@@ -153,4 +162,4 @@
}

&--error { background: #c83232; }
}
}

+ 1
- 0
httpdocs/config/packages/security.yaml 查看文件

@@ -19,6 +19,7 @@ security:
security: false
main:
lazy: true
user_checker: App\Security\ArchivedUserChecker
provider: app_user_provider
access_denied_handler: App\Security\AccessDeniedHandler
form_login:


+ 35
- 0
httpdocs/migrations/central/Version20260523231929.php 查看文件

@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260523231929 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}

public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE account ADD super_admin_user_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE account ADD CONSTRAINT FK_7D3656A4B8CD8675 FOREIGN KEY (super_admin_user_id) REFERENCES `user` (id) ON DELETE SET NULL');
$this->addSql('CREATE INDEX IDX_7D3656A4B8CD8675 ON account (super_admin_user_id)');
}

public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE account DROP FOREIGN KEY FK_7D3656A4B8CD8675');
$this->addSql('DROP INDEX IDX_7D3656A4B8CD8675 ON account');
$this->addSql('ALTER TABLE account DROP super_admin_user_id');
}
}

+ 51
- 1
httpdocs/src/Controller/AccountController.php 查看文件

@@ -37,11 +37,21 @@ class AccountController extends AbstractController
$tab = 'user';
}

// Aktive Admins für das Superadmin-Dropdown (nur im Account-Tab relevant)
$adminUsers = $isAdmin
? array_map(
fn($au) => ['id' => $au->getUser()->getId(), 'name' => $au->getUser()->getFullName()],
$account->getAdmins()->toArray()
)
: [];

return $this->render('account/index.html.twig', [
'account' => $account,
'user' => $user,
'isAdmin' => $isAdmin,
'tab' => $tab,
'adminUsers' => $adminUsers,
'superAdminUserId' => $account->getSuperAdminUser()?->getId(),
'intervalOptions' => [
1 => 'Minuten',
15 => 'Viertelstunde',
@@ -80,6 +90,46 @@ class AccountController extends AbstractController
return $this->json(['ok' => true, 'name' => $account->getName()]);
}

#[Route('/api/account/superadmin', name: 'api_account_superadmin', methods: ['PATCH'])]
public function updateSuperAdmin(Request $request): JsonResponse
{
$account = $this->tenantContext->getAccount();
/** @var User $currentUser */
$currentUser = $this->getUser();
$accountUser = $this->accountUserRepo->findOneBy(['account' => $account, 'user' => $currentUser]);

if (!$accountUser?->isAdmin()) {
return $this->json(['error' => 'Zugriff verweigert'], 403);
}

// Nur der aktuelle Superadmin darf den Besitzer übertragen
if ($account->getSuperAdminUser()?->getId() !== $currentUser->getId()) {
return $this->json(['error' => 'Nur der aktuelle Kontoinhaber kann diese Funktion nutzen.'], 403);
}

$data = json_decode($request->getContent(), true) ?? [];
$userId = (int) ($data['userId'] ?? 0);

if ($userId === $currentUser->getId()) {
return $this->json(['error' => 'Du bist bereits Kontoinhaber.'], 400);
}

$newOwner = $this->userRepo->find($userId);
if ($newOwner === null) {
return $this->json(['error' => 'Benutzer nicht gefunden.'], 404);
}

$newAccountUser = $this->accountUserRepo->findOneBy(['account' => $account, 'user' => $newOwner]);
if ($newAccountUser === null || !$newAccountUser->isAdmin() || $newAccountUser->isArchived()) {
return $this->json(['error' => 'Der Benutzer muss aktiver Administrator sein.'], 400);
}

$account->setSuperAdminUser($newOwner);
$this->em->flush();

return $this->json(['ok' => true, 'newOwnerName' => $newOwner->getFullName()]);
}

#[Route('/api/account/user', name: 'api_account_user_update', methods: ['PATCH'])]
public function updateUser(Request $request): JsonResponse
{
@@ -122,4 +172,4 @@ class AccountController extends AbstractController

return $this->json(['ok' => true]);
}
}
}

+ 8
- 0
httpdocs/src/Controller/TeamController.php 查看文件

@@ -138,6 +138,10 @@ class TeamController extends AbstractController
return $this->json(['error' => 'Du kannst dich nicht selbst archivieren.'], 400);
}

if ($account->getSuperAdminUser()?->getId() === $accountUser->getUser()->getId()) {
return $this->json(['error' => 'Der Kontoinhaber kann nicht archiviert werden.'], 403);
}

$accountUser->setArchivedAt(new \DateTimeImmutable());
$this->em->flush();

@@ -246,6 +250,10 @@ class TeamController extends AbstractController
return $this->json(['error' => 'Du kannst dich nicht selbst entfernen.'], 400);
}

if ($account->getSuperAdminUser()?->getId() === $accountUser->getUser()->getId()) {
return $this->json(['error' => 'Der Kontoinhaber kann nicht entfernt werden.'], 403);
}

$userId = $accountUser->getUser()->getId();
if ($this->timeEntryRepo->countByUserId($userId) > 0) {
return $this->json(['error' => 'has_dependencies', 'canArchive' => true], 409);


+ 19
- 5
httpdocs/src/Entity/Central/Account.php 查看文件

@@ -29,6 +29,11 @@ class Account
#[ORM\Column]
private \DateTimeImmutable $createdAt;

/** Der ursprüngliche Ersteller des Accounts – kann nicht gelöscht werden */
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?User $superAdminUser = null;

#[ORM\OneToMany(targetEntity: AccountUser::class, mappedBy: 'account', cascade: ['persist', 'remove'])]
private Collection $accountUsers;

@@ -46,18 +51,27 @@ class Account
public function getSlug(): string { return $this->slug; }
public function setSlug(string $slug): static { $this->slug = $slug; return $this; }

public function getTrackingInterval(): int { return $this->trackingInterval; }
public function setTrackingInterval(int $v): static { $this->trackingInterval = $v; return $this; }
public function getTrackingInterval(): int { return $this->trackingInterval; }
public function setTrackingInterval(int $v): static { $this->trackingInterval = $v; return $this; }

public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }

public function getSuperAdminUser(): ?User { return $this->superAdminUser; }
public function setSuperAdminUser(?User $user): static { $this->superAdminUser = $user; return $this; }

public function isSuperAdmin(User $user): bool
{
return $this->superAdminUser !== null
&& $this->superAdminUser->getId() === $user->getId();
}

public function getAccountUsers(): Collection { return $this->accountUsers; }

/** Gibt alle User zurück, die Admin dieses Accounts sind */
/** Gibt alle aktiven (nicht archivierten) Admin-AccountUser zurück */
public function getAdmins(): Collection
{
return $this->accountUsers->filter(
fn(AccountUser $au) => $au->getRole() === AccountUser::ROLE_ADMIN
fn(AccountUser $au) => $au->getRole() === AccountUser::ROLE_ADMIN && !$au->isArchived()
);
}

@@ -67,4 +81,4 @@ class Account
}

public function __toString(): string { return $this->name; }
}
}

+ 73
- 0
httpdocs/src/EventSubscriber/ArchivedUserSubscriber.php 查看文件

@@ -0,0 +1,73 @@
<?php

namespace App\EventSubscriber;

use App\Repository\Central\AccountUserRepository;
use App\Service\TenantContext;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class ArchivedUserSubscriber implements EventSubscriberInterface
{
public function __construct(
private readonly TokenStorageInterface $tokenStorage,
private readonly AccountUserRepository $accountUserRepo,
private readonly TenantContext $tenantContext,
private readonly RouterInterface $router,
) {}

public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}

$token = $this->tokenStorage->getToken();
$user = $token?->getUser();

if ($user === null) {
return;
}

$account = $this->tenantContext->getAccount();
if ($account === null) {
return;
}

$accountUser = $this->accountUserRepo->findOneBy([
'account' => $account,
'user' => $user,
]);

if ($accountUser === null || !$accountUser->isArchived()) {
return;
}

// Token + Session löschen
$this->tokenStorage->setToken(null);
$request = $event->getRequest();
if ($request->hasSession()) {
$request->getSession()->invalidate();
}

// API: 401, sonst Redirect zu Login
if (str_starts_with($request->getPathInfo(), '/api/')) {
$event->setResponse(new JsonResponse(['error' => 'Konto deaktiviert.'], 401));
} else {
$event->setResponse(new RedirectResponse($this->router->generate('app_login')));
}
}

public static function getSubscribedEvents(): array
{
// Prio 6: nach Firewall (8), nach TenantRequestSubscriber (20)
return [
KernelEvents::REQUEST => ['onKernelRequest', 6],
];
}
}

+ 36
- 0
httpdocs/src/Security/ArchivedUserChecker.php 查看文件

@@ -0,0 +1,36 @@
<?php

namespace App\Security;

use App\Repository\Central\AccountUserRepository;
use App\Service\TenantContext;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class ArchivedUserChecker implements UserCheckerInterface
{
public function __construct(
private readonly AccountUserRepository $accountUserRepo,
private readonly TenantContext $tenantContext,
) {}

public function checkPreAuth(UserInterface $user): void {}

public function checkPostAuth(UserInterface $user): void
{
$account = $this->tenantContext->getAccount();
if ($account === null) {
return;
}

$accountUser = $this->accountUserRepo->findOneBy([
'account' => $account,
'user' => $user,
]);

if ($accountUser !== null && $accountUser->isArchived()) {
throw new CustomUserMessageAccountStatusException('Dein Konto wurde deaktiviert.');
}
}
}

+ 1
- 0
httpdocs/src/Service/RegistrationService.php 查看文件

@@ -112,6 +112,7 @@ class RegistrationService
$accountUser->setUser($user);
$accountUser->setRole(AccountUser::ROLE_ADMIN);
$this->centralEm->persist($accountUser);
$account->setSuperAdminUser($user);

$this->centralEm->flush();



+ 37
- 37
httpdocs/templates/_nav.html.twig 查看文件

@@ -2,40 +2,40 @@
{% set currentRoute = app.request.attributes.get('_route') %}

<nav class="main-nav">
<div class="main-nav__left">
<a href="{{ path('timetracking_week') }}"
class="main-nav__item{% if currentRoute starts with 'timetracking' %} main-nav__item--active{% endif %}">
Zeit erfassen
</a>
<span class="main-nav__item main-nav__item--disabled">Reports</span>
</div>
<div class="main-nav__right">
{% if isCurrentUserMemberOrAdmin() %}
<a href="{{ path('client_index') }}"
class="main-nav__item{% if currentRoute starts with 'client' %} main-nav__item--active{% endif %}">
Kunden
</a>
<a href="{{ path('project_index') }}"
class="main-nav__item{% if currentRoute starts with 'project' %} main-nav__item--active{% endif %}">
Projekte
</a>
<a href="{{ path('service_index') }}"
class="main-nav__item{% if currentRoute starts with 'service' %} main-nav__item--active{% endif %}">
Leistungen
</a>
{% endif %}
{% if isCurrentUserAdmin() %}
<a href="{{ path('team_index') }}"
class="main-nav__item{% if currentRoute starts with 'team' %} main-nav__item--active{% endif %}">
Team
</a>
<a href="{{ path('account_index') }}"
class="main-nav__item{% if currentRoute starts with 'account' %} main-nav__item--active{% endif %}">
Account
</a>
{% endif %}
<a href="{{ path('app_logout') }}" class="main-nav__item">
Abmelden
</a>
</div>
</nav>
<div class="main-nav__left">
<a href="{{ path('timetracking_week') }}"
class="main-nav__item{% if currentRoute starts with 'timetracking' %} main-nav__item--active{% endif %}">
Zeit erfassen
</a>
<span class="main-nav__item main-nav__item--disabled">Reports</span>
</div>
<div class="main-nav__right">
{% if isCurrentUserMemberOrAdmin() %}
<a href="{{ path('client_index') }}"
class="main-nav__item{% if currentRoute starts with 'client' %} main-nav__item--active{% endif %}">
Kunden
</a>
<a href="{{ path('project_index') }}"
class="main-nav__item{% if currentRoute starts with 'project' %} main-nav__item--active{% endif %}">
Projekte
</a>
<a href="{{ path('service_index') }}"
class="main-nav__item{% if currentRoute starts with 'service' %} main-nav__item--active{% endif %}">
Leistungen
</a>
{% endif %}
{% if isCurrentUserAdmin() %}
<a href="{{ path('team_index') }}"
class="main-nav__item{% if currentRoute starts with 'team' %} main-nav__item--active{% endif %}">
Team
</a>
{% endif %}
<a href="{{ path('account_index') }}"
class="main-nav__item{% if currentRoute starts with 'account' %} main-nav__item--active{% endif %}">
Account
</a>
<a href="{{ path('app_logout') }}" class="main-nav__item">
Abmelden
</a>
</div>
</nav>

+ 40
- 91
httpdocs/templates/account/index.html.twig 查看文件

@@ -7,6 +7,13 @@

{% block body %}

<script>
window.ACCOUNT = {
tab: '{{ tab }}',
isSuperAdmin: {{ superAdminUserId is not null and superAdminUserId == user.id ? 'true' : 'false' }},
};
</script>

<div class="account-page">

<div class="account-header">
@@ -41,8 +48,8 @@
<input type="text" id="account-name" class="input"
value="{{ account.name|e('html_attr') }}" />
<span class="account-form__hint">
Subdomain: <strong>{{ account.slug }}.{{ app.request.host|split('.')|slice(1)|join('.') }}</strong> — ändert sich nicht.
</span>
Subdomain: <strong>{{ account.slug }}.{{ app.request.host|split('.')|slice(1)|join('.') }}</strong> — ändert sich nicht.
</span>
</div>

<label class="account-form__label" for="account-interval">Zeitintervall</label>
@@ -121,99 +128,41 @@
{% endif %}

</div>

{# ── Besitzer des Accounts (nur Admin, Account-Tab) ─────────────────── #}
{% if tab == 'account' and isAdmin %}
<div class="account-card account-card--owner">
<div class="account-form__grid">

<label class="account-form__label" for="superadmin-select">Besitzer des Accounts</label>
<div class="account-form__field">
<select id="superadmin-select" class="select"
{% if superAdminUserId != user.id %}disabled{% endif %}>
{% for admin in adminUsers %}
<option value="{{ admin.id }}"{% if admin.id == superAdminUserId %} selected{% endif %}>
{{ admin.name }}
</option>
{% endfor %}
</select>
<p class="account-form__hint account-form__hint--owner">
Der Besitzer des Accounts ist für die Verwaltung der Zahlungsdaten zuständig.
Nur er kann den Account kündigen.
</p>
</div>

</div>
</div>
{% endif %}

</div>

</div>

<div class="account-toast" id="account-toast"></div>

<script>
(function () {
const toast = document.getElementById('account-toast');

function showToast(msg, isError = false) {
toast.textContent = msg;
toast.classList.toggle('account-toast--error', isError);
toast.classList.add('account-toast--visible');
setTimeout(() => toast.classList.remove('account-toast--visible'), 3000);
}

async function patchJson(url, data) {
const res = await fetch(url, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error ?? 'Fehler');
return json;
}

// ── Account-Formular ──────────────────────────────────────────────────────
const btnAccountSave = document.getElementById('btn-account-save');
if (btnAccountSave) {
btnAccountSave.addEventListener('click', async () => {
try {
await patchJson('/api/account', {
name: document.getElementById('account-name').value.trim(),
trackingInterval: parseInt(document.getElementById('account-interval').value, 10),
});
showToast('Gespeichert.');
} catch (e) {
showToast(e.message, true);
}
});
}

// ── Passwort-Toggle ───────────────────────────────────────────────────────
const btnPwToggle = document.getElementById('btn-pw-toggle');
const pwSection = document.getElementById('pw-section');
if (btnPwToggle && pwSection) {
btnPwToggle.addEventListener('click', (e) => {
e.preventDefault();
const open = !pwSection.hidden;
pwSection.hidden = open;
btnPwToggle.textContent = open ? 'ändern' : 'abbrechen';
});
}

// ── Benutzer-Formular ────────────────────────────────────────────────────
const btnUserSave = document.getElementById('btn-user-save');
if (btnUserSave) {
btnUserSave.addEventListener('click', async () => {
const data = {
firstName: document.getElementById('user-firstname').value.trim(),
lastName: document.getElementById('user-lastname').value.trim(),
email: document.getElementById('user-email').value.trim(),
};

if (pwSection && !pwSection.hidden) {
const pwNew = document.getElementById('user-pw-new').value;
const pwRepeat = document.getElementById('user-pw-repeat').value;
if (pwNew !== pwRepeat) {
showToast('Die Passwörter stimmen nicht überein.', true);
return;
}
data.currentPassword = document.getElementById('user-pw-current').value;
data.newPassword = pwNew;
}

try {
await patchJson('/api/account/user', data);
showToast('Gespeichert.');
if (pwSection) {
pwSection.hidden = true;
document.getElementById('btn-pw-toggle').textContent = 'ändern';
['user-pw-current', 'user-pw-new', 'user-pw-repeat'].forEach(id => {
document.getElementById(id).value = '';
});
}
} catch (e) {
showToast(e.message, true);
}
});
}
})();
</script>
{% endblock %}

{% endblock %}
{% block javascripts %}
{{ parent() }}
{{ encore_entry_script_tags('account') }}
{% endblock %}

+ 1
- 1
httpdocs/templates/team/index.html.twig 查看文件

@@ -151,7 +151,7 @@
<div class="crud-list" id="team-list-archived" data-tab-panel="archived" hidden>

{% for au in archivedUsers %}
<div class="crud-row crud-row--archived" id="au-{{ au.id }}">
<div class="crud-row crud-row--archived" id="au-{{ au.id }}" data-id="{{ au.id }}">
<div class="crud-row__display">
<div class="crud-row__info">
<span class="crud-row__name">{{ au.user.fullName }}</span>


+ 1
- 0
httpdocs/webpack.config.js 查看文件

@@ -24,6 +24,7 @@ Encore
.addEntry('crud', './assets/scripts/crud.js')
.addEntry('registration', './assets/scripts/registration.js')
.addEntry('team', './assets/scripts/team.js')
.addEntry('account', './assets/scripts/account.js')

// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
.splitEntryChunks()


Loading…
取消
儲存