247 lines
9.3 KiB
JavaScript
247 lines
9.3 KiB
JavaScript
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", "timestampSelect", "deletionSelect"];
|
|
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 form = event.target;
|
|
const formData = new FormData(form); // This automatically picks up the 'logo' file
|
|
|
|
// 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`
|
|
: `/project/new/ajax`;
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
// 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();
|
|
location.reload();
|
|
} else {
|
|
const result = await response.json();
|
|
if (response.status === 409) {
|
|
alert("Un projet avec ce nom existe déjà.");
|
|
} else {
|
|
alert(result.error || "Une erreur est survenue.");
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
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
|
|
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.");
|
|
}
|
|
}
|
|
} |