configstuff
This commit is contained in:
parent
ca0e7d300d
commit
c3411e5754
12 changed files with 243 additions and 121 deletions
52
migrations/Version20250614171912.php
Normal file
52
migrations/Version20250614171912.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?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 Version20250614171912 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(<<<'SQL'
|
||||||
|
ALTER TABLE system_config ADD COLUMN description CLOB DEFAULT '' NOT NULL
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE TEMPORARY TABLE __temp__system_config AS SELECT id, created_at, updated_at, "key", value FROM system_config
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP TABLE system_config
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE TABLE system_config (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, created_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||||
|
, updated_at DATETIME NOT NULL --(DC2Type:datetime_immutable)
|
||||||
|
, "key" VARCHAR(255) NOT NULL, value CLOB NOT NULL)
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
INSERT INTO system_config (id, created_at, updated_at, "key", value) SELECT id, created_at, updated_at, "key", value FROM __temp__system_config
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP TABLE __temp__system_config
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE UNIQUE INDEX UNIQ_C4049ABD8A90ABA9 ON system_config ("key")
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,121 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Entity\DrinkType;
|
|
||||||
use App\Entity\InventoryRecord;
|
|
||||||
use App\Form\InventoryRecordForm;
|
|
||||||
use App\Repository\InventoryRecordRepository;
|
|
||||||
use App\Service\InventoryService;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
|
||||||
|
|
||||||
#[Route('/inventory-record')]
|
|
||||||
final class InventoryRecordController extends AbstractController
|
|
||||||
{
|
|
||||||
#[Route(name: 'app_inventory_record_index', methods: ['GET'])]
|
|
||||||
public function index(InventoryRecordRepository $inventoryRecordRepository): Response
|
|
||||||
{
|
|
||||||
return $this->render('inventory_record/index.html.twig', [
|
|
||||||
'inventory_records' => $inventoryRecordRepository->findAll(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/new/{drinkType}', name: 'app_inventory_record_new', methods: ['GET', 'POST'])]
|
|
||||||
public function new(Request $request, EntityManagerInterface $entityManager, DrinkType $drinkType): Response
|
|
||||||
{
|
|
||||||
$inventoryRecord = new InventoryRecord();
|
|
||||||
$inventoryRecord->setDrinkType($drinkType);
|
|
||||||
$form = $this->createForm(InventoryRecordForm::class, $inventoryRecord);
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
$entityManager->persist($inventoryRecord);
|
|
||||||
$entityManager->flush();
|
|
||||||
|
|
||||||
// If it's an HTMX request, return a redirect that HTMX will follow
|
|
||||||
if ($request->headers->has('HX-Request')) {
|
|
||||||
$response = new Response('', Response::HTTP_SEE_OTHER);
|
|
||||||
$response->headers->set('HX-Redirect', $this->generateUrl('app_index'));
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_index', [], Response::HTTP_SEE_OTHER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's an HTMX request
|
|
||||||
if ($request->headers->has('HX-Request')) {
|
|
||||||
return $this->render('inventory_record/_modal_form.html.twig', [
|
|
||||||
'inventory_record' => $inventoryRecord,
|
|
||||||
'form' => $form,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('inventory_record/new.html.twig', [
|
|
||||||
'inventory_record' => $inventoryRecord,
|
|
||||||
'form' => $form,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/modal/{drinkType}', name: 'app_inventory_record_modal', methods: ['GET'])]
|
|
||||||
public function modal(DrinkType $drinkType, InventoryService $inventoryService): Response
|
|
||||||
{
|
|
||||||
$inventoryRecord = new InventoryRecord();
|
|
||||||
$inventoryRecord->setDrinkType($drinkType);
|
|
||||||
$inventoryRecord->setQuantity(
|
|
||||||
$inventoryService->getLatestInventoryRecord($drinkType)->getQuantity() ?? 0
|
|
||||||
);
|
|
||||||
$form = $this->createForm(InventoryRecordForm::class, $inventoryRecord, [
|
|
||||||
'action' => $this->generateUrl('app_inventory_record_new', [
|
|
||||||
'drinkType' => $drinkType->getId(),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $this->render('inventory_record/_modal_form.html.twig', [
|
|
||||||
'inventory_record' => $inventoryRecord,
|
|
||||||
'form' => $form,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/{id}', name: 'app_inventory_record_show', methods: ['GET'])]
|
|
||||||
public function show(InventoryRecord $inventoryRecord): Response
|
|
||||||
{
|
|
||||||
return $this->render('inventory_record/show.html.twig', [
|
|
||||||
'inventory_record' => $inventoryRecord,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/{id}/edit', name: 'app_inventory_record_edit', methods: ['GET', 'POST'])]
|
|
||||||
public function edit(Request $request, InventoryRecord $inventoryRecord, EntityManagerInterface $entityManager): Response
|
|
||||||
{
|
|
||||||
$form = $this->createForm(InventoryRecordForm::class, $inventoryRecord);
|
|
||||||
$form->handleRequest($request);
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
|
||||||
$entityManager->flush();
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_inventory_record_index', [], Response::HTTP_SEE_OTHER);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->render('inventory_record/edit.html.twig', [
|
|
||||||
'inventory_record' => $inventoryRecord,
|
|
||||||
'form' => $form,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/{id}', name: 'app_inventory_record_delete', methods: ['POST'])]
|
|
||||||
public function delete(Request $request, InventoryRecord $inventoryRecord, EntityManagerInterface $entityManager): Response
|
|
||||||
{
|
|
||||||
if ($this->isCsrfTokenValid('delete' . $inventoryRecord->getId(), $request->getPayload()->getString('_token'))) {
|
|
||||||
$entityManager->remove($inventoryRecord);
|
|
||||||
$entityManager->flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->redirectToRoute('app_inventory_record_index', [], Response::HTTP_SEE_OTHER);
|
|
||||||
}
|
|
||||||
}
|
|
61
src/Controller/SystemConfigController.php
Normal file
61
src/Controller/SystemConfigController.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\SystemConfig;
|
||||||
|
use App\Enum\SystemSettingKey;
|
||||||
|
use App\Form\SystemConfigForm;
|
||||||
|
use App\Repository\SystemConfigRepository;
|
||||||
|
use App\Service\ConfigurationService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
#[Route('/system-config')]
|
||||||
|
final class SystemConfigController extends AbstractController
|
||||||
|
{
|
||||||
|
#[Route(name: 'app_system_config_index', methods: ['GET'])]
|
||||||
|
public function index(SystemConfigRepository $systemConfigRepository): Response
|
||||||
|
{
|
||||||
|
$configs = [];
|
||||||
|
foreach (SystemSettingKey::cases() as $key) {
|
||||||
|
$configs[] = $systemConfigRepository->findByKey($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('system_config/index.html.twig', [
|
||||||
|
'system_configs' => $configs,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{id}/edit', name: 'app_system_config_edit', methods: ['GET', 'POST'])]
|
||||||
|
public function edit(Request $request, SystemConfig $systemConfig, EntityManagerInterface $entityManager): Response
|
||||||
|
{
|
||||||
|
$form = $this->createForm(SystemConfigForm::class, $systemConfig);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_system_config_index', [], Response::HTTP_SEE_OTHER);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('system_config/edit.html.twig', [
|
||||||
|
'system_config' => $systemConfig,
|
||||||
|
'form' => $form,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(path: '/{id}/reset', name: 'app_system_config_reset', methods: ['POST'])]
|
||||||
|
public function reset(Request $request, SystemConfig $systemConfig, ConfigurationService $configurationService): Response
|
||||||
|
{
|
||||||
|
if ($this->isCsrfTokenValid('reset' . $systemConfig->getId(), $request->getPayload()->getString('_token'))) {
|
||||||
|
$configurationService->reset($systemConfig->getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_system_config_index', [], Response::HTTP_SEE_OTHER);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,11 +14,17 @@ use Doctrine\ORM\Mapping as ORM;
|
||||||
class SystemConfig
|
class SystemConfig
|
||||||
{
|
{
|
||||||
public const string DEFAULT_STOCK_ADJUSTMENT_LOOKBACK_ORDERS = '3';
|
public const string DEFAULT_STOCK_ADJUSTMENT_LOOKBACK_ORDERS = '3';
|
||||||
|
public const string STOCK_ADJUSTMENT_LOOKBACK_ORDERS_DESCRIPTION = 'Number of orders to look back for stock adjustment';
|
||||||
public const string DEFAULT_DEFAULT_WANTED_STOCK = '2';
|
public const string DEFAULT_DEFAULT_WANTED_STOCK = '2';
|
||||||
|
public const string DEFAULT_DEFAULT_WANTED_STOCK_DESCRIPTION = 'Default wanted stock for new drink types';
|
||||||
public const string DEFAULT_SYSTEM_NAME = 'Zaufen';
|
public const string DEFAULT_SYSTEM_NAME = 'Zaufen';
|
||||||
|
public const string DEFAULT_SYSTEM_NAME_DESCRIPTION = 'System name';
|
||||||
public const string DEFAULT_STOCK_INCREASE_AMOUNT = '1';
|
public const string DEFAULT_STOCK_INCREASE_AMOUNT = '1';
|
||||||
|
public const string DEFAULT_STOCK_INCREASE_AMOUNT_DESCRIPTION = 'Amount to increase stock by for stock adjustment suggested by the system';
|
||||||
public const string DEFAULT_STOCK_DECREASE_AMOUNT = '1';
|
public const string DEFAULT_STOCK_DECREASE_AMOUNT = '1';
|
||||||
|
public const string DEFAULT_STOCK_DECREASE_AMOUNT_DESCRIPTION = 'Amount to decrease stock by for stock adjustment suggested by the system';
|
||||||
public const string DEFAULT_STOCK_LOW_MULTIPLIER = '0.3';
|
public const string DEFAULT_STOCK_LOW_MULTIPLIER = '0.3';
|
||||||
|
public const string DEFAULT_STOCK_LOW_MULTIPLIER_DESCRIPTION = 'Multiplier to to check if stock is low.';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
|
@ -37,6 +43,12 @@ class SystemConfig
|
||||||
#[ORM\Column(type: 'text')]
|
#[ORM\Column(type: 'text')]
|
||||||
private string $value;
|
private string $value;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', options: [
|
||||||
|
'default' => '',
|
||||||
|
])]
|
||||||
|
private string $description;
|
||||||
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
) {
|
) {
|
||||||
$this->createdAt = new DateTimeImmutable();
|
$this->createdAt = new DateTimeImmutable();
|
||||||
|
@ -85,4 +97,14 @@ class SystemConfig
|
||||||
{
|
{
|
||||||
$this->updatedAt = new DateTimeImmutable();
|
$this->updatedAt = new DateTimeImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(string $description): void
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,4 +29,16 @@ enum SystemSettingKey: string
|
||||||
self::STOCK_LOW_MULTIPLIER => SystemConfig::DEFAULT_STOCK_LOW_MULTIPLIER,
|
self::STOCK_LOW_MULTIPLIER => SystemConfig::DEFAULT_STOCK_LOW_MULTIPLIER,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function defaultDescription(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::STOCK_ADJUSTMENT_LOOKBACK_ORDERS => SystemConfig::STOCK_ADJUSTMENT_LOOKBACK_ORDERS_DESCRIPTION,
|
||||||
|
self::DEFAULT_WANTED_STOCK => SystemConfig::DEFAULT_DEFAULT_WANTED_STOCK_DESCRIPTION,
|
||||||
|
self::SYSTEM_NAME => SystemConfig::DEFAULT_SYSTEM_NAME_DESCRIPTION,
|
||||||
|
self::STOCK_INCREASE_AMOUNT => SystemConfig::DEFAULT_STOCK_INCREASE_AMOUNT_DESCRIPTION,
|
||||||
|
self::STOCK_DECREASE_AMOUNT => SystemConfig::DEFAULT_STOCK_DECREASE_AMOUNT_DESCRIPTION,
|
||||||
|
self::STOCK_LOW_MULTIPLIER => SystemConfig::DEFAULT_STOCK_LOW_MULTIPLIER_DESCRIPTION,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
35
src/Form/SystemConfigForm.php
Normal file
35
src/Form/SystemConfigForm.php
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\SystemConfig;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class SystemConfigForm extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('value', TextType::class, [
|
||||||
|
'label' => 'Value',
|
||||||
|
'help' => 'The value of the system setting',
|
||||||
|
])
|
||||||
|
->add('description', TextType::class, [
|
||||||
|
'label' => 'Description',
|
||||||
|
'help' => 'Description of the system setting',
|
||||||
|
'disabled' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => SystemConfig::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,11 @@ class SystemConfigRepository extends AbstractRepository
|
||||||
$config = new SystemConfig();
|
$config = new SystemConfig();
|
||||||
$config->setKey($key);
|
$config->setKey($key);
|
||||||
$config->setValue($key->defaultValue());
|
$config->setValue($key->defaultValue());
|
||||||
|
$config->setDescription($key->defaultDescription());
|
||||||
|
$this->save($config);
|
||||||
|
}
|
||||||
|
if ($config->getDescription() === '') {
|
||||||
|
$config->setDescription($key->defaultDescription());
|
||||||
$this->save($config);
|
$this->save($config);
|
||||||
}
|
}
|
||||||
return $config;
|
return $config;
|
||||||
|
|
|
@ -24,6 +24,9 @@
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" aria-current="page" href="/">Home</a>
|
<a class="nav-link active" aria-current="page" href="/">Home</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ path('app_system_config_index') }}">System Settings</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="btn-group ms-auto" role="group" aria-label="Mode selection">
|
<div class="btn-group ms-auto" role="group" aria-label="Mode selection">
|
||||||
<input type="radio" class="btn-check" name="mode" id="normal" autocomplete="off" checked>
|
<input type="radio" class="btn-check" name="mode" id="normal" autocomplete="off" checked>
|
||||||
|
|
4
templates/system_config/_form.html.twig
Normal file
4
templates/system_config/_form.html.twig
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{{ form_start(form) }}
|
||||||
|
{{ form_widget(form) }}
|
||||||
|
<button class="btn">{{ button_label|default('Save') }}</button>
|
||||||
|
{{ form_end(form) }}
|
4
templates/system_config/_reset_form.html.twig
Normal file
4
templates/system_config/_reset_form.html.twig
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<form method="post" action="{{ path('app_system_config_reset', {'id': system_config.id}) }}" onsubmit="return confirm('Are you sure you want to reset this setting to its default value?');">
|
||||||
|
<input type="hidden" name="_token" value="{{ csrf_token('reset' ~ system_config.id) }}">
|
||||||
|
<button class="btn">Reset</button>
|
||||||
|
</form>
|
13
templates/system_config/edit.html.twig
Normal file
13
templates/system_config/edit.html.twig
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Edit System Setting{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>Edit System Setting: {{ system_config.key.name }}</h1>
|
||||||
|
|
||||||
|
{{ include('system_config/_form.html.twig', {'button_label': 'Update'}) }}
|
||||||
|
|
||||||
|
<a href="{{ path('app_system_config_index') }}">back to list</a>
|
||||||
|
|
||||||
|
{{ include('system_config/_reset_form.html.twig') }}
|
||||||
|
{% endblock %}
|
32
templates/system_config/index.html.twig
Normal file
32
templates/system_config/index.html.twig
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}System Settings{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>System Settings</h1>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for system_config in system_configs %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ system_config.description }}</td>
|
||||||
|
<td>{{ system_config.value }}</td>
|
||||||
|
<td>
|
||||||
|
<a class="btn btn-primary" href="{{ path('app_system_config_edit', {'id': system_config.id}) }}">edit</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">no records found</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue