set up edit for project in api

This commit is contained in:
Charles 2026-02-18 15:38:52 +01:00
parent 782ca27b5e
commit c0a8a9ab82
12 changed files with 383 additions and 67 deletions

View File

@ -10,7 +10,7 @@ export default class extends Controller {
orgId: Number,
admin: Boolean
}
static targets = ["modal", "appList", "nameInput", "formTitle"];
static targets = ["modal", "appList", "nameInput", "formTitle", "timestampSelect", "deletionSelect"];
connect(){
if(this.listProjectValue){
this.table();
@ -137,17 +137,23 @@ export default class extends Controller {
async submitForm(event) {
event.preventDefault();
const formData = new FormData(event.target);
const form = event.target;
const formData = new FormData(form); // This automatically picks up the 'logo' file
const payload = {
organizationId: this.orgIdValue,
applications: formData.getAll('applications[]')
};
// Only include name if it wasn't disabled (new projects)
if (!this.nameInputTarget.disabled) {
payload.name = formData.get('name');
// 1. Validate File Format
const logoFile = formData.get('logo');
if (logoFile && logoFile.size > 0) {
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
if (!allowedTypes.includes(logoFile.type)) {
alert("Format invalide. Veuillez utiliser uniquement des fichiers PNG ou JPG.");
return; // Stop submission
}
}
// 2. Prepare for Multipart sending
// Since we are using FormData, we don't need JSON.stringify or 'Content-Type': 'application/json'
// We add the extra fields to the formData object
formData.append('organizationId', this.orgIdValue);
const url = this.currentProjectId
? `/project/edit/${this.currentProjectId}/ajax`
@ -155,17 +161,20 @@ export default class extends Controller {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
// IMPORTANT: Do NOT set Content-Type header when sending FormData with files
// The browser will set 'multipart/form-data' and the boundary automatically
body: formData
});
if (response.ok) {
this.modal.hide();
// Use Tabulator's setData() instead of reload() for better UX if possible
location.reload();
} else {
const result = await response.json();
if (response.status === 409) {
alert("Un projet avec ce nom existe déjà. Veuillez choisir un nom différent.");
alert("Un projet avec ce nom existe déjà.");
} else {
alert(result.error || "Une erreur est survenue.");
}
}
}
@ -188,6 +197,9 @@ export default class extends Controller {
// 3. Set the name
this.nameInputTarget.value = project.name;
console.log(project);
this.timestampSelectTarget.value = project.timestampPrecision;
this.deletionSelectTarget.value = project.deletionAllowed;
// 4. Check the boxes
// We look for all checkboxes inside our appList target

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5M1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4z"/></svg>

After

Width:  |  Height:  |  Size: 272 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" d="M5 2a.5.5 0 0 1 .5-.5c.862 0 1.573.287 2.06.566c.174.099.321.198.44.286c.119-.088.266-.187.44-.286A4.17 4.17 0 0 1 10.5 1.5a.5.5 0 0 1 0 1c-.638 0-1.177.213-1.564.434a3.5 3.5 0 0 0-.436.294V7.5H9a.5.5 0 0 1 0 1h-.5v4.272c.1.08.248.187.436.294c.387.221.926.434 1.564.434a.5.5 0 0 1 0 1a4.17 4.17 0 0 1-2.06-.566A5 5 0 0 1 8 13.65a5 5 0 0 1-.44.285a4.17 4.17 0 0 1-2.06.566a.5.5 0 0 1 0-1c.638 0 1.177-.213 1.564-.434c.188-.107.335-.214.436-.294V8.5H7a.5.5 0 0 1 0-1h.5V3.228a3.5 3.5 0 0 0-.436-.294A3.17 3.17 0 0 0 5.5 2.5A.5.5 0 0 1 5 2"/><path d="M10 5h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4v1h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4zM6 5V4H2a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4v-1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/></g></svg>

After

Width:  |  Height:  |  Size: 827 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1l-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/></svg>

After

Width:  |  Height:  |  Size: 609 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm0-2h16V6H4zm0 0V6zm8-5h6q.425 0 .713-.288T19 12V8q0-.425-.288-.712T18 7h-6q-.425 0-.712.288T11 8v4q0 .425.288.713T12 13m1-2V9h4v2z"/></svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@ -0,0 +1,32 @@
<?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 Version20260218111608 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 project ADD logo VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE project DROP logo');
}
}

View File

@ -0,0 +1,36 @@
<?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 Version20260218111821 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 project ADD timestamp_precision VARCHAR(10) DEFAULT NULL');
$this->addSql('ALTER TABLE project ADD deletion_allowed BOOLEAN DEFAULT NULL');
$this->addSql('ALTER TABLE project ADD audits_enabled BOOLEAN DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE project DROP timestamp_precision');
$this->addSql('ALTER TABLE project DROP deletion_allowed');
$this->addSql('ALTER TABLE project DROP audits_enabled');
}
}

View File

@ -44,39 +44,66 @@ final class ProjectController extends AbstractController
public function new(Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
if (!$data) {
return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST);
} $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]);
if(!$org) {
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
$orgId = $request->request->get('organizationId');
$name = $request->request->get('name');
$applications = $request->request->all('applications') ?? []; // Expects applications[] from JS
$org = $this->organizationsRepository->find($orgId);
if (!$org) {
return new JsonResponse(['error' => 'Organization not found'], 404);
}
$sanitizedDbName = $this->projectService->getProjectDbName($data['name'], $org->getProjectPrefix());
if($this->projectRepository->findOneBy(['bddName' => $sanitizedDbName])) {
return new JsonResponse(['error' => 'A project with the same name already exists'], Response::HTTP_CONFLICT);
// 2. Handle File Upload
$logoFile = $request->files->get('logo');
$logoPath = null;
// 3. Project Creation
$sanitizedDbName = $this->projectService->getProjectDbName($name, $org->getProjectPrefix());
if ($this->projectRepository->findOneBy(['bddName' => $sanitizedDbName])) {
return new JsonResponse(['error' => 'A project with the same name already exists'], 409);
}
if(!$this->projectService->isApplicationArrayValid($data['applications'])) {
return new JsonResponse(['error' => 'Invalid applications array'], Response::HTTP_BAD_REQUEST);
if ($logoFile) {
$logoPath = $this->projectService->handleLogoUpload($logoFile, $sanitizedDbName);
}
$project = new Project();
$project->setName($data['name']);
$project->setName($name);
$project->setBddName($sanitizedDbName);
$project->setOrganization($org);
$project->setApplications($data['applications']);
$project->setApplications($applications);
$project->setTimestampPrecision($request->request->get('timestamp'));
$project->setDeletionAllowed($request->request->getBoolean('deletion'));
if ($logoPath) {
$project->setLogo($logoPath);
}
$this->entityManager->persist($project);
$this->entityManager->flush(); //On met le flush avant parce qu'on a besoin de l'ID du projet pour la création distante.
//Oui ducoup c'est chiant parce que le projet est crée même s'il y a une erreur API, mais OH ffs at that point.
// Remote creation logic
try {
$this->SSOProjectService->createRemoteProject('http://api.solutions-easy.moi', $project);
$this->entityManager->flush();
return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500);
}
return new JsonResponse(['message' => 'Project created successfully'], 201);
}
#[Route(path:'/edit/{id}/ajax', name: '_edit', methods: ['POST'])]
public function edit(Request $request, int $id): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$data = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
if (!$data) {
return new JsonResponse(['error' => 'Invalid JSON'], Response::HTTP_BAD_REQUEST);
} $org = $this->organizationsRepository->findOneBy(['id' => $data['organizationId']]);
$orgId = $request->request->get('organizationId');
$applications = $request->request->all('applications') ?? []; // Expects applications[] from JS
$org = $this->organizationsRepository->findOneBy(['id' => $orgId]);
if(!$org) {
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
}
@ -84,9 +111,29 @@ final class ProjectController extends AbstractController
if(!$project) {
return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND);
}
$project->setApplications($data['applications']);
$logoFile = $request->files->get('logo');
$logoPath = null;
if ($logoFile) {
$logoPath = $this->projectService->handleLogoUpload($logoFile, $project->getBddName());
}
$project->setApplications($applications);
$project->setModifiedAt(new \DateTimeImmutable());
$project->setTimestampPrecision($request->request->get('timestamp'));
$project->setDeletionAllowed($request->request->getBoolean('deletion'));
if ($logoPath) {
$project->setLogo($logoPath);
}
$this->entityManager->persist($project);
// Remote editing logic
try {
$this->SSOProjectService->editRemoteProject('http://api.solutions-easy.moi', $project);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'Remote creation failed: ' . $e->getMessage()], 500);
}
$this->entityManager->flush();
return new JsonResponse(['message' => 'Project updated successfully'], Response::HTTP_OK);
}
@ -148,6 +195,8 @@ final class ProjectController extends AbstractController
'id' => $project->getId(),
'name' => ucfirst($project->getName()),
'applications' => $project->getApplications(),
'timestampPrecision'=> $project->getTimestampPrecision(),
'deletionAllowed' => $project->isDeletionAllowed(),
]);
}

View File

@ -35,9 +35,21 @@ class Project
#[ORM\Column]
private ?bool $isDeleted = null;
#[ORM\Column(length: 255, nullable: true)]
#[ORM\Column(length: 255)]
private ?string $bddName = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $logo = null;
#[ORM\Column(length: 10)]
private ?string $timestampPrecision = null;
#[ORM\Column()]
private ?bool $deletionAllowed = null;
#[ORM\Column(nullable: true)]
private ?bool $auditsEnabled = null;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
@ -146,4 +158,52 @@ class Project
return $this;
}
public function getLogo(): ?string
{
return $this->logo;
}
public function setLogo(?string $logo): static
{
$this->logo = $logo;
return $this;
}
public function getTimestampPrecision(): ?string
{
return $this->timestampPrecision;
}
public function setTimestampPrecision(?string $timestampPrecision): static
{
$this->timestampPrecision = $timestampPrecision;
return $this;
}
public function isDeletionAllowed(): ?bool
{
return $this->deletionAllowed;
}
public function setDeletionAllowed(?bool $deletionAllowed): static
{
$this->deletionAllowed = $deletionAllowed;
return $this;
}
public function isAuditsEnabled(): ?bool
{
return $this->auditsEnabled;
}
public function setAuditsEnabled(?bool $auditsEnabled): static
{
$this->auditsEnabled = $auditsEnabled;
return $this;
}
}

View File

@ -3,12 +3,16 @@
namespace App\Service;
use App\Repository\AppsRepository;
use App\Service\LoggerService;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\String\Slugger\AsciiSlugger;
class ProjectService{
public function __construct(private readonly AppsRepository $appsRepository)
public function __construct(private readonly AppsRepository $appsRepository, private readonly Security $security, private readonly LoggerService $loggerService)
{
}
@ -40,4 +44,43 @@ class ProjectService{
}
return true;
}
public function handleLogoUpload($logoFile, $projectBddName): ?string
{
// 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures')
$destinationDir = 'uploads/project_logos';
// 2. Create the directory if it doesn't exist
if (!file_exists($destinationDir)) {
// 0755 is the standard permission (Owner: read/write/exec, Others: read/exec)
if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) {
throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir));
}
}
$extension = $logoFile->guessExtension();
// Sanitize the filename to remove special characters/spaces to prevent filesystem errors
$customFilename = $projectBddName . '.' . $extension;
try {
// 4. Move the file to the destination directory
$logoFile->move($destinationDir, $customFilename);
// 5. Update the user entity with the relative path
// Ensure you store the path relative to your public folder usually
return $destinationDir . '/' . $customFilename;
} catch (\Exception $e) {
// 6. Log the critical error as requested
$this->loggerService->logError('File upload failed',[
'target_user_id' => $this->security->getUser()->getId(),
'message' => $e->getMessage(),
'file_name' => $customFilename,
]);
// Optional: Re-throw the exception if you want the controller/user to know the upload failed
throw new FileException('File upload failed.');
}
}
}

View File

@ -7,6 +7,7 @@ use App\Entity\Project;
use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\Client;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class ProjectService
{
@ -20,23 +21,12 @@ class ProjectService
public function createRemoteProject(string $clientAppUrl, Project $project): void
{
// 1. Get a token for "ourselves" -> on en a besoin parce que c'est du M2M.
$portalClient = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]);
$tokenResponse = $this->httpClient->request('POST', $this->appUrl . 'token', [
'auth_basic' => [$portalClient->getIdentifier(),$portalClient->getSecret()], // ID and Secret go here
'body' => [
'grant_type' => 'client_credentials',
],
]);
$tokenResponse = $this->getTokenResponse();
$accessToken = $tokenResponse->toArray()['access_token'];
// data must match easy check database
$projectJson = [
'id' => $project->getId(),
'projet' => $project->getName(),
'entity_id' => 3,
'bdd' => $project->getBddName(),
'isactive' => $project->isActive(),
];
$projectJson = $this->getTokenResponse($project);
// 2. Call the Client Application's Webhook/API
$this->httpClient->request('POST', $clientAppUrl . '/api/v1/project/create', [
@ -45,4 +35,41 @@ class ProjectService
]);
}
public function editRemoteProject(string $clientAppUrl, Project $project): void
{
$tokenResponse = $this->getTokenResponse();
$accessToken = $tokenResponse->toArray()['access_token'];
// data must match easy check database
$projectJson = $this->getProjectToJson($project);
// 2. Call the Client Application's Webhook/API
$this->httpClient->request('PUT', $clientAppUrl . '/api/v1/project/edit/'. $project->getId(), [
'headers' => ['Authorization' => 'Bearer ' . $accessToken],
'json' => $projectJson
]);
}
public function getTokenResponse(): ResponseInterface{
$portalClient = $this->entityManager->getRepository(Client::class)->findOneBy(['identifier' => $this->clientIdentifier]);
return $this->httpClient->request('POST', $this->appUrl . 'token', [
'auth_basic' => [$portalClient->getIdentifier(),$portalClient->getSecret()], // ID and Secret go here
'body' => [
'grant_type' => 'client_credentials',
],
]);
}
public function getProjectToJson(Project $project): array {
return [
'id' => $project->getId(),
'projet' => $project->getName(),
'entity_id' => 3,
'bdd' => $project->getBddName(),
'isactive' => $project->isActive(),
'logo' => $project->getLogo(),
'timestamp'=> $project->getTimestampPrecision(),
'deletion' => $project->isDeletionAllowed()
];
}
}

View File

@ -1,7 +1,7 @@
{% extends 'base.html.twig' %}
{% block body %}
{% set isSA = is_granted('ROLE_SUPER_ADMIN')%}
{% set isSA = is_granted('ROLE_SUPER_ADMIN') %}
<div class="w-100 h-100 p-5 m-auto">
{% for type, messages in app.flashes %}
{% for message in messages %}
@ -70,7 +70,8 @@
</div>
{# New User Modal #}
<div class="modal fade" id="newUserModal" tabindex="-1" aria-hidden="true" data-user-target="modal">
<div class="modal fade" id="newUserModal" tabindex="-1" aria-hidden="true"
data-user-target="modal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
@ -99,19 +100,23 @@
</div>
<div class="mb-3">
<label class="form-label">Photo de profil</label>
<input type="file" name="pictureUrl" class="form-control" accept="image/*">
<input type="file" name="pictureUrl" class="form-control"
accept="image/*">
</div>
<hr>
<label class="form-label"><b>Applications à associer**</b></label>
<div class="row" data-user-target="appList">
{# Applications will be injected here #}
<div class="text-center p-3 text-muted">Chargement des applications...</div>
<div class="text-center p-3 text-muted">Chargement des applications...
</div>
</div>
<input type="hidden" name="organizationId" value="{{ organization.id }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Annuler
</button>
<button type="submit" class="btn btn-primary">Créer l'utilisateur</button>
</div>
</form>
@ -137,18 +142,21 @@
</div>
{# Modal for Adding Admin #}
<div class="modal fade" id="addAdminModal" tabindex="-1" aria-hidden="true" data-user-target="modal">
<div class="modal fade" id="addAdminModal" tabindex="-1" aria-hidden="true"
data-user-target="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ajouter un administrateur</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<form data-action="submit->user#submitAddAdmin">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Sélectionner l'utilisateur</label>
<select name="userId" class="form-select" data-user-target="userSelect" required>
<select name="userId" class="form-select" data-user-target="userSelect"
required>
<option value="">Chargement...</option>
</select>
</div>
@ -157,7 +165,9 @@
<input type="hidden" name="organizationId" value="{{ organization.id }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Annuler
</button>
<button type="submit" class="btn btn-primary">Ajouter</button>
</div>
</form>
@ -183,7 +193,6 @@
</div>
{# APPLICATION ROW #}
{# TODO:remove app acces and replace wioth project overview#}
<div class="row mb-3 card no-header-bg"
data-controller="project"
data-project-list-project-value="true"
@ -203,7 +212,8 @@
</div>
</div>
<div class="modal fade" id="createProjectModal" tabindex="-1" aria-hidden="true" data-project-target="modal">
<div class="modal fade" id="createProjectModal" tabindex="-1" aria-hidden="true"
data-project-target="modal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
@ -213,20 +223,63 @@
<form data-action="submit->project#submitForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nom du projet</label>
<label class="form-label">
<i class="color-primary">{{ ux_icon('bi:input-cursor-text', {height: '15px', width: '15px'}) }}</i>
Nom du projet</label>
<input type="text" name="name"
data-project-target="nameInput"
class="form-control" required>
</div>
<div class="row">
<div class="mb-3 col-6">
<label class="form-label">
<i class="color-primary">
{{ ux_icon('bi:calendar', {height: '15px', width: '15px'}) }}
</i>Horodatage</label>
<select name="timestamp" class="form-select"
data-project-target="timestampSelect" required>
<option value="day">Jour uniquement (YYYY-MM-DD)</option>
<option value="full">Horodatage complet (YYYY-MM-DD HH:MM:SS)
</option>
</select>
</div>
<div class="mb-3 col-6">
<label class="form-label">
<i class="color-primary">
{{ ux_icon('bi:trash3', {height: '15px', width: '15px'}) }}
</i>
Autorisation de suppression</label>
<select name="deletion" class="form-select"
data-project-target="deletionSelect" required>
<option value="true">Suppression autorisée</option>
<option value="false">Suppression interdite</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">
<i class="color-primary">
{{ ux_icon('material-symbols:picture-in-picture-outline-rounded', {height: '15px', width: '15px'}) }}
</i>
Logo de projet
</label>
<input type="file" name="logo" class="form-control"
accept="image/*">
</div>
</div>
<label class="form-label">Applications</label>
<label class="form-label">
<i class="color-primary">{{ ux_icon('bi:grid-3x3-gap', {height: '15px', width: '15px'}) }}</i>
Applications</label>
<div class="row" data-project-target="appList">
{# Checkboxes will be injected here #}
<div class="text-center p-3">Chargement des applications...</div>
</div>
<input name="organizationId" type="hidden" value="{{ organization.id }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Annuler
</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
@ -239,7 +292,7 @@
<div class="col-3 m-auto">
<div class="card "
data-controller="organization"
data-organization-activities-value = "true"
data-organization-activities-value="true"
data-organization-id-value="{{ organization.id }}">
<div class="card-header d-flex justify-content-between align-items-center border-0">