Compare commits

...

4 Commits

Author SHA1 Message Date
Julien Rosset 4e299630b6 Add controller for material types 2 months ago
Julien Rosset 067786a3ea Fix routes for User 2 months ago
Julien Rosset 8fef71ef6c Preparing assets 2 months ago
Julien Rosset eca2464430 Migration to Symfony 7.3 2 months ago

@ -1,12 +1,18 @@
//region CSS //region CSS
import './styles/app.scss'; import './styles/app.scss';
//endregion //endregion
//region Bootstrap
import 'bootstrap';
//endregion
//region jQuery //region jQuery
import 'jqueryLocal'; // Declare $ as a global variable, accessible in all files import 'jqueryLocal'; // Declare $ as a global variable, accessible in all files
//endregion //endregion
//region Bootstrap
import '@popperjs/core';
import {Tooltip} from 'bootstrap';
//region Tooltips
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
[...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl));
//endregion
//endregion
//region FontAwesome //region FontAwesome
import 'fontawesome'; import 'fontawesome';
//endregion //endregion
import 'modalDynamic';

@ -1,4 +1,15 @@
{ {
"controllers": {}, "controllers": {
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": false,
"fetch": "eager"
}
}
},
"entrypoints": [] "entrypoints": []
} }

@ -1,148 +0,0 @@
//region Datatables.net
import 'datatables.net';
import 'datatables.net-dt/css/dataTables.dataTables.min.css';
//endregion
//region Bootstrap 5 styling for Datatables.net
import 'datatables.net-bs5';
import 'datatables.net-bs5/css/dataTables.bootstrap5.min.css';
//endregion
//region Custom style for Datatables.net
import './styles/datatables.scss';
//endregion
import 'jqueryLocal';
$(function () {
$('table.table-datatable').each(function () {
const self = $(this);
//region Options du datatable
let options = {
paging: self.data('sortPaging', false),
pageLength: self.data('sortPerPage', 50),
processing: true,
layout: self.data(
'sortLayout',
{
topStart: null,
topEnd: null,
bottomStart: null,
bottomEnd: 'paging',
},
),
};
//endregion
//region Gestion tri initial
let initialSort = [];
$('> thead > tr > th[data-sort-onLoad]', this).each(function () {
let self = $(this);
initialSort.push(
{
index: self.index(),
priority: self.data('sortOnload'),
direction: self.data('sortOrder', 'asc').toLowerCase(),
},
);
});
initialSort.sort(function (sort1, sort2) {
if (sort1.priority === sort2.priority) {
return sort1.index - sort2.index;
}
return sort1.priority - sort2.priority;
});
if (initialSort.length > 0) {
options.order = [];
initialSort.forEach(function (sortOrder) {
options.order.push([sortOrder.index, sortOrder.direction]);
});
}
//endregion
//region Gestion sens de tri (1er clic ou initial si concerné)
let descSort = [];
$('> thead > tr > th[data-sort-order]', this).each(function () {
const self = $(this);
if (self.data('sortOrder', 'asc').toLowerCase() === 'desc') {
descSort.push(self.index());
}
});
if (descSort.length > 0) {
if (!options.hasOwnProperty('columnDefs')) {
options.columnDefs = [];
}
options.columnDefs.push(
{
targets: descSort,
orderSequence: ['desc', 'asc'],
},
);
}
//endregion
//region Gestion désactivation tri
let disabledSort = [];
$('> thead > tr > th[data-sort]', this).each(function () {
const self = $(this);
if (!self.data('sort', true)) {
disabledSort.push(self.index());
}
});
if (disabledSort.length > 0) {
if (!options.hasOwnProperty('columnDefs')) {
options.columnDefs = [];
}
options.columnDefs.push(
{
targets: disabledSort,
orderable: false,
},
);
}
//endregion
//region Gestion nom des colonnes
const columnsName = $('> thead > tr > th[data-sort-name]', this);
if (columnsName.length > 0) {
if (!options.hasOwnProperty('columnDefs')) {
options.columnDefs = [];
}
columnsName.each(function () {
const self = $(this);
const name = self.data('sortName', '');
if (name !== '') {
options.columnDefs.push(
{
targets: self.index(),
name: name,
},
);
}
});
}
//endregion
//region Gestion de AJAX
const ajaxUrl = self.data('sortAjax', false);
if (ajaxUrl !== false) {
options.serverSide = true;
options.ajax = {
url: ajaxUrl,
type: 'POST',
//createCDATASection: '',
cache: self.data('sortAjaxCache', false),
};
options.columns = [];
columnsName.each(function () {
const self = $(this);
const name = self.data('sortName', '');
if (name !== '') {
options.columns[self.index()] = {
data: name,
};
}
});
}
//endregion
self.DataTable(options);
});
});

@ -0,0 +1,329 @@
//region Datatables.net
import 'datatables.net';
import 'datatables.net-dt/css/dataTables.dataTables.min.css';
//endregion
//region Bootstrap 5 styling for Datatables.net
import 'datatables.net-bs5';
import 'datatables.net-bs5/css/dataTables.bootstrap5.min.css';
//endregion
//region Custom style for Datatables.net
import './styles/datatables2.scss';
//endregion
import 'jqueryLocal';
import Utils from 'utils';
/**
* Chargement automatique des éléments : tableaux, {@link https://datatables.net/ DataTables (v2)}
*
* Valide seulement si le JS "DataTable" est bien présent\
* S'instancie automatiquement sur les <table class="table-datatable2">
*
* Options :
* * table :
* * data-sort-paging : boolean\
*     Pagination gérée ?\
*     Défaut : faux
* * data-sort-per-page : integer\
*     Nombre d'éléments par défaut par page\
*     Défaut : 50
* * data-sort-layout : string (JSON)\
*     Le JSON du modèle du tableau (https://datatables.net/reference/option/layout)\
*     Défaut : pagination (si active) en bas à droite
* * data-sort-ajax : string|false\
*     Faux si pas de requête AJAX géré\
*     Sinon l'URL AJAX appelée\
*         Bien penser à définir les "data-sort-name" sur les <th>\
*     Défaut : false
* * data-sort-ajax-type : string\
*     Le type de la requête AJAX (GET, POST, etc.)\
*     Défaut : POST
* * data-sort-ajax-cache : boolean\
*     Mise en cache du résultat des requêtes AJAX ?\
*     Défaut : faux
* * data-sort-lang : string (JSON)\
*     Le JSON des traductions
* * data-sort-lang-url : string (url)\
*     L'URL renvoyant le JSON des traductions\
*     Ignore si "data-sort-lang" renseigné
* * data-sort-lang-code : string\
*     Code langue (plugin, voir https://datatables.net/plug-ins/i18n/French.html#Browser-loading-/-CDN)\
*         Exemple avec "fr-FR" : chargera "//cdn.datatables.net/plug-ins/2.2.2/i18n/fr-FR.json"\
*     Ignoré si "data-sort-lang" ou "data-sort-lang-url" est renseigné
* * data-sort-child : string (HTML)|false\
*     Faux si pas de lignes enfants gérées\
*     Sinon le contenu HTML de la ligne enfant\
*     Ignoré si supplanté par la version sur les <tr>\
*     Défaut : false
* * data-sort-child-ajax : string (url)|false\
*     Faux si pas de lignes enfants gérées\
*     L'URL des requêtes AJAX pour les lignes enfants\
*     Ignoré si "data-sort-child" est renseigné ou supplanté par la version sur les <tr>\
*     Défaut : false
* * data-sort-child-ajax-type : string\
*     Le type de la requête AJAX pour les lignes enfants (GET, POST, etc.)\
*     Ignoré si supplanté par la version sur les <tr>\
*     Défaut : POST
* * data-sort-child-ajax-cache : boolean\
*     Mise en cache du résultat des requêtes AJAX pour les lignes enfants ?\
*     Ignoré si supplanté par la version sur les <tr>\
*     Défaut : faux
* * thead th :
* * data-sort-onLoad : integer\
*     Ordre de tri de la colonne pour le tri initial/au chargement
* * data-sort-order : string ("asc", "desc")\
*     Sens du tri de la colonne lors du 1er clic (et lors du tri initial/au chargement si "data-sort-onLoad" également renseigné)
* * data-sort : boolean\
*     Activer le tri sur la colonne ?\
*     Défaut : vrai
* * data-sort-name : string\
*     Le nom de la colonne dans la source de données (AJAX : cf. data-sort-ajax)
* * data-sort-name-value : boolean\
*     La colonne à une valeur de tri différente de son affichage ?\
*     Si vrai attends une paire display/sort dans le JSON (exemple : https://datatables.net/examples/ajax/orthogonal-data.html)\
*         Cf. SolulogSAS\BaseSite\Web\DataTables2_ColumnWithSortValue\
*     Défaut : faux
* * data-sort-child : string (HTML)|false\
*     Faux si pas de lignes enfants gérées\
*     Sinon le contenu HTML de la ligne enfant\
*     Défaut : false
* * data-sort-child-ajax : string (url)|false\
*     Faux si pas de lignes enfants gérées\
*     L'URL des requêtes AJAX pour les lignes enfants\
*     Ignoré si "data-sort-child" est renseigné\
*     Défaut : false
* * data-sort-child-ajax-id : any\
*     L'ID ajax de la ligne enfant (transmis dans "ajaxId")\
*     Défaut : {néant}
* * data-sort-child-ajax-type : string\
*     Le type de la requête AJAX pour les lignes enfants (GET, POST, etc.)\
*     Défaut : POST
* * data-sort-child-ajax-cache : boolean\
*     Mise en cache du résultat des requêtes AJAX pour les lignes enfants ?\
*     Défaut : faux
* * tbody td :
* * data-order : any\
*     La valeur utilisée pour le tri
* * data-filter : any\
*     La valeur utilisée pour le filtrage
*
* @param {Document|jQuery} page
*/
function datatables2 (page) {
$('table.table-datatable2', page).each(function () {
const self = $(this);
//region Options du datatable
let options = {
paging: self.dataDefault('sortPaging', false),
pageLength: self.dataDefault('sortPerPage', 50),
processing: true,
layout:
self.dataDefault('sortLayout',
{
topStart: null,
topEnd: null,
bottomStart: null,
bottomEnd: 'paging',
},
),
};
//endregion
//region Gestion traduction
let lang = self.dataDefault('sortLang', undefined);
if (Utils.isUndefined(lang)) {
let langUrl = self.dataDefault('sortLangUrl', undefined);
if (Utils.isUndefined(langUrl)) {
const langCode = self.dataDefault('sortLangCode', undefined);
if (!Utils.isUndefined(langCode)) {
langUrl = '//cdn.datatables.net/plug-ins/2.2.2/i18n/' + langCode + '.json';
}
}
if (!Utils.isUndefined(langUrl)) {
lang = {url: langUrl};
}
}
if (!Utils.isUndefined(lang)) {
options.language = lang;
}
//endregion
//region Gestion tri initial
let initialSort = [];
$('> thead > tr > th[data-sort-onLoad]', self).each(function () {
let self = $(this);
initialSort.push(
{
index: self.index(),
priority: self.dataDefault('sortOnload', 0),
direction: self.dataDefault('sortOrder', 'asc').toLowerCase(),
},
);
});
initialSort.sort(function (sort1, sort2) {
if (sort1.priority === sort2.priority) {
return sort1.index - sort2.index;
}
return sort1.priority - sort2.priority;
});
if (initialSort.length > 0) {
options.order = [];
initialSort.forEach(function (sortOrder) {
options.order.push([sortOrder.index, sortOrder.direction]);
});
}
//endregion
//region Gestion sens de tri (1er clic ou initial si concerné)
let descSort = [];
$('> thead > tr > th[data-sort-order]', self).each(function () {
const self = $(this);
if (self.dataDefault('sortOrder', 'asc').toLowerCase() === 'desc') {
descSort.push(self.index());
}
});
if (descSort.length > 0) {
if (!options.hasOwnProperty('columnDefs')) {
options.columnDefs = [];
}
options.columnDefs.push(
{
targets: descSort,
orderSequence: ['desc', 'asc'],
},
);
}
//endregion
//region Gestion désactivation tri
let disabledSort = [];
$('> thead > tr > th[data-sort]', self).each(function () {
const self = $(this);
if (!self.dataDefault('sort', true)) {
disabledSort.push(self.index());
}
});
if (disabledSort.length > 0) {
if (!options.hasOwnProperty('columnDefs')) {
options.columnDefs = [];
}
options.columnDefs.push(
{
targets: disabledSort,
orderable: false,
},
);
}
//endregion
//region Gestion nom des colonnes
const columnsName = $('> thead > tr > th[data-sort-name]', self);
if (columnsName.length > 0) {
if (!options.hasOwnProperty('columnDefs')) {
options.columnDefs = [];
}
columnsName.each(function () {
const self = $(this);
const name = self.dataDefault('sortName', '');
if (name !== '') {
options.columnDefs.push(
{
targets: self.index(),
name: name,
},
);
}
});
}
//endregion
//region Gestion de AJAX
const ajaxUrl = self.dataDefault('sortAjax', false);
if (ajaxUrl !== false) {
options.serverSide = true;
options.ajax = {
url: ajaxUrl,
type: self.dataDefault('sortAjaxType', 'POST'),
cache: self.dataDefault('sortAjaxCache', false),
};
options.columns = [];
columnsName.each(function () {
const self = $(this);
const name = self.dataDefault('sortName', '');
const nameValue = self.dataDefault('sortNameValue', false);
if (name !== '') {
options.columns[self.index()] = {
data: nameValue
? {_: name + '.display', sort: name + '.sort'}
: name,
};
}
});
}
//endregion
const datatable = self.DataTable(options);
//region Gestion lignes enfants
const child = self.dataDefault('sortChild', false);
const childAjaxUrl = self.dataDefault('sortChildAjax', false);
const childAjaxType = self.dataDefault('sortChildAjaxType', 'POST');
const childAjaxCache = self.dataDefault('sortChildAjaxCache', false);
if (child === true || childAjaxUrl !== false) {
const showData = function (dtRow, childData) {
const parentHtml = '<div></div>';
const childJquery = $(parentHtml).append(childData);
datatables2(childJquery);
//noinspection JSUnresolvedReference
dtRow.child(childJquery).show();
};
self.on('click', '> tbody > tr:not([data-dt-row!=""][data-dt-row]) > td', function () {
//region Obtention ligne
const row = $(this).parents('tr').first();
const dtRow = datatable.row(row);
//endregion
//region Lignes enfants déjà affichées ? Les masques
//noinspection JSUnresolvedReference
if (dtRow.child.isShown()) {
//noinspection JSUnresolvedReference
dtRow.child.hide();
return;
}
//endregion
//region Données enfants directement spécifiées
const childData = row.dataDefault('sortChild', false);
if (childData !== false) {
showData(dtRow, childData);
return;
}
//endregion
//region Données enfants via AJAX
const rowChildAjaxUrl = row.dataDefault('sortChildAjax', childAjaxUrl);
if (Utils.isUndefined(rowChildAjaxUrl) || rowChildAjaxUrl === false) {
return;
}
$.ajax(
{
type: row.dataDefault('sortChildAjaxType', childAjaxType),
url: rowChildAjaxUrl,
data: {
ajaxId: row.dataDefault('sortChildAjaxId', undefined),
},
cache: row.dataDefault('sortChildAjaxCache', childAjaxCache),
},
)
.done(childData => {
showData(dtRow, childData);
});
//endregion
});
}
//endregion
});
}
$(function () {
datatables2(document);
});

@ -8,18 +8,16 @@ window.jQuery = $; // Ensure global “jQuery” property available: necess
(function ($) { (function ($) {
//region .data //region .data
const jqueryDataOrig = $.fn.data;
/** /**
* Get a DOM "data" value * Get a DOM data value, with a default value
* *
* @param {string} key The "data" key * @param {string} key The data key
* @param {any} defaultValue The default value * @param {any} defaultValue The default value
* *
* @return {any} The "data" value * @return {any} The data value
*/ */
$.fn.data = function (key, defaultValue = undefined) { $.fn.dataDefault = function (key, defaultValue = undefined) {
const value = jqueryDataOrig.apply(this, [key]); const value = this.data(key);
if (Utils.isUndefined(value)) { if (Utils.isUndefined(value)) {
return defaultValue; return defaultValue;
} }

@ -0,0 +1,26 @@
import 'bootstrap';
import 'jqueryLocal';
import Utils from 'utils';
$(function () {
$('.modal-dynamic').on('show.bs.modal', function (event) {
const dialog = $(this);
const sender = $(event.relatedTarget);
const title = sender.dataDefault('modalDynamicTitle');
const body = sender.dataDefault('modalDynamicBody');
const linkUrl = sender.dataDefault('modalDynamicLinkUrl', sender.matchTag('a') ? sender.attr('href') : undefined);
if (!Utils.isUndefined(title)) {
$('.modal-title', dialog).html(title);
}
if (!Utils.isUndefined(body)) {
$('.modal-body', dialog).html(body);
}
if (!Utils.isUndefined(linkUrl)) {
$('a.modal-confirm-link', dialog).attr('href', linkUrl);
}
return true;
});
});

@ -16,7 +16,56 @@ $alert-margin-bottom : 0.5rem;
@import '../../vendor/twbs/bootstrap/scss/variables-dark'; @import '../../vendor/twbs/bootstrap/scss/variables-dark';
//endregion //endregion
//region Maps personnalisés //region Maps personnalisés
$utilities : (
//region Largeur max
'max-width': (
property: max-width,
class: mw,
values: (
25: 25%,
50: 50%,
75: 75%,
100: 100%,
)
),
//endregion
//region Hauteur max
'max-height': (
property: max-height,
class: mh,
values: (
25: 25%,
50: 50%,
75: 75%,
100: 100%,
)
),
//endregion
//region Largeur min
'min-width': (
property: min-width,
class: mnw,
values: (
25: 25%,
50: 50%,
75: 75%,
100: 100%,
)
),
//endregion
//region Hauteur min
'min-height': (
property: min-height,
class: mnh,
values: (
25: 25%,
50: 50%,
75: 75%,
100: 100%,
)
),
//endregion
);
//endregion //endregion
//region Reste de la configuration de Bootstrap //region Reste de la configuration de Bootstrap
@import '../../vendor/twbs/bootstrap/scss/maps'; @import '../../vendor/twbs/bootstrap/scss/maps';

@ -5,3 +5,8 @@
.required:not(.form-check-label) { .required:not(.form-check-label) {
color : var(--bs-red); color : var(--bs-red);
} }
.fit-content {
white-space : nowrap;
width : 1px;
}

@ -1,36 +0,0 @@
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order::after,
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order::after,
table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order::after,
table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order::after,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order::after,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order::after,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order::after,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order::after {
margin-top : 3px;
}
table.dataTable thead > tr > th.dt-orderable-asc span.dt-column-order,
table.dataTable thead > tr > th.dt-orderable-desc span.dt-column-order,
table.dataTable thead > tr > th.dt-ordering-asc span.dt-column-order,
table.dataTable thead > tr > th.dt-ordering-desc span.dt-column-order,
table.dataTable thead > tr > td.dt-orderable-asc span.dt-column-order,
table.dataTable thead > tr > td.dt-orderable-desc span.dt-column-order,
table.dataTable thead > tr > td.dt-ordering-asc span.dt-column-order,
table.dataTable thead > tr > td.dt-ordering-desc span.dt-column-order {
position : relative;
left : 10px;
top : none;
bottom : none;
}
div.dt-container .dt-paging .dt-paging-button {
padding : 0;
margin : 0;
}
table.dataTable th.dt-type-numeric,
table.dataTable th.dt-type-date,
table.dataTable td.dt-type-numeric,
table.dataTable td.dt-type-date {
text-align : left;
}

@ -0,0 +1,35 @@
table.table-datatable2 {
& > thead > tr {
& > th.dt-orderable-asc span.dt-column-title {
margin-right : 5px;
}
& > th.dt-orderable-asc span.dt-column-order,
& > th.dt-orderable-desc span.dt-column-order {
position : relative;
left : 0;
top : 0;
bottom : 0;
&::after {
margin-top : 3px;
}
}
}
& > tbody > tr[data-dt-row]:not([data-dt-row='']) > td {
padding : 0.75rem;
}
div.dt-container .dt-paging .dt-paging-button {
padding : 0;
margin : 0;
}
th.dt-type-numeric,
th.dt-type-date,
td.dt-type-numeric,
td.dt-type-date {
text-align : left;
}
}

@ -13,37 +13,37 @@
"doctrine/orm": "^3.3", "doctrine/orm": "^3.3",
"phpdocumentor/reflection-docblock": "^5.6", "phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1", "phpstan/phpdoc-parser": "^2.1",
"symfony/asset": "7.2.*", "symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.2.*", "symfony/asset-mapper": "7.3.*",
"symfony/console": "7.2.*", "symfony/console": "7.3.*",
"symfony/doctrine-messenger": "7.2.*", "symfony/doctrine-messenger": "7.3.*",
"symfony/dotenv": "7.2.*", "symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.2.*", "symfony/expression-language": "7.3.*",
"symfony/flex": "^2", "symfony/flex": "^2",
"symfony/form": "7.2.*", "symfony/form": "7.3.*",
"symfony/framework-bundle": "7.2.*", "symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.2.*", "symfony/http-client": "7.3.*",
"symfony/intl": "7.2.*", "symfony/intl": "7.3.*",
"symfony/mailer": "7.2.*", "symfony/mailer": "7.3.*",
"symfony/messenger": "7.2.*", "symfony/messenger": "7.3.*",
"symfony/mime": "7.2.*", "symfony/mime": "7.3.*",
"symfony/monolog-bundle": "^3.0", "symfony/monolog-bundle": "^3.0",
"symfony/notifier": "7.2.*", "symfony/notifier": "7.3.*",
"symfony/process": "7.2.*", "symfony/process": "7.3.*",
"symfony/property-access": "7.2.*", "symfony/property-access": "7.3.*",
"symfony/property-info": "7.2.*", "symfony/property-info": "7.3.*",
"symfony/rate-limiter": "7.2.*", "symfony/rate-limiter": "7.3.*",
"symfony/runtime": "7.2.*", "symfony/runtime": "7.3.*",
"symfony/security-bundle": "7.2.*", "symfony/security-bundle": "7.3.*",
"symfony/serializer": "7.2.*", "symfony/serializer": "7.3.*",
"symfony/stimulus-bundle": "^2.25", "symfony/stimulus-bundle": "^2.25",
"symfony/string": "7.2.*", "symfony/string": "7.3.*",
"symfony/translation": "7.2.*", "symfony/translation": "7.3.*",
"symfony/twig-bundle": "7.2.*", "symfony/twig-bundle": "7.3.*",
"symfony/ux-turbo": "^2.25", "symfony/ux-turbo": "^2.25",
"symfony/validator": "7.2.*", "symfony/validator": "7.3.*",
"symfony/web-link": "7.2.*", "symfony/web-link": "7.3.*",
"symfony/yaml": "7.2.*", "symfony/yaml": "7.3.*",
"symfonycasts/sass-bundle": "^0.8.2", "symfonycasts/sass-bundle": "^0.8.2",
"twbs/bootstrap": "^5.3", "twbs/bootstrap": "^5.3",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
@ -97,17 +97,17 @@
"extra": { "extra": {
"symfony": { "symfony": {
"allow-contrib": false, "allow-contrib": false,
"require": "7.2.*" "require": "7.3.*"
} }
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9.5", "phpunit/phpunit": "^9.5",
"symfony/browser-kit": "7.2.*", "symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.2.*", "symfony/css-selector": "7.3.*",
"symfony/debug-bundle": "7.2.*", "symfony/debug-bundle": "7.3.*",
"symfony/maker-bundle": "^1.0", "symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^7.2", "symfony/phpunit-bridge": "^7.3",
"symfony/stopwatch": "7.2.*", "symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.2.*" "symfony/web-profiler-bundle": "7.3.*"
} }
} }

809
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true

@ -0,0 +1,5 @@
symfonycasts_sass:
# Path to your Sass root file
root_sass:
- '%kernel.project_dir%/assets/styles/app.scss'
- '%kernel.project_dir%/assets/styles/datatables2.scss'

@ -16,10 +16,13 @@ return [
'path' => './assets/app.js', 'path' => './assets/app.js',
'entrypoint' => true, 'entrypoint' => true,
], ],
'datatables' => [ 'datatables2' => [
'path' => './assets/datatables.js', 'path' => './assets/datatables2.js',
'entrypoint' => true, 'entrypoint' => true,
], ],
'modalDynamic' => [
'path' => './assets/modalDynamic.js',
],
'jqueryLocal' => [ 'jqueryLocal' => [
'path' => './assets/jqueryLocal.js', 'path' => './assets/jqueryLocal.js',
], ],
@ -66,4 +69,13 @@ return [
'version' => '5.3.6', 'version' => '5.3.6',
'type' => 'css', 'type' => 'css',
], ],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
],
]; ];

@ -0,0 +1,89 @@
<?php
namespace App\Controller\Config;
use App\Entity\MaterialType;
use App\Form\Config\MaterialTypeEditForm;
use App\Misc\FlashType;
use App\Repository\MaterialTypeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
/**
* Controllers for the configuration pages
*/
#[Route('/Config/MaterialType')]
class MaterialTypeController extends AbstractController {
private readonly EntityManagerInterface $entityManager;
/**
* @var MaterialTypeRepository The material type repository
*/
private readonly MaterialTypeRepository $materialTypeRepository;
/**
* Initialisation
*
* @param MaterialTypeRepository $materialTypeRepository The material type repository
*/
public function __construct (EntityManagerInterface $entityManager, MaterialTypeRepository $materialTypeRepository) {
$this->entityManager = $entityManager;
$this->materialTypeRepository = $materialTypeRepository;
}
/**
* List of material types
*
* @return Response The response
*/
#[Route('/', name: 'config_materialType_list', alias: 'config_materialType')]
public function list (): Response {
return $this->render(
'Config/MaterialType/List.html.twig',
[
'materialTypes' => $this->materialTypeRepository->findAll(),
]
);
}
/**
* Edit/Create a material type
*
* @return Response The response
*/
#[Route('/Create', name: 'config_materialType_create')]
#[Route('/Edit-{id}', name: 'config_materialType_edit')]
public function edit (Request $request, ?MaterialType $materialType = null): Response {
$materialType ??= new MaterialType();
$form = $this->createForm(MaterialTypeEditForm::class, $materialType);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->entityManager->persist($materialType);
$this->entityManager->flush();
$this->addFlash(FlashType::SUCCESS, 'Le type de matériau a bien été enregistré.');
return $this->redirectToRoute('config_materialType_list');
}
return $this->render('Config/MaterialType/Edit.html.twig', [
'form' => $form->createView(),
]);
}
/**
* Delete a material type
*
* @return Response The response
*/
#[Route('/Delete-{id}', name: 'config_materialType_delete')]
public function delete (MaterialType $materialType): Response {
$this->entityManager->remove($materialType);
$this->entityManager->flush();
$this->addFlash(FlashType::SUCCESS, 'Le type de matériau a bien été supprimé.');
return $this->redirectToRoute('config_materialType_list');
}
}

@ -26,7 +26,7 @@ use Throwable;
/** /**
* Controllers for user pages * Controllers for user pages
*/ */
#[Route('/user')] #[Route('/User')]
class UserController extends AbstractController { class UserController extends AbstractController {
/** /**
* @var TranslatorInterface The translator service * @var TranslatorInterface The translator service
@ -59,7 +59,7 @@ class UserController extends AbstractController {
* *
* @throws TransportExceptionInterface * @throws TransportExceptionInterface
*/ */
#[Route('/signUp', name: 'user_signUp')] #[Route('/SignUp', name: 'user_signUp')]
public function signUp (Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response { public function signUp (Request $request, UserPasswordHasherInterface $userPasswordHasher, EntityManagerInterface $entityManager): Response {
if (($response = $this->connectedUserService->checkNotConnected())) { if (($response = $this->connectedUserService->checkNotConnected())) {
return $response; return $response;
@ -99,7 +99,7 @@ class UserController extends AbstractController {
* *
* @return Response The response * @return Response The response
*/ */
#[Route(path: '/signIn', name: 'user_signIn')] #[Route(path: '/SignIn', name: 'user_signIn')]
public function login (AuthenticationUtils $authenticationUtils): Response { public function login (AuthenticationUtils $authenticationUtils): Response {
if (($response = $this->connectedUserService->checkNotConnected())) { if (($response = $this->connectedUserService->checkNotConnected())) {
return $response; return $response;
@ -123,7 +123,7 @@ class UserController extends AbstractController {
* *
* @return void * @return void
*/ */
#[Route(path: '/signOut', name: 'user_signOut')] #[Route(path: '/SignOut', name: 'user_signOut')]
public function logout (): void { public function logout (): void {
throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
} }

@ -161,10 +161,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Stringa
} }
/** /**
* Removes sensitive data from the user * @inheritDoc
*
* @see UserInterface
*/ */
public function eraseCredentials (): void { public function eraseCredentials (): void {
// Nothing to do
} }
} }

@ -0,0 +1,45 @@
<?php
namespace App\Form\Config;
use App\Entity\MaterialType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* The form for editing a material type
*/
class MaterialTypeEditForm extends AbstractType {
/**
* @inheritDoc
*/
public function configureOptions (OptionsResolver $resolver): void {
$resolver->setDefaults(
[
'data_class' => MaterialType::class,
]
);
}
/**
* @inheritDoc
*/
public function buildForm (FormBuilderInterface $builder, array $options): void {
$builder
->add('name', null, [
'label' => 'Nom',
])
->add('stackName', null, [
'label' => 'Nom de la pile',
])
->add('stackSize', null, [
'label' => 'Taille de la pile',
])
->add('submit', SubmitType::class, [
'label' => 'Enregistrer',
]);
}
}

@ -1,4 +1,13 @@
{ {
"doctrine/deprecations": {
"version": "1.1",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "1.0",
"ref": "87424683adc81d7dc305eefec1fced883084aab9"
}
},
"doctrine/doctrine-bundle": { "doctrine/doctrine-bundle": {
"version": "2.14", "version": "2.14",
"recipe": { "recipe": {
@ -195,6 +204,18 @@
"./tests/bootstrap.php" "./tests/bootstrap.php"
] ]
}, },
"symfony/property-info": {
"version": "7.3",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "7.3",
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
},
"files": [
"./config/packages/property_info.yaml"
]
},
"symfony/routing": { "symfony/routing": {
"version": "7.2", "version": "7.2",
"recipe": { "recipe": {

@ -0,0 +1,8 @@
{% extends 'base.html.twig' %}
{% block title %}Modification type de matériau - {{ parent() }}{% endblock %}
{% block mainContent %}
<h1>Modification type de matériau</h1>
{{ form(form) }}
{% endblock %}

@ -0,0 +1,62 @@
{% extends '/base.html.twig' %}
{% block title %}Liste des types de matériaux - {{ parent() }}{% endblock %}
{% block importmap %}{{ importmap(['app', 'datatables2']) }}{% endblock %}
{% block mainContent %}
<h1>Liste des types de matériaux</h1>
<div class="d-flex">
<div class="table-responsive mnw-25">
<table class="table table-sm table-striped table-hover table-bordered table-datatable2">
<thead>
<tr>
<th scope="col" data-sort-onLoad="1" class="align-middle">Nom</th>
<th scope="col" data-sort="false" class="fit-content align-middle">
<a href="{{ path('config_materialType_create') }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-title="Ajouter">
<i class="fa-solid fa-square-plus"></i>
</a>
</th>
</tr>
</thead>
<tbody>
{% for materialType in materialTypes %}
<tr>
<td>{{ materialType.name }}</td>
<td class="fit-content">
<a href="{{ path('config_materialType_edit', {id: materialType.id}) }}"
class="text-primary me-2"
data-bs-toggle="tooltip"
data-bs-title="Éditer"
><i class="fa-solid fa-pen"></i></a>
<a href="#" class="text-danger" id="btDelete" data-bs-toggle="tooltip" data-bs-title="Supprimer">
<span data-bs-toggle="modal"
data-bs-target="#deleteConfirmation"
data-modal-dynamic-link-url="{{ path('config_materialType_delete', {id: materialType.id}) }}"
><i class="fa-solid fa-xmark"></i></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="modal modal-dynamic fade" id="deleteConfirmation" tabindex="-1" aria-label="btDelete" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Suppression</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Êtes-vous sûr de vouloir supprimer ce type de matériau ?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Non</button>
<a href="#" class="modal-confirm-link btn btn-danger">Oui</a>
</div>
</div>
</div>
</div>
{% endblock %}

@ -5,7 +5,18 @@
{% block mainContent %} {% block mainContent %}
<h1>Recipe Manager</h1> <h1>Recipe Manager</h1>
{% if app.user %} {% if app.user %}
Bienvenu {{ app.user }} <nav class="navbar navbar-expand-lg py-0">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle py-0" id="dropdown-config" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Configuration
</a>
<ul class="dropdown-menu" aria-labelledby="dropdown-config">
<li><a href="{{ path('config_materialType_list') }}" class="dropdown-item">Types de matériaux</a></li>
</ul>
</li>
</ul>
</nav>
{% else %} {% else %}
<p>Bienvenu sur Recipe Manager, le gestionnaire de recette de jeux vidéos.</p> <p>Bienvenu sur Recipe Manager, le gestionnaire de recette de jeux vidéos.</p>
<p>Merci de vous <a href="{{ path('user_signIn') }}">connecter</a> ou <a href="{{ path('user_signOut') }}">créer un compte</a> pour commencer.</p> <p>Merci de vous <a href="{{ path('user_signIn') }}">connecter</a> ou <a href="{{ path('user_signOut') }}">créer un compte</a> pour commencer.</p>

Loading…
Cancel
Save