set up project CRUD
This commit is contained in:
parent
3e06a348ff
commit
2626d27288
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
</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>
|
||||
{% endfor %}
|
||||
</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 %}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue