set up project CRUD

This commit is contained in:
Charles 2026-02-16 13:58:15 +01:00
parent 3e06a348ff
commit 2626d27288
14 changed files with 858 additions and 12 deletions

View File

@ -18,7 +18,7 @@ export default class extends Controller {
this.loadActivities();
setInterval(() => {
this.loadActivities();
}, 60000); // Refresh every 60 seconds
}, 300000); // Refresh every 5 minutes
}
if (this.tableValue && this.sadminValue) {
this.table();

View File

@ -0,0 +1,235 @@
import {Controller} from '@hotwired/stimulus';
import { Modal } from "bootstrap";
import {TabulatorFull as Tabulator} from 'tabulator-tables';
import {eyeIconLink, pencilIcon, TABULATOR_FR_LANG, trashIcon} from "../js/global.js";
export default class extends Controller {
static values = {
listProject : Boolean,
orgId: Number,
admin: Boolean
}
static targets = ["modal", "appList", "nameInput", "formTitle"];
connect(){
if(this.listProjectValue){
this.table();
}
this.modal = new Modal(this.modalTarget);
}
table(){
const columns = [
{title: "<b>ID</b> ", field: "id", visible: false},
{title: "<b>Nom du projet</b> ", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
{
title: "Applications",
field: "applications",
headerSort: false,
hozAlign: "left",
formatter: (cell) => {
const apps = cell.getValue();
if (!apps || apps.length === 0) {
return "<span class='text-muted' style='font-size: 0.8rem;'>Aucune</span>";
}
// Wrap everything in a flex container to keep them on one line
const content = apps.map(app => `
<div class="me-1" title="${app.name}">
<img src="${app.logoMiniUrl}"
alt="${app.name}"
style="height: 35px; width: 35px; object-fit: contain; border-radius: 4px;">
</div>
`).join('');
return `<div class="d-flex flex-wrap align-items-center">${content}</div>`;
}
}
];
// 2. Add the conditional column if admin value is true
if (this.adminValue) {
columns.push({
title: "<b>Base de données</b>",
field: "bddName",
hozAlign: "left",
},
{
title: "<b>Actions</b>",
field: "id",
width: 120,
hozAlign: "center",
headerSort: false,
formatter: (cell) => {
const id = cell.getValue();
// Return a button that Stimulus can listen to
return `<div class="d-flex gap-2 align-content-center">
<button class="btn btn-link p-0 border-0" data-action="click->project#openEditModal"
data-id="${id}">
${pencilIcon()}</button>
<button class="btn btn-link p-0 border-0" data-action="click->project#deleteProject"
data-id="${id}"> ${trashIcon()} </button>
</div>`;
}
});
}
const tabulator = new Tabulator("#tabulator-projectListOrganization", {
langs: TABULATOR_FR_LANG,
locale: "fr",
ajaxURL: `/project/organization/data`,
ajaxConfig: "GET",
pagination: true,
paginationMode: "remote",
paginationSize: 15,
ajaxParams: {orgId: this.orgIdValue},
ajaxResponse: (url, params, response) => response,
paginationDataSent: {page: "page", size: "size"},
paginationDataReceived: {last_page: "last_page"},
ajaxSorting: true,
ajaxFiltering: true,
filterMode: "remote",
ajaxURLGenerator: function(url, config, params) {
let queryParams = new URLSearchParams();
queryParams.append('orgId', params.orgId);
queryParams.append('page', params.page || 1);
queryParams.append('size', params.size || 15);
// Add filters
if (params.filter) {
params.filter.forEach(filter => {
queryParams.append(`filter[${filter.field}]`, filter.value);
});
}
return `${url}?${queryParams.toString()}`;
},
rowHeight: 60,
layout: "fitColumns", // activate French
columns
})
}
async loadApplications() {
try {
const response = await fetch('/application/data/all');
const apps = await response.json();
this.appListTarget.innerHTML = apps.map(app => `
<div class="col-md-4 mb-3">
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
${app.name}
</label>
</div>
</div>
`).join('');
} catch (error) {
this.appListTarget.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
}
}
async submitForm(event) {
event.preventDefault();
const formData = new FormData(event.target);
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');
}
const url = this.currentProjectId
? `/project/edit/${this.currentProjectId}/ajax`
: `/project/new/ajax`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.ok) {
this.modal.hide();
// Use Tabulator's setData() instead of reload() for better UX if possible
location.reload();
} else {
if (response.status === 409) {
alert("Un projet avec ce nom existe déjà. Veuillez choisir un nom différent.");
}
}
}
async openEditModal(event) {
const projectId = event.currentTarget.dataset.id;
this.currentProjectId = projectId;
this.modal.show();
this.nameInputTarget.disabled = true;
this.formTitleTarget.textContent = "Modifier le projet";
try {
// 1. Ensure checkboxes are loaded first
await this.loadApplications();
// 2. Fetch the project data
const response = await fetch(`/project/data/${projectId}`);
const project = await response.json();
// 3. Set the name
this.nameInputTarget.value = project.name;
// 4. Check the boxes
// We look for all checkboxes inside our appList target
const checkboxes = this.appListTarget.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
cb.checked = project.applications.includes(cb.value);
});
} catch (error) {
console.error("Error loading project data", error);
alert("Erreur lors de la récupération des données du projet.");
}
}
// Update your openCreateModal to reset the state
openCreateModal() {
this.currentProjectId = null;
this.modal.show();
this.nameInputTarget.disabled = false;
this.nameInputTarget.value = "";
this.formTitleTarget.textContent = "Nouveau Projet";
this.loadApplications();
}
async deleteProject(event) {
const projectId = event.currentTarget.dataset.id;
if (!confirm("Êtes-vous sûr de vouloir supprimer ce projet ?")) {
return;
}
try {
const response = await fetch(`/project/delete/${projectId}/ajax`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
if (response.ok) {
location.reload();
}
}catch (error) {
console.error("Error deleting project", error);
alert("Erreur lors de la suppression du projet.");
}
}
}

View File

@ -32,6 +32,25 @@ export function eyeIconLink(url) {
</a>`;
}
export function pencilIcon() {
return `
<span class="align-middle color-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
<path fill="currentColor" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5L13.5 4.793L14.793 3.5L12.5 1.207zm1.586 3L10.5 3.207L4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175l-.106.106l-1.528 3.821l3.821-1.528l.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/></svg>
</span>
`
}
export function trashIcon(url) {
return `
<span class="align-middle color-delete">
<svg xmlns="http://www.w3.org/2000/svg" width="35px" height="35px" viewBox="0 0 24 24">
<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>
</span>
`
}
export function deactivateUserIcon() {
return `<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
<path fill="currentColor" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2s-6.3 25.5 4.1 33.7l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L353.3 251.6C407.9 237 448 187.2 448 128C448 57.3 390.7 0 320 0c-69.8 0-126.5 55.8-128 125.2zm225.5 299.2C170.5 309.4 96 387.2 96 482.3c0 16.4 13.3 29.7 29.7 29.7h388.6c3.9 0 7.6-.7 11-2.1z"/>

View File

@ -150,6 +150,13 @@ body {
color: var(--primary-blue-dark);
}
.color-delete{
color: var(--delete) !important;
}
.color-delete-dark{
color: var(--delete-dark);
}
.btn-secondary{
background: var(--secondary);
color : #FFFFFF;

View File

@ -0,0 +1,37 @@
<?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 Version20260211145606 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('CREATE TABLE project (id SERIAL NOT NULL, organization_id INT NOT NULL, name VARCHAR(255) NOT NULL, applications JSON DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, modified_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, is_deleted BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_2FB3D0EE32C8A3DE ON project (organization_id)');
$this->addSql('COMMENT ON COLUMN project.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN project.modified_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE project ADD CONSTRAINT FK_2FB3D0EE32C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
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 CONSTRAINT FK_2FB3D0EE32C8A3DE');
$this->addSql('DROP TABLE project');
}
}

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 Version20260216092531 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 bdd_name 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 bdd_name');
}
}

View File

@ -127,4 +127,20 @@ class ApplicationController extends AbstractController
return new JsonResponse($data, Response::HTTP_OK);
}
#[Route(path: '/data/all', name: 'data_all', methods: ['GET'])]
public function getAllApplications(): JsonResponse
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
$applications = $this->entityManager->getRepository(Apps::class)->findAll();
$data = array_map(function($app) {
return [
'id' => $app->getId(),
'name' => $app->getName(),
'subDomain' => $app->getSubDomain(),
'logoMiniUrl' => $this->assetsManager->getUrl($app->getLogoMiniUrl()),
];
}, $applications);
return new JsonResponse($data, Response::HTTP_OK);
}
}

View File

@ -0,0 +1,162 @@
<?php
namespace App\Controller;
use App\Entity\Apps;
use App\Entity\Project;
use App\Repository\AppsRepository;
use App\Repository\OrganizationsRepository;
use App\Repository\ProjectRepository;
use App\Service\ProjectService;
use App\Service\UserService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Asset\Packages;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route(path: '/project', name: 'project_')]
final class ProjectController extends AbstractController
{
public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly OrganizationsRepository $organizationsRepository,
private readonly ProjectRepository $projectRepository,
private readonly ProjectService $projectService,
private readonly UserService $userService, private readonly AppsRepository $appsRepository)
{
}
#[Route('/', name: '_index', methods: ['GET'])]
public function index(): Response
{
return $this->render('project/index.html.twig', [
'controller_name' => 'ProjectController',
]);
}
#[Route('/new/ajax', name: '_new', methods: ['POST'])]
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);
}
$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);
}
if(!$this->projectService->isApplicationArrayValid($data['applications'])) {
return new JsonResponse(['error' => 'Invalid applications array'], Response::HTTP_BAD_REQUEST);
}
$project = new Project();
$project->setName($data['name']);
$project->setBddName($sanitizedDbName);
$project->setOrganization($org);
$project->setApplications($data['applications']);
$this->entityManager->persist($project);
$this->entityManager->flush();
return new JsonResponse(['message' => 'Project created successfully'], Response::HTTP_CREATED);
}
#[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']]);
if(!$org) {
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
}
$project = $this->projectRepository->findOneBy(['id' => $id]);
if(!$project) {
return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND);
}
$project->setApplications($data['applications']);
$project->setModifiedAt(new \DateTimeImmutable());
$this->entityManager->persist($project);
$this->entityManager->flush();
return new JsonResponse(['message' => 'Project updated successfully'], Response::HTTP_OK);
}
#[Route('/organization/data', name: '_organization_data', methods: ['GET'])]
public function organizationData(Request $request, Packages $assetPackage): JsonResponse {
$this->denyAccessUnlessGranted('ROLE_USER');
$page = $request->query->getInt('page', 1);
$size = $request->query->getInt('size', 15);
$filters = $request->query->all('filter');
$orgId = $request->query->get('orgId');
$org = $this->organizationsRepository->findOneBy(['id' => $orgId, 'isDeleted' => false]);
if(!$org) {
return new JsonResponse(['error' => 'Organization not found'], Response::HTTP_NOT_FOUND);
}
$paginator = $this->projectRepository->findProjectByOrganization($orgId, $page, $size, $filters);
$total = count($paginator);
$data = array_map(function (Project $project) use ($assetPackage) {
// Map ONLY the applications linked to THIS specific project
$projectApps = array_map(function($appId) use ($assetPackage) {
// Note: If $project->getApplications() returns IDs, we need to find the entities.
// If your Project entity has a ManyToMany relationship, use $project->getApps() instead.
$appEntity = $this->appsRepository->find($appId);
return $appEntity ? [
'id' => $appEntity->getId(),
'name' => $appEntity->getName(),
'logoMiniUrl' => $assetPackage->getUrl($appEntity->getLogoMiniUrl()),
] : null;
}, $project->getApplications() ?? []);
return [
'id' => $project->getId(),
'name' => ucfirst($project->getName()),
'applications' => array_filter($projectApps), // Remove nulls
'bddName' => $project->getBddName(),
'isActive' => $project->isActive(),
];
}, iterator_to_array($paginator));
return $this->json([
'data' => $data,
'total' => $total,
'last_page' => (int)ceil($total / $size),
]);
}
#[Route(path: '/data/{id}', name: '_project_data', methods: ['GET'])]
public function projectData(Request $request, int $id): JsonResponse{
$this->denyAccessUnlessGranted('ROLE_USER');
$project = $this->projectRepository->findOneBy(['id' => $id]);
if(!$project) {
return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND);
}
return new JsonResponse([
'id' => $project->getId(),
'name' => ucfirst($project->getName()),
'applications' => $project->getApplications(),
]);
}
#[Route(path: '/delete/{id}/ajax', name: '_delete', methods: ['POST'])]
public function delete(int $id): JsonResponse {
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
$project = $this->projectRepository->findOneBy(['id' => $id]);
if(!$project) {
return new JsonResponse(['error' => 'Project not found'], Response::HTTP_NOT_FOUND);
}
$project->setIsDeleted(true);
$this->entityManager->persist($project);
$this->entityManager->flush();
return new JsonResponse(['message' => 'Project deleted successfully'], Response::HTTP_OK);
}
}

View File

@ -64,12 +64,19 @@ class Organizations
#[ORM\Column(length: 4, nullable: true)]
private ?string $projectPrefix = null;
/**
* @var Collection<int, Project>
*/
#[ORM\OneToMany(targetEntity: Project::class, mappedBy: 'organization')]
private Collection $projects;
public function __construct()
{
$this->apps = new ArrayCollection();
$this->actions = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
$this->userOrganizatonApps = new ArrayCollection();
$this->projects = new ArrayCollection();
}
public function getId(): ?int
@ -271,4 +278,34 @@ class Organizations
return $this;
}
/**
* @return Collection<int, Project>
*/
public function getProjects(): Collection
{
return $this->projects;
}
public function addProject(Project $project): static
{
if (!$this->projects->contains($project)) {
$this->projects->add($project);
$project->setOrganization($this);
}
return $this;
}
public function removeProject(Project $project): static
{
if ($this->projects->removeElement($project)) {
// set the owning side to null (unless already changed)
if ($project->getOrganization() === $this) {
$project->setOrganization(null);
}
}
return $this;
}
}

149
src/Entity/Project.php Normal file
View File

@ -0,0 +1,149 @@
<?php
namespace App\Entity;
use App\Repository\ProjectRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ProjectRepository::class)]
class Project
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\ManyToOne(inversedBy: 'projects')]
#[ORM\JoinColumn(nullable: false)]
private ?Organizations $organization = null;
#[ORM\Column(nullable: true)]
private ?array $applications = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column]
private ?\DateTimeImmutable $modifiedAt = null;
#[ORM\Column]
private ?bool $isActive = null;
#[ORM\Column]
private ?bool $isDeleted = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $bddName = null;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
$this->modifiedAt = new \DateTimeImmutable();
$this->isActive = true;
$this->isDeleted = false;
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getOrganization(): ?Organizations
{
return $this->organization;
}
public function setOrganization(?Organizations $organization): static
{
$this->organization = $organization;
return $this;
}
public function getApplications(): ?array
{
return $this->applications;
}
public function setApplications(?array $applications): static
{
$this->applications = $applications;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getModifiedAt(): ?\DateTimeImmutable
{
return $this->modifiedAt;
}
public function setModifiedAt(\DateTimeImmutable $modifiedAt): static
{
$this->modifiedAt = $modifiedAt;
return $this;
}
public function isActive(): ?bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): static
{
$this->isActive = $isActive;
return $this;
}
public function isDeleted(): ?bool
{
return $this->isDeleted;
}
public function setIsDeleted(bool $isDeleted): static
{
$this->isDeleted = $isDeleted;
return $this;
}
public function getBddName(): ?string
{
return $this->bddName;
}
public function setBddName(string $bddName): static
{
$this->bddName = $bddName;
return $this;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace App\Repository;
use App\Entity\Project;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Project>
*/
class ProjectRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Project::class);
}
// /**
// * @return Project[] Returns an array of Project objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('p.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Project
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
public function findProjectByOrganization(int $organizationId, int $page, int $size, array $filters)
{
$qb = $this->createQueryBuilder('p')
->where('p.organization = :orgId')
->andWhere('p.isDeleted = :del')
->setParameter('orgId', $organizationId)
->setParameter('del', false);
if (!empty($filters['name'])) {
$qb->andWhere('p.name LIKE :name')
->setParameter('name', '%' . strtoLower($filters['name']) . '%');
}
return $qb->setFirstResult(($page - 1) * $size)
->setMaxResults($size)
->getQuery()
->getResult();
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Service;
use App\Repository\AppsRepository;
use Symfony\Component\String\Slugger\AsciiSlugger;
class ProjectService{
public function __construct(private readonly AppsRepository $appsRepository)
{
}
/** Function that will return the project name.
* Project name are build using the project prefix field present in the organization entity and the normalized project name.
* The normalized project name is the project name with all spaces replaced by underscores and all characters in lowercase.
* For example, if the project prefix is "yumi" and the project name is "My Project", the project name will be "yumi_my_project".
*
* @param string $projectName The name of the project.
* @param string $projectPrefix The prefix of the project.
* @return string The project name.
*/
public function getProjectDbName(string $projectName, string $projectPrefix): string
{
$slugger = new AsciiSlugger();
$slug = $slugger->slug($projectName, '_')->lower()->toString();
// \d matches any digit character, equivalent to [0-9]. So, the regular expression '/\d/' will match any digit in the string.
$str = preg_replace('/\d/', '', $slug);
return $projectPrefix . '_' . $str;
}
public function isApplicationArrayValid(array $applicationArray): bool
{
foreach ($applicationArray as $app) {
$app = (int) $app;
if (empty($app) || $app <= 0 || empty($this->appsRepository->findOneBy(['id' => $app]))) {
return false;
}
}
return true;
}
}

View File

@ -1,6 +1,7 @@
{% extends 'base.html.twig' %}
{% block body %}
{% 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 %}
@ -18,7 +19,7 @@
<h1 class="mb-4 ms-3">{{ organization.name|title }} - Dashboard</h1>
</div>
<div class="d-flex gap-2">
{% if is_granted("ROLE_SUPER_ADMIN") %}
{% if isSA %}
<a href="{{ path('organization_edit', {'id': organization.id}) }}" class="btn btn-primary">Gérer
l'organisation</a>
<form method="POST" action="{{ path('organization_delete', {'id': organization.id}) }}"
@ -101,20 +102,61 @@
</div>
{# APPLICATION ROW #}
{# TODO: Weird gap not going away #}
<div class="row mb-3 ">
{% for application in applications %}
<div class="col-6 mb-3">
{% include 'application/appSmall.html.twig' with {
application: application
} %}
{# 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"
data-project-org-id-value="{{ organization.id }}"
data-project-admin-value="{{ isSA ? 'true' : 'false' }}">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>Mes Projets</h2>
{% if is_granted("ROLE_SUPER_ADMIN") %}
{# Trigger for the Modal #}
<button type="button" class="btn btn-primary" data-action="click->project#openCreateModal">
Crée un projet
</button>
{% endif %}
</div>
<div class="card-body">
<div id="tabulator-projectListOrganization">
</div>
{% endfor %}
</div>
<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">
<h5 class="modal-title" data-project-target="formTitle">Nouveau Projet</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form data-action="submit->project#submitForm">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Nom du projet</label>
<input type="text" name="name"
data-project-target="nameInput"
class="form-control" required>
</div>
<label class="form-label">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>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{# Activities col #}
<div class="col-3 m-auto">
<div class="card border-0"
<div class="card "
data-controller="organization"
data-organization-activities-value = "true"
data-organization-id-value="{{ organization.id }}">
@ -122,7 +164,7 @@
<div class="card-header d-flex justify-content-between align-items-center border-0">
<h3>Activité récente</h3>
<button class="btn btn-sm btn-outline-secondary" data-action="organization#loadActivities">
<button class="btn btn-sm btn-primary" data-action="organization#loadActivities">
<i class="fas fa-sync"></i> Rafraîchir
</button>
</div>
@ -148,6 +190,7 @@
{% endblock %}

View File

@ -0,0 +1,5 @@
{% extends 'base.html.twig' %}
{% block body %}
{% endblock %}