Compare commits
43 Commits
307e615fb3
...
b974b56a17
| Author | SHA1 | Date |
|---|---|---|
|
|
b974b56a17 | |
|
|
5f50584f0d | |
|
|
6aacf0cefc | |
|
|
e6068fd538 | |
|
|
a219f0f067 | |
|
|
ec3fc7f5ca | |
|
|
aee352924e | |
|
|
83da6d0be4 | |
|
|
afac1467fa | |
|
|
519556d35e | |
|
|
e4f63c9b85 | |
|
|
772b920a44 | |
|
|
c54df8a327 | |
|
|
2418e43703 | |
|
|
36fe5f5588 | |
|
|
003ee40992 | |
|
|
b430e13e3b | |
|
|
2d7adf20ec | |
|
|
5a39804dd4 | |
|
|
9a51c2d86f | |
|
|
3680621fcc | |
|
|
e1659accab | |
|
|
016c415c11 | |
|
|
2b9b030d9a | |
|
|
bb959a1ac1 | |
|
|
143277455a | |
|
|
0fc507d4c7 | |
|
|
8c7336b821 | |
|
|
9270849e12 | |
|
|
abbaf016cc | |
|
|
00c58b55d1 | |
|
|
e818a17371 | |
|
|
6e6d02e658 | |
|
|
0222274a17 | |
|
|
ead3666a4f | |
|
|
25bad81f03 | |
|
|
fd02fc26f1 | |
|
|
6dc6d3bfa9 | |
|
|
20509385f6 | |
|
|
3485bcc48f | |
|
|
1a49265658 | |
|
|
a01df6345a | |
|
|
41c6e82a13 |
|
|
@ -21,5 +21,7 @@
|
|||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="quill.snow" level="application" />
|
||||
<orderEntry type="library" name="quill" level="application" />
|
||||
</component>
|
||||
</module>
|
||||
|
|
@ -10,6 +10,11 @@
|
|||
<option name="highlightLevel" value="WARNING" />
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpCodeSniffer">
|
||||
<phpcs_settings>
|
||||
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="30000" />
|
||||
</phpcs_settings>
|
||||
</component>
|
||||
<component name="PhpIncludePathManager">
|
||||
<include_path>
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
|
||||
|
|
@ -173,9 +178,15 @@
|
|||
<path value="$PROJECT_DIR$/vendor/guzzlehttp/guzzle" />
|
||||
<path value="$PROJECT_DIR$/vendor/mtdowling/jmespath.php" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-client" />
|
||||
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||
<component name="PhpStan">
|
||||
<PhpStan_settings>
|
||||
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
|
||||
</PhpStan_settings>
|
||||
</component>
|
||||
<component name="PhpStanOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
|
|
@ -184,6 +195,11 @@
|
|||
<PhpUnitSettings configuration_file_path="$PROJECT_DIR$/phpunit.xml.dist" custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" use_configuration_file="true" />
|
||||
</phpunit_settings>
|
||||
</component>
|
||||
<component name="Psalm">
|
||||
<Psalm_settings>
|
||||
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="e61c7a46-7290-4f1b-ace7-5462f6da9ae0" timeout="60000" />
|
||||
</Psalm_settings>
|
||||
</component>
|
||||
<component name="PsalmOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
## Notes
|
||||
- Certaines abbreviations sont utilisées afin de simplifier le code et d'éviter les répétitions ou noms trop longs :
|
||||
- `uo` pour `User Organization`
|
||||
- `uoId` pour `User Organization Id`
|
||||
- `oa` pour `Organization Application`
|
||||
- `at` pour `Access Token`
|
||||
- A delete command is available to delete roles
|
||||
|
||||
|
||||
### ROLES
|
||||
```bash
|
||||
php bin/console app:delete-role ROLE_NAME
|
||||
```
|
||||
|
||||
### Tabulator
|
||||
- Certaines fonctions sont déjà disponibles (snippet) mais commentées, car on ne les utilise pas
|
||||
- Exemples de sorting et filtering sont disponibles dans 'src/controller/organization.php' L.268
|
||||
|
||||
### Frontend:
|
||||
- Le Body/content de chaque page sont dans des div avec le style suivant :
|
||||
``` html
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
```
|
||||
- L'espace entre les éléments cartes est avec l'un des styles suivants :
|
||||
``` html
|
||||
<div class="mb-3"> <!-- margin bottom -->
|
||||
<div class="mt-3"> <!-- margin top -->
|
||||
<div class="me-3"> <!-- margin end/right -->
|
||||
<div class="ms-3"> <!-- margin start/left -->
|
||||
<div class="mx-3"> <!-- margin left and right -->
|
||||
<div class="my-3"> <!-- margin top and bottom -->
|
||||
<div class="m-3"> <!-- margin -->
|
||||
<div class="d-flex gap-2"> <!-- gap entre les boutons -->
|
||||
```
|
||||
- Chaque élément est une carte afin de donner un style uniforme :
|
||||
``` html
|
||||
<div class="card p-3">
|
||||
```
|
||||
16
README.MD
16
README.MD
|
|
@ -13,22 +13,14 @@
|
|||
php bin/console doctrine:database:create
|
||||
php bin/console doctrine:schema:update --force
|
||||
```
|
||||
#### SQL
|
||||
#### Roles
|
||||
```bash
|
||||
insert into public.roles (id, name, created_at)
|
||||
values (3, 'USER', '2025-05-21 13:22:52'),
|
||||
(2, 'ADMIN', '2025-05-21 13:22:52'),
|
||||
(1, 'SUPER ADMIN', '2025-05-21 13:22:52');
|
||||
php bin/console app:create-role USER
|
||||
php bin/console app:create-role ADMIN
|
||||
php bin/console app:create-role "SUPER ADMIN"
|
||||
```
|
||||
#### Choices.js
|
||||
```bash
|
||||
php bin/console importmap:require choices.js
|
||||
php bin/console importmap:require choices.js/public/assets/styles/choices.min.css
|
||||
```
|
||||
### Notes
|
||||
- certaines abbreviations sont utilisées afin de simplifier le code et d'éviter les répétitions ou noms trop longs :
|
||||
- `uo` pour `User Organization`
|
||||
- `uoId` pour `User Organization Id`
|
||||
- `oa` pour `Organization Application`
|
||||
- `at` pour `Access Token`
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ import './styles/app.css';
|
|||
import './styles/navbar.css';
|
||||
import './styles/sidebar.css';
|
||||
import './styles/choices.css'
|
||||
import 'choices.js/public/assets/styles/choices.min.css';
|
||||
import 'tabulator-tables/dist/css/tabulator.min.css';
|
||||
import './styles/tabulator.css';
|
||||
import './styles/card.css';
|
||||
|
||||
import 'bootstrap';
|
||||
import './js/template.js';
|
||||
|
|
@ -17,4 +21,5 @@ import './js/off_canvas.js';
|
|||
import './js/hoverable-collapse.js';
|
||||
import './js/cookies.js';
|
||||
import 'choices.js';
|
||||
import 'choices.js/public/assets/styles/choices.min.css';
|
||||
import 'quill'
|
||||
import 'tabulator-tables'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import {Controller} from '@hotwired/stimulus'
|
||||
import Quill from 'quill'
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
application: String,
|
||||
organization: String,
|
||||
}
|
||||
static targets = ['hidden', 'submitBtn']
|
||||
|
||||
connect() {
|
||||
// Map each editor to its toolbar and hidden field
|
||||
if (document.querySelector('#editor-description')) {
|
||||
this.editors = [
|
||||
{
|
||||
editorSelector: '#editor-description',
|
||||
toolbarSelector: '#toolbar-description',
|
||||
hiddenTarget: this.hiddenTargets[0],
|
||||
},
|
||||
{
|
||||
editorSelector: '#editor-descriptionSmall',
|
||||
toolbarSelector: '#toolbar-descriptionSmall',
|
||||
hiddenTarget: this.hiddenTargets[1],
|
||||
},
|
||||
]
|
||||
|
||||
this.editors.forEach(({editorSelector, toolbarSelector, hiddenTarget}) => {
|
||||
const quill = new Quill(editorSelector, {
|
||||
modules: {
|
||||
toolbar: toolbarSelector,
|
||||
},
|
||||
theme: 'snow',
|
||||
placeholder: 'Écrivez votre texte...',
|
||||
})
|
||||
|
||||
quill.on('text-change', () => {
|
||||
hiddenTarget.value = quill.root.innerHTML
|
||||
})
|
||||
|
||||
hiddenTarget.value = quill.root.innerHTML
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleAuthorizeSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const originalText = this.submitBtnTarget.textContent;
|
||||
|
||||
if (!confirm(`Vous vous apprêtez à donner l'accès à ${this.organizationValue} pour ${this.applicationValue}. Êtes‑vous sûr(e) ?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitBtnTarget.textContent = 'En cours...';
|
||||
this.submitBtnTarget.disabled = true;
|
||||
|
||||
fetch(event.target.action, {
|
||||
method: 'POST',
|
||||
body: new FormData(event.target)
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.submitBtnTarget.textContent = 'Autorisé ✓';
|
||||
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-success');
|
||||
} else {
|
||||
this.submitBtnTarget.textContent = originalText;
|
||||
this.submitBtnTarget.disabled = false;
|
||||
alert('Erreur lors de l\'action');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.submitBtnTarget.textContent = originalText;
|
||||
this.submitBtnTarget.disabled = false;
|
||||
alert('Erreur lors de l\'action');
|
||||
});
|
||||
}
|
||||
|
||||
handleRemoveSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const originalText = this.submitBtnTarget.textContent;
|
||||
|
||||
if (!confirm(`Vous vous apprêtez à retirer l'accès à ${this.applicationValue} pour ${this.organizationValue}. Êtes‑vous sûr(e) ?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitBtnTarget.textContent = 'En cours...';
|
||||
this.submitBtnTarget.disabled = true;
|
||||
|
||||
fetch(event.target.action, {
|
||||
method: 'POST',
|
||||
body: new FormData(event.target)
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
this.submitBtnTarget.textContent = 'Retiré ✓';
|
||||
this.submitBtnTarget.classList.replace('btn-secondary', 'btn-danger');
|
||||
} else {
|
||||
this.submitBtnTarget.textContent = originalText;
|
||||
this.submitBtnTarget.disabled = false;
|
||||
alert('Erreur lors de l\'action');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.submitBtnTarget.textContent = originalText;
|
||||
this.submitBtnTarget.disabled = false;
|
||||
alert('Erreur lors de l\'action');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import {Controller} from '@hotwired/stimulus'
|
||||
// Important: include a build with Ajax + pagination (TabulatorFull is simplest)
|
||||
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {aws: String};
|
||||
|
||||
connect() {
|
||||
this.table();
|
||||
}
|
||||
|
||||
table(){
|
||||
const table = new Tabulator("#tabulator-org", {
|
||||
// Register locales here
|
||||
langs: {
|
||||
fr: {
|
||||
ajax: {
|
||||
loading: "Chargement...",
|
||||
error: "Erreur",
|
||||
},
|
||||
pagination: {
|
||||
page_size: "Taille de page",
|
||||
page_title: "Afficher la page",
|
||||
first: "Premier",
|
||||
first_title: "Première page",
|
||||
last: "Dernier",
|
||||
last_title: "Dernière page",
|
||||
prev: "Précédent",
|
||||
prev_title: "Page précédente",
|
||||
next: "Suivant",
|
||||
next_title: "Page suivante",
|
||||
all: "Tout",
|
||||
counter: {
|
||||
showing: "Affiche",
|
||||
of: "de",
|
||||
rows: "lignes",
|
||||
pages: "pages",
|
||||
},
|
||||
},
|
||||
headerFilters: {
|
||||
default: "Filtrer la colonne...",
|
||||
columns: {},
|
||||
},
|
||||
data: {
|
||||
loading: "Chargement des données...",
|
||||
error: "Erreur de chargement des données",
|
||||
},
|
||||
groups: { item: "élément", items: "éléments" },
|
||||
},
|
||||
},
|
||||
|
||||
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
||||
|
||||
ajaxURL: "/organization/data",
|
||||
ajaxConfig: "GET",
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: 10,
|
||||
//paginationSizeSelector: [5, 10, 20, 50], // Désactivé pour l'instant car jpp faire de jolie style
|
||||
|
||||
ajaxResponse: (url, params, response) => response,
|
||||
paginationDataSent: { page: "page", size: "size" },
|
||||
paginationDataReceived: { last_page: "last_page" },
|
||||
|
||||
ajaxSorting: true,
|
||||
ajaxFiltering: true,
|
||||
rowHeight: 60,
|
||||
layout: "fitColumns", // activate French
|
||||
columns: [
|
||||
{
|
||||
title: "Logo",
|
||||
field: "logoUrl",
|
||||
formatter: "image",
|
||||
formatterParams: {
|
||||
height: "50px",
|
||||
width: "50px",
|
||||
urlPrefix: this.awsValue,
|
||||
urlSuffix: "",
|
||||
},
|
||||
width: 100,
|
||||
},
|
||||
// TODO: regarder quel style est mieux entre les "hozAlign"
|
||||
// TODO: regarder quel style est mieux avec/sans headerFilter
|
||||
{title: "Nom", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle", headerHozAlign: "left"},
|
||||
{title: "Email", field: "email", headerFilter: "input", widthGrow: 2, vertAlign: "middle", hozAlign: "center"},
|
||||
{
|
||||
title: "Actions",
|
||||
field: "showUrl",
|
||||
hozAlign: "center",
|
||||
width: 100,
|
||||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const url = cell.getValue();
|
||||
if (url) {
|
||||
return `
|
||||
<a href="${url}" class="p-3 align-middle color-primary" title="Voir">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="35px"
|
||||
height="35px"
|
||||
viewBox="0 0 576 512">
|
||||
<path fill="currentColor"
|
||||
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,62 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import {Controller} from '@hotwired/stimulus';
|
||||
import Choices from 'choices.js';
|
||||
import {TabulatorFull as Tabulator} from 'tabulator-tables';
|
||||
|
||||
const TABULATOR_FR_LANG = {
|
||||
fr: {
|
||||
ajax: {loading: "Chargement...", error: "Erreur"},
|
||||
pagination: {
|
||||
page_size: "Taille de page",
|
||||
page_title: "Afficher la page",
|
||||
first: "Premier",
|
||||
first_title: "Première page",
|
||||
last: "Dernier",
|
||||
last_title: "Dernière page",
|
||||
prev: "Précédent",
|
||||
prev_title: "Page précédente",
|
||||
next: "Suivant",
|
||||
next_title: "Page suivante",
|
||||
all: "Tout",
|
||||
counter: {showing: "Affiche", of: "de", rows: "lignes", pages: "pages"},
|
||||
},
|
||||
headerFilters: {default: "Filtrer la colonne...", columns: {}},
|
||||
data: {loading: "Chargement des données...", error: "Erreur de chargement des données"},
|
||||
groups: {item: "élément", items: "éléments"},
|
||||
},
|
||||
};
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
rolesArray: Array,
|
||||
selectedRoleIds: Array,
|
||||
id: Number,
|
||||
aws: String,
|
||||
list: Boolean,
|
||||
listOrganization: Boolean,
|
||||
new: Boolean,
|
||||
admin: Boolean,
|
||||
listSmall: Boolean,
|
||||
statut: Boolean,
|
||||
orgId: Number
|
||||
}
|
||||
|
||||
static targets = ["select"];
|
||||
|
||||
connect() {
|
||||
this.roleSelect();
|
||||
if (this.listValue) {
|
||||
this.table();
|
||||
}
|
||||
if (this.newValue) {
|
||||
this.tableSmall();
|
||||
}
|
||||
if (this.adminValue) {
|
||||
this.tableSmallAdmin();
|
||||
}
|
||||
if (this.listOrganizationValue) {
|
||||
this.tableOrganization()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
roleSelect() {
|
||||
|
|
@ -29,4 +75,761 @@ export default class extends Controller {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: vérifier le style des header filter et vertAlign/hozalign
|
||||
table() {
|
||||
const columns = [
|
||||
{
|
||||
title: "",
|
||||
field: "isConnected",
|
||||
width: 40, // small column
|
||||
hozAlign: "center",
|
||||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
tooltip: false,
|
||||
formatter: (cell) => {
|
||||
const online = !!cell.getValue();
|
||||
const color = online ? "#80F20E" : "#E42E31"; // green/red
|
||||
return `<span class="status-dot" style="
|
||||
display:inline-block;
|
||||
width:10px;height:10px;
|
||||
border-radius:50%;
|
||||
background:${color};
|
||||
"></span>`;
|
||||
},
|
||||
// Optional: for accessibility
|
||||
formatterPrint: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||
formatterClipboard: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||
},
|
||||
{
|
||||
title: "Profil",
|
||||
field: "pictureUrl",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const data = cell.getRow().getData();
|
||||
const url = cell.getValue();
|
||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||
|
||||
// wrapper is for centering and circle clipping
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "avatar-wrapper";
|
||||
// same size for both cases
|
||||
wrapper.style.width = "40px";
|
||||
wrapper.style.height = "40px";
|
||||
wrapper.style.display = "flex";
|
||||
wrapper.style.alignItems = "center";
|
||||
wrapper.style.justifyContent = "center";
|
||||
wrapper.style.borderRadius = "50%";
|
||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||
|
||||
if (!url) {
|
||||
wrapper.style.background = "#6c757d"; // gray background
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "14px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Image case: make it fill the same wrapper
|
||||
const img = document.createElement("img");
|
||||
img.src = `${this.awsValue || ""}${url}`;
|
||||
img.alt = initials || "avatar";
|
||||
img.style.width = "100%";
|
||||
img.style.height = "100%";
|
||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||
wrapper.appendChild(img);
|
||||
|
||||
// Optional: fallback if image fails
|
||||
img.addEventListener("error", () => {
|
||||
wrapper.innerHTML = "";
|
||||
wrapper.style.background = "#6c757d";
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "12px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
},
|
||||
{title: "<b>Nom</b>", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||
{title: "<b>Prénom</b>", field: "prenom", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||
{title: "<b>Email</b>", field: "email", headerFilter: "input", widthGrow: 3, vertAlign: "middle"},
|
||||
{
|
||||
title: "<b>Statut</b>", field: "statut", vertAlign: "middle",
|
||||
formatter: (cell) => {
|
||||
const statut = cell.getValue();
|
||||
if (statut) {
|
||||
return `<span class="badge bg-success">Actif</span>`
|
||||
} else {
|
||||
return `<span class="badge bg-secondary">Inactif</span>`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "<b>Actions</b>",
|
||||
field: "showUrl",
|
||||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const url = cell.getValue();
|
||||
if (!url) return '';
|
||||
|
||||
const rowData = cell.getRow().getData();
|
||||
const userId = rowData.id;
|
||||
const statut = rowData.statut;
|
||||
|
||||
// Decide which action (deactivate vs activate)
|
||||
const isActive = Boolean(statut);
|
||||
|
||||
const actionClass = isActive ? 'deactivate-user' : 'activate-user';
|
||||
const actionTitle = isActive ? 'Désactiver' : 'Réactiver';
|
||||
const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
|
||||
|
||||
// SVGs
|
||||
const deactivateSvg = `
|
||||
<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"/>
|
||||
</svg>`;
|
||||
|
||||
const activateSvg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
||||
<path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>`;
|
||||
|
||||
const actionSvg = isActive ? deactivateSvg : activateSvg;
|
||||
|
||||
return `
|
||||
<div class="d-flex gap-2 align-content-center">
|
||||
<a href="${url}" class="color-primary" title="Voir">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
||||
<path fill="currentColor"
|
||||
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
class="${actionColorClass} ${actionClass}"
|
||||
data-id="${userId}"
|
||||
title="${actionTitle}">
|
||||
${actionSvg}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
cellClick: function (e, cell) {
|
||||
const target = e.target.closest('a');
|
||||
if (!target) return;
|
||||
|
||||
// Deactivate
|
||||
if (target.classList.contains('deactivate-user')) {
|
||||
e.preventDefault();
|
||||
const userId = target.getAttribute('data-id');
|
||||
if (confirm('Voulez-vous vraiment désactiver cet utilisateur ?')) {
|
||||
|
||||
fetch(`/user/deactivate/${userId}`, {
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
// Option 1: update row status and re-render to switch icon
|
||||
const data = cell.getRow().getData();
|
||||
data.statut = false;
|
||||
cell.getRow().reformat();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
alert('Erreur lors de la désactivation: ' + text);
|
||||
}
|
||||
})
|
||||
.catch(() => alert('Erreur lors de la désactivation'));
|
||||
}
|
||||
}
|
||||
|
||||
// Activate
|
||||
if (target.classList.contains('activate-user')) {
|
||||
e.preventDefault();
|
||||
const userId = target.getAttribute('data-id');
|
||||
if (confirm('Voulez-vous réactiver cet utilisateur ?')) {
|
||||
|
||||
fetch(`/user/activate/${userId}`, {
|
||||
method: 'POST',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
// Switch status back to active and re-render row
|
||||
const data = cell.getRow().getData();
|
||||
data.statut = true;
|
||||
cell.getRow().reformat();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
alert('Erreur lors de la réactivation: ' + text);
|
||||
}
|
||||
})
|
||||
.catch(() => alert('Erreur lors de la réactivation'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
const tabulator = new Tabulator("#tabulator-userList", {
|
||||
langs: TABULATOR_FR_LANG,
|
||||
locale: "fr",
|
||||
ajaxURL: "/user/data",
|
||||
ajaxConfig: "GET",
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: 10,
|
||||
|
||||
ajaxResponse: (url, params, response) => response,
|
||||
paginationDataSent: {page: "page", size: "size"},
|
||||
paginationDataReceived: {last_page: "last_page"},
|
||||
|
||||
ajaxSorting: true,
|
||||
ajaxFiltering: true,
|
||||
filterMode: "remote",
|
||||
|
||||
// Add this to send filter data
|
||||
ajaxURLGenerator: function(url, config, params) {
|
||||
let queryParams = new URLSearchParams();
|
||||
queryParams.append('page', params.page || 1);
|
||||
queryParams.append('size', params.size || 10);
|
||||
|
||||
// Add filters
|
||||
if (params.filter) {
|
||||
params.filter.forEach(filter => {
|
||||
queryParams.append(`filter[${filter.field}]`, filter.value);
|
||||
});
|
||||
}
|
||||
|
||||
return `${url}?${queryParams.toString()}`;
|
||||
},
|
||||
|
||||
rowHeight: 60,
|
||||
layout: "fitColumns",
|
||||
columns
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
onSelectChange(row, newValue) {
|
||||
const data = row.getData();
|
||||
console.log("Change select" + data);
|
||||
|
||||
};
|
||||
|
||||
|
||||
tableSmall() {
|
||||
const columns = [
|
||||
{
|
||||
title: "Profil",
|
||||
field: "pictureUrl",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const data = cell.getRow().getData();
|
||||
const url = cell.getValue();
|
||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||
const initials = `${data.initials}`;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "avatar-wrapper";
|
||||
// same size for both cases
|
||||
wrapper.style.width = "40px";
|
||||
wrapper.style.height = "40px";
|
||||
wrapper.style.display = "flex";
|
||||
wrapper.style.alignItems = "center";
|
||||
wrapper.style.justifyContent = "center";
|
||||
wrapper.style.borderRadius = "50%";
|
||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||
|
||||
if (!url) {
|
||||
wrapper.style.background = "#6c757d"; // gray background
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "14px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Image case: make it fill the same wrapper
|
||||
const img = document.createElement("img");
|
||||
img.src = `${this.awsValue || ""}${url}`;
|
||||
img.alt = initials || "avatar";
|
||||
img.style.width = "100%";
|
||||
img.style.height = "100%";
|
||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||
wrapper.appendChild(img);
|
||||
|
||||
// Optional: fallback if image fails
|
||||
img.addEventListener("error", () => {
|
||||
wrapper.innerHTML = "";
|
||||
wrapper.style.background = "#6c757d";
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "12px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
},
|
||||
{title: "<b>Email</b>", field: "email", widthGrow: 3, vertAlign: "middle"},
|
||||
{
|
||||
title: "<b>Actions</b>",
|
||||
field: "showUrl",
|
||||
hozAlign: "center",
|
||||
width: 100,
|
||||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const url = cell.getValue();
|
||||
if (url) {
|
||||
return `
|
||||
<a href="${url}" class="p-3 align-middle color-primary" title="Voir">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="35px"
|
||||
height="35px"
|
||||
viewBox="0 0 576 512">
|
||||
<path fill="currentColor"
|
||||
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const tabulator = new Tabulator("#tabulator-userListSmall", {
|
||||
|
||||
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
||||
ajaxURL: "/user/data/new",
|
||||
|
||||
ajaxConfig: "GET",
|
||||
pagination: false,
|
||||
paginationMode: "remote",
|
||||
// paginationSize: 5,
|
||||
ajaxParams: {orgId: this.orgIdValue},
|
||||
langs: TABULATOR_FR_LANG,
|
||||
ajaxResponse: (url, params, response) => response.data,
|
||||
// paginationDataSent: {page: "page", size: "size"},
|
||||
// paginationDataReceived: {last_page: "last_page"},
|
||||
|
||||
// ajaxSorting: true,
|
||||
// ajaxFiltering: true,
|
||||
rowHeight: 60,
|
||||
layout: "fitColumns", // activate French
|
||||
|
||||
columns
|
||||
});
|
||||
}
|
||||
|
||||
tableSmallAdmin() {
|
||||
const columns = [
|
||||
{
|
||||
title: "Profil",
|
||||
field: "pictureUrl",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const data = cell.getRow().getData();
|
||||
const url = cell.getValue();
|
||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||
const initials = `${data.initials}`;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "avatar-wrapper";
|
||||
// same size for both cases
|
||||
wrapper.style.width = "40px";
|
||||
wrapper.style.height = "40px";
|
||||
wrapper.style.display = "flex";
|
||||
wrapper.style.alignItems = "center";
|
||||
wrapper.style.justifyContent = "center";
|
||||
wrapper.style.borderRadius = "50%";
|
||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||
|
||||
if (!url) {
|
||||
wrapper.style.background = "#6c757d"; // gray background
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "14px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Image case: make it fill the same wrapper
|
||||
const img = document.createElement("img");
|
||||
img.src = `${this.awsValue || ""}${url}`;
|
||||
img.alt = initials || "avatar";
|
||||
img.style.width = "100%";
|
||||
img.style.height = "100%";
|
||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||
wrapper.appendChild(img);
|
||||
|
||||
// Optional: fallback if image fails
|
||||
img.addEventListener("error", () => {
|
||||
wrapper.innerHTML = "";
|
||||
wrapper.style.background = "#6c757d";
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "12px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
},
|
||||
{title: "<b>Email</b>", field: "email", widthGrow: 3, vertAlign: "middle"},
|
||||
{
|
||||
title: "<b>Actions</b>",
|
||||
field: "showUrl",
|
||||
hozAlign: "center",
|
||||
width: 100,
|
||||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const url = cell.getValue();
|
||||
if (url) {
|
||||
return `
|
||||
<a href="${url}" class="p-3 align-middle color-primary" title="Voir">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="35px"
|
||||
height="35px"
|
||||
viewBox="0 0 576 512">
|
||||
<path fill="currentColor"
|
||||
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
const tabulator = new Tabulator("#tabulator-userListSmallAdmin", {
|
||||
|
||||
locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it)
|
||||
ajaxURL: "/user/data/admin",
|
||||
|
||||
ajaxConfig: "GET",
|
||||
pagination: false,
|
||||
paginationMode: "remote",
|
||||
// paginationSize: 5,
|
||||
ajaxParams: {orgId: this.orgIdValue},
|
||||
langs: TABULATOR_FR_LANG,
|
||||
ajaxResponse: (url, params, response) => response.data,
|
||||
// paginationDataSent: {page: "page", size: "size"},
|
||||
// paginationDataReceived: {last_page: "last_page"},
|
||||
|
||||
// ajaxSorting: true,
|
||||
// ajaxFiltering: true,
|
||||
rowHeight: 60,
|
||||
layout: "fitColumns", // activate French
|
||||
|
||||
columns
|
||||
});
|
||||
}
|
||||
|
||||
tableOrganization() {
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "",
|
||||
field: "isConnected",
|
||||
width: 40, // small column
|
||||
hozAlign: "center",
|
||||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
tooltip: false,
|
||||
formatter: (cell) => {
|
||||
const online = !!cell.getValue();
|
||||
const color = online ? "#80F20E" : "#E42E31"; // green/red
|
||||
return `<span class="status-dot" style="
|
||||
display:inline-block;
|
||||
width:10px;height:10px;
|
||||
border-radius:50%;
|
||||
background:${color};
|
||||
"></span>`;
|
||||
},
|
||||
// Optional: for accessibility
|
||||
formatterPrint: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||
formatterClipboard: (cell) => (cell.getValue() ? "online" : "offline"),
|
||||
},
|
||||
{
|
||||
title: "Profil",
|
||||
field: "pictureUrl",
|
||||
width: 80,
|
||||
hozAlign: "center",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const data = cell.getRow().getData();
|
||||
const url = cell.getValue();
|
||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "avatar-wrapper";
|
||||
// same size for both cases
|
||||
wrapper.style.width = "40px";
|
||||
wrapper.style.height = "40px";
|
||||
wrapper.style.display = "flex";
|
||||
wrapper.style.alignItems = "center";
|
||||
wrapper.style.justifyContent = "center";
|
||||
wrapper.style.borderRadius = "50%";
|
||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
||||
|
||||
if (!url) {
|
||||
wrapper.style.background = "#6c757d"; // gray background
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "14px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
// Image case: make it fill the same wrapper
|
||||
const img = document.createElement("img");
|
||||
img.src = `${this.awsValue || ""}${url}`;
|
||||
img.alt = initials || "avatar";
|
||||
img.style.width = "100%";
|
||||
img.style.height = "100%";
|
||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
||||
wrapper.appendChild(img);
|
||||
|
||||
// Optional: fallback if image fails
|
||||
img.addEventListener("error", () => {
|
||||
wrapper.innerHTML = "";
|
||||
wrapper.style.background = "#6c757d";
|
||||
const span = document.createElement("span");
|
||||
span.className = "avatar-initials";
|
||||
span.style.color = "#fff";
|
||||
span.style.fontWeight = "600";
|
||||
span.style.fontSize = "12px";
|
||||
span.textContent = initials || "•";
|
||||
wrapper.appendChild(span);
|
||||
});
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
},
|
||||
{title: "<b>Nom</b>", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||
{title: "<b>Prénom</b>", field: "prenom", headerFilter: "input", widthGrow: 2, vertAlign: "middle"},
|
||||
{title: "<b>Email</b>", field: "email", headerFilter: "input", widthGrow: 3, vertAlign: "middle"},
|
||||
{
|
||||
title: "<b>Statut</b>", field: "statut", vertAlign: "middle",
|
||||
formatter: (cell) => {
|
||||
const statut = cell.getValue();
|
||||
if (statut) {
|
||||
return `<span class="badge bg-success">Actif</span>`
|
||||
} else {
|
||||
return `<span class="badge bg-secondary">Inactif</span>`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "<b>Actions</b>",
|
||||
field: "showUrl",
|
||||
vertAlign: "middle",
|
||||
headerSort: false,
|
||||
formatter: (cell) => {
|
||||
const url = cell.getValue();
|
||||
if (!url) return '';
|
||||
|
||||
const rowData = cell.getRow().getData();
|
||||
const userId = rowData.id;
|
||||
const statut = rowData.statut;
|
||||
const orgId = this.orgIdValue;
|
||||
|
||||
// Decide which action (deactivate vs activate)
|
||||
const isActive = Boolean(statut);
|
||||
|
||||
const actionClass = isActive ? 'deactivate-user' : 'activate-user';
|
||||
const actionTitle = isActive ? 'Désactiver' : 'Réactiver';
|
||||
const actionColorClass = isActive ? 'color-secondary' : 'color-primary';
|
||||
|
||||
// SVGs
|
||||
const deactivateSvg = `
|
||||
<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"/>
|
||||
</svg>`;
|
||||
|
||||
const activateSvg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
||||
<path fill="currentColor" d="M96 128a128 128 0 1 1 256 0a128 128 0 1 1-256 0M0 482.3C0 383.8 79.8 304 178.3 304h91.4c98.5 0 178.3 79.8 178.3 178.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3M625 177L497 305c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L591 143c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>`;
|
||||
|
||||
const actionSvg = isActive ? deactivateSvg : activateSvg;
|
||||
|
||||
return `
|
||||
<div class="d-flex gap-2 align-content-center">
|
||||
<a href="${url}" class="color-primary" title="Voir">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 640 512">
|
||||
<path fill="currentColor"
|
||||
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256C63 286 89.6 328.5 128 364.3c41.2 38.1 94.8 67.7 160 67.7s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80M95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6M288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80h-2c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2v2c0 44.2 35.8 80 80 80m0-208a128 128 0 1 1 0 256a128 128 0 1 1 0-256"/></svg>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
class="${actionColorClass} ${actionClass}"
|
||||
data-id="${userId}"
|
||||
data-org-id="${orgId}"
|
||||
title="${actionTitle}">
|
||||
${actionSvg}
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
cellClick: function (e, cell) {
|
||||
const target = e.target.closest('a');
|
||||
if (!target) return;
|
||||
|
||||
// Deactivate
|
||||
if (target.classList.contains('deactivate-user')) {
|
||||
e.preventDefault();
|
||||
const userId = target.getAttribute('data-id');
|
||||
if (confirm('Voulez-vous vraiment désactiver cet utilisateur ?')) {
|
||||
const formData = new FormData();
|
||||
formData.append('organizationId', target.getAttribute('data-org-id'));
|
||||
|
||||
fetch(`/user/organization/deactivate/${userId}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
// Option 1: update row status and re-render to switch icon
|
||||
const data = cell.getRow().getData();
|
||||
data.statut = false;
|
||||
cell.getRow().reformat();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
alert('Erreur lors de la désactivation: ' + text);
|
||||
}
|
||||
})
|
||||
.catch(() => alert('Erreur lors de la désactivation'));
|
||||
}
|
||||
}
|
||||
|
||||
// Activate
|
||||
if (target.classList.contains('activate-user')) {
|
||||
e.preventDefault();
|
||||
const userId = target.getAttribute('data-id');
|
||||
if (confirm('Voulez-vous réactiver cet utilisateur ?')) {
|
||||
const formData = new FormData();
|
||||
formData.append('organizationId', target.getAttribute('data-org-id'));
|
||||
|
||||
fetch(`/user/organization/activate/${userId}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
// Switch status back to active and re-render row
|
||||
const data = cell.getRow().getData();
|
||||
data.statut = true;
|
||||
cell.getRow().reformat();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
alert('Erreur lors de la réactivation: ' + text);
|
||||
}
|
||||
})
|
||||
.catch(() => alert('Erreur lors de la réactivation'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
// if (this.statutValue) {
|
||||
// columns.push(
|
||||
// {
|
||||
// title: "Statut", field: "role", // or any field you want
|
||||
// headerSort: false,
|
||||
// hozAlign: "center",
|
||||
// vertAlign: "middle",
|
||||
// formatter: (cell) => {
|
||||
// const row = cell.getRow();
|
||||
// const current = cell.getValue() ?? "";
|
||||
//
|
||||
// const select = document.createElement("select");
|
||||
// select.className = "table-select-action";
|
||||
// // Options
|
||||
// [
|
||||
// {value: "", label: "Choisir..."},
|
||||
// {value: "viewer", label: "Viewer"},
|
||||
// {value: "editor", label: "Editor"},
|
||||
// {value: "admin", label: "Admin"},
|
||||
// ].forEach(opt => {
|
||||
// const o = document.createElement("option");
|
||||
// o.value = opt.value;
|
||||
// o.textContent = opt.label;
|
||||
// if (opt.value === current) o.selected = true;
|
||||
// select.appendChild(o);
|
||||
// });
|
||||
//
|
||||
// // Hook change
|
||||
// select.addEventListener("change", (e) => {
|
||||
// this.onSelectChange(row, e.target.value);
|
||||
// });
|
||||
//
|
||||
// // Return a DOM node from a formatter → Tabulator will mount it
|
||||
// return select;
|
||||
// },
|
||||
// // Optional: provide text for clipboard/print
|
||||
// formatterClipboard: cell => cell.getValue(),
|
||||
// formatterPrint: cell => cell.getValue(),
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
const tabulator = new Tabulator("#tabulator-userListOrganization", {
|
||||
langs: TABULATOR_FR_LANG,
|
||||
locale: "fr",
|
||||
ajaxURL: "/user/data/organization",
|
||||
ajaxConfig: "GET",
|
||||
ajaxParams: {orgId: this.orgIdValue},
|
||||
|
||||
pagination: true,
|
||||
paginationMode: "remote",
|
||||
paginationSize: 10,
|
||||
|
||||
ajaxResponse: (url, params, response) => response,
|
||||
paginationDataSent: {page: "page", size: "size"},
|
||||
paginationDataReceived: {last_page: "last_page"},
|
||||
|
||||
ajaxSorting: true,
|
||||
ajaxFiltering: true,
|
||||
rowHeight: 60,
|
||||
layout: "fitColumns", // activate French
|
||||
|
||||
columns
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -132,4 +132,8 @@ body {
|
|||
|
||||
.btn-warning{
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.color-secondary{
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.card.no-header-bg .card-header{
|
||||
background-color: transparent !important;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
|
||||
/* Remove outer table border */
|
||||
.tabulator {
|
||||
border: none !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
/* Remove header and row cell borders */
|
||||
.tabulator-header,
|
||||
.tabulator-header .tabulator-col,
|
||||
.tabulator-tableholder,
|
||||
.tabulator-table,
|
||||
.tabulator-row,
|
||||
.tabulator-row .tabulator-cell {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Remove column header bottom border and row separators */
|
||||
.tabulator-header {
|
||||
border-bottom: none !important;
|
||||
background-color: transparent !important;
|
||||
/*border-top-left-radius: 25%;*/
|
||||
/*border-top-right-radius: 25%;*/
|
||||
}
|
||||
.tabulator-row {
|
||||
border-bottom: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Remove look on hover/selected without borders */
|
||||
.tabulator-row:hover {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.tabulator-row.tabulator-selected {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.tabulator-row.tabulator-row-odd {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Rounded border for images in cells */
|
||||
.tabulator-cell img {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
/* Scope to this table only */
|
||||
.tabulator,
|
||||
.tabulator-header,
|
||||
.tabulator-header .tabulator-header-contents,
|
||||
.tabulator-header .tabulator-col{
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.tabulator-footer {border-top: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.tabulator-footer .tabulator-page.active{
|
||||
background-color: var(--primary-blue-light) !important;
|
||||
border: 1px solid var(--primary-blue-light) !important;
|
||||
color: #FFFFFF/* text color */ !important
|
||||
}
|
||||
.tabulator-footer .tabulator-page {
|
||||
background-color: transparent !important;
|
||||
border: 1px solid var(--primary-blue-light) !important;
|
||||
color: var(--black-font)/* text color */ !important;
|
||||
}
|
||||
.tabulator-footer .tabulator-page:hover,
|
||||
.tabulator-footer .tabulator-page.active:hover{
|
||||
background-color: var(--primary-blue-dark) !important;
|
||||
border: 1px solid var(--primary-blue-dark) !important;
|
||||
color: #FFFFFF/* text color */ !important
|
||||
}
|
||||
|
||||
.tabulator-footer select{
|
||||
border: 1px solid var(--primary-blue-light) !important;
|
||||
background-color: transparent !important;
|
||||
color: var(--black-font)/* text color */ !important;
|
||||
}
|
||||
|
||||
.tabulator-header input{
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
height: 40px;
|
||||
background-color: lightgray !important;
|
||||
padding-left: 15px !important;
|
||||
}
|
||||
.tabulator-header input::placeholder{
|
||||
color: var(--black-font) !important;
|
||||
font-size: 14px !important;
|
||||
opacity: 1 !important; /* Firefox */
|
||||
}
|
||||
|
||||
.tabulator-header input:focus {
|
||||
border:0;
|
||||
}
|
||||
.tabulator .tabulator-header .tabulator-col .tabulator-col-title {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
/* Select hover Désactivé pour l'instant car jpp faire de jolie style */
|
||||
/*#tabulator-org .tabulator-footer select:hover {*/
|
||||
/* border: 1px solid var(--primary-blue-dark) !important;*/
|
||||
/* background-color: var(--primary-blue-dark) !important;*/
|
||||
/* color: #fff !important;*/
|
||||
/*}*/
|
||||
|
||||
/*.tabulator-footer select:focus {*/
|
||||
/* border: 1px solid var(--primary-blue-dark) !important;*/
|
||||
/* outline: none !important;*/
|
||||
/* background-color: var(--primary-blue-dark) !important;*/
|
||||
/* color: #fff !important;*/
|
||||
/*}*/
|
||||
|
|
@ -36,6 +36,7 @@ security:
|
|||
stateless: true
|
||||
oauth2: true
|
||||
main:
|
||||
user_checker: App\Security\UserChecker
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ twig:
|
|||
|
||||
globals:
|
||||
application: '%env(APPLICATION)%'
|
||||
aws_url: '%env(AWS_S3_PORTAL_URL)%'
|
||||
version: '0.4'
|
||||
|
||||
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
|
|
|
|||
|
|
@ -42,4 +42,35 @@ return [
|
|||
'version' => '11.1.0',
|
||||
'type' => 'css',
|
||||
],
|
||||
'quill' => [
|
||||
'version' => '2.0.3',
|
||||
],
|
||||
'lodash-es' => [
|
||||
'version' => '4.17.21',
|
||||
],
|
||||
'parchment' => [
|
||||
'version' => '3.0.0',
|
||||
],
|
||||
'quill-delta' => [
|
||||
'version' => '5.1.0',
|
||||
],
|
||||
'eventemitter3' => [
|
||||
'version' => '5.0.1',
|
||||
],
|
||||
'fast-diff' => [
|
||||
'version' => '1.3.0',
|
||||
],
|
||||
'lodash.clonedeep' => [
|
||||
'version' => '4.5.0',
|
||||
],
|
||||
'lodash.isequal' => [
|
||||
'version' => '4.5.0',
|
||||
],
|
||||
'tabulator-tables' => [
|
||||
'version' => '6.3.1',
|
||||
],
|
||||
'tabulator-tables/dist/css/tabulator.min.css' => [
|
||||
'version' => '6.3.1',
|
||||
'type' => 'css',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251008081943 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 apps ALTER description TYPE TEXT');
|
||||
$this->addSql('ALTER TABLE apps ALTER description DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE apps ALTER description_small TYPE TEXT');
|
||||
}
|
||||
|
||||
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 apps ALTER description TYPE VARCHAR(255)');
|
||||
$this->addSql('ALTER TABLE apps ALTER description SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE apps ALTER description_small TYPE VARCHAR(255)');
|
||||
}
|
||||
}
|
||||
|
|
@ -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 Version20251013133256 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 "user" ALTER picture_url DROP NOT 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 "user" ALTER picture_url SET NOT NULL');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Roles; // ⚡ your Roles entity
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-role',
|
||||
description: 'Creates a new role in the database'
|
||||
)]
|
||||
class CreateRoleCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('name', InputArgument::REQUIRED, 'The name of the role'); // role name required
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$roleName = trim($input->getArgument('name'));
|
||||
$roleName = strtoupper($roleName); // Normalize to uppercase
|
||||
|
||||
// Ensure not empty
|
||||
if ($roleName === '') {
|
||||
$output->writeln('<error>The role name cannot be empty</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Check if role already exists
|
||||
$existing = $this->entityManager->getRepository(Roles::class)
|
||||
->findOneBy(['name' => $roleName]);
|
||||
|
||||
if ($existing) {
|
||||
$output->writeln("<comment>Role '{$roleName}' already exists.</comment>");
|
||||
return Command::SUCCESS; // not failure, just redundant
|
||||
}
|
||||
|
||||
// Create and persist new role
|
||||
$role = new Roles();
|
||||
$role->setName($roleName);
|
||||
|
||||
$this->entityManager->persist($role);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$output->writeln("<info>Role '{$roleName}' created successfully!</info>");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Roles;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:delete-role',
|
||||
description: 'Deletes a role from the database'
|
||||
)]
|
||||
class DeleteRoleCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('name', InputArgument::REQUIRED, 'The name of the role to delete');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$roleName = trim($input->getArgument('name'));
|
||||
$roleName = strtoupper($roleName); // Normalize to uppercase
|
||||
|
||||
if ($roleName === '') {
|
||||
$output->writeln('<error>The role name cannot be empty</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Find the role
|
||||
$role = $this->entityManager->getRepository(Roles::class)
|
||||
->findOneBy(['name' => $roleName]);
|
||||
|
||||
if (!$role) {
|
||||
$output->writeln("<error>Role '{$roleName}' not found.</error>");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Check if role is being used (optional safety check)
|
||||
$usageCount = $this->entityManager->getRepository(\App\Entity\UserOrganizatonApp::class)
|
||||
->count(['role' => $role]);
|
||||
|
||||
if ($usageCount > 0) {
|
||||
$output->writeln("<error>Cannot delete role '{$roleName}' - it is assigned to {$usageCount} user(s).</error>");
|
||||
$output->writeln('<comment>Remove all assignments first, then try again.</comment>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Confirmation prompt
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new ConfirmationQuestion(
|
||||
"Are you sure you want to delete role '{$roleName}'? [y/N] ",
|
||||
false
|
||||
);
|
||||
|
||||
if (!$helper->ask($input, $output, $question)) {
|
||||
$output->writeln('<comment>Operation cancelled.</comment>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// Delete the role
|
||||
$this->entityManager->remove($role);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$output->writeln("<info>Role '{$roleName}' deleted successfully!</info>");
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,12 @@
|
|||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Apps;
|
||||
use App\Entity\Organizations;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\UserService;
|
||||
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;
|
||||
|
||||
|
|
@ -12,7 +16,7 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||
|
||||
class ApplicationController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly UserService $userService, private readonly ActionService $actionService)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -27,4 +31,73 @@ class ApplicationController extends AbstractController
|
|||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
|
||||
public function edit(int $id, Request $request): Response{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
$this->addFlash('error', "L'application n'existe pas ou n'est pas reconnu.");
|
||||
return $this->redirectToRoute('application_index');
|
||||
}
|
||||
$applicationData = [
|
||||
'id' => $application->getId(),
|
||||
'name' => $application->getName(),
|
||||
'description' => $application->getDescription(),
|
||||
'descriptionSmall' => $application->getDescriptionSmall(),
|
||||
'isActive' => $application->isActive(),
|
||||
];
|
||||
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$data = $request->request->all();
|
||||
$application->setName($data['name']);
|
||||
$application->setDescription($data['description']);
|
||||
$application->setDescriptionSmall($data['descriptionSmall']);
|
||||
$this->entityManager->persist($application);
|
||||
$this->actionService->createAction("Modification de l'application ", $actingUser, null, $application->getId());
|
||||
|
||||
return $this->redirectToRoute('application_index');
|
||||
}
|
||||
return $this->render('application/edit.html.twig', [
|
||||
'apps' => $applicationData,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
#[Route(path: '/authorize/{id}', name: 'authorize', methods: ['POST'])]
|
||||
public function authorize(int $id, Request $request)
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||
}
|
||||
$orgId = $request->get('organizationId');
|
||||
|
||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||
$application->addOrganization($organization);
|
||||
|
||||
$this->actionService->createAction("Authorization d'accès", $actingUser, $organization, $application->getName());
|
||||
return new Response('', Response::HTTP_OK);
|
||||
}
|
||||
|
||||
#[Route(path: '/remove/{id}', name: 'remove', methods: ['POST'])]
|
||||
public function remove(int $id, Request $request)
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$application = $this->entityManager->getRepository(Apps::class)->find($id);
|
||||
if (!$application) {
|
||||
throw $this->createNotFoundException("L'application n'existe pas.");
|
||||
}
|
||||
$orgId = $request->get('organizationId');
|
||||
$organization = $this->entityManager->getRepository(Organizations::Class)->find($orgId);
|
||||
$application->removeOrganization($organization);
|
||||
|
||||
$this->actionService->createAction("Authorization retirer", $actingUser, $organization, $application->getName());
|
||||
|
||||
return new Response('', Response::HTTP_OK);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use App\Entity\User;
|
|||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Form\OrganizationForm;
|
||||
use App\Repository\OrganizationsRepository;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\OrganizationsService;
|
||||
use App\Service\UserOrganizationService;
|
||||
|
|
@ -16,6 +17,7 @@ use App\Service\UserService;
|
|||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Exception;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use App\Entity\Organizations;
|
||||
|
|
@ -28,9 +30,12 @@ class OrganizationController extends AbstractController
|
|||
private const ACCESS_DENIED = 'Access denied';
|
||||
|
||||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserService $userService,
|
||||
private readonly OrganizationsService $organizationsService, private readonly ActionService $actionService, private readonly UserOrganizationService $userOrganizationService)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserService $userService,
|
||||
private readonly OrganizationsService $organizationsService,
|
||||
private readonly ActionService $actionService,
|
||||
private readonly UserOrganizationService $userOrganizationService,
|
||||
private readonly OrganizationsRepository $organizationsRepository,)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -39,8 +44,11 @@ class OrganizationController extends AbstractController
|
|||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
if ($this->isGranted("ROLE_SUPER_ADMIN")) {
|
||||
$organizations = $this->entityManager->getRepository(Organizations::class)->findBy([ 'isDeleted' => false ]);
|
||||
$organizations = $this->organizationsRepository->findBy(['isDeleted' => false]);
|
||||
|
||||
|
||||
} else {
|
||||
//get all the UO of the user
|
||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]);
|
||||
|
|
@ -52,19 +60,29 @@ class OrganizationController extends AbstractController
|
|||
$organizations[] = $uo->getOrganization();
|
||||
}
|
||||
}
|
||||
if(count($organizations) === 1 && $organizations[0]->isActive() === true){
|
||||
if (count($organizations) === 1 && $organizations[0]->isActive() === true) {
|
||||
return $this->redirectToRoute('organization_show', ['id' => $organizations[0]->getId()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Map the entities for tabulator
|
||||
$organizationsData = array_map(function ($org) {
|
||||
return [
|
||||
'id' => $org->getId(),
|
||||
'name' => $org->getName(),
|
||||
'email' => $org->getEmail(),
|
||||
'logoUrl' => $org->getLogoUrl() ? $org->getLogoUrl() : null,
|
||||
'active' => $org->isActive(),
|
||||
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
||||
];
|
||||
}, $organizations);
|
||||
return $this->render('organization/index.html.twig', [
|
||||
'organizations' => $organizations,
|
||||
'organizationsData' => $organizationsData,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/new', name: 'new', methods: ['GET', 'POST'])]
|
||||
public function new(Request $request)
|
||||
public function new(Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
|
@ -75,7 +93,7 @@ class OrganizationController extends AbstractController
|
|||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$logoFile = $form->get('logoUrl')->getData();
|
||||
if ($logoFile) {
|
||||
$this->organizationService->handleLogo($organization, $logoFile);
|
||||
$this->organizationsService->handleLogo($organization, $logoFile);
|
||||
}
|
||||
try {
|
||||
$this->entityManager->persist($organization);
|
||||
|
|
@ -98,11 +116,11 @@ class OrganizationController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
|
||||
public function edit(Request $request, $id)
|
||||
public function edit(Request $request, $id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->entityManager->getRepository(Organizations::class)->find($id);
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
$this->addFlash('error', self::NOT_FOUND);
|
||||
return $this->redirectToRoute('organization_index');
|
||||
|
|
@ -130,6 +148,7 @@ class OrganizationController extends AbstractController
|
|||
$this->organizationsService->handleLogo($organization, $logoFile);
|
||||
}
|
||||
try {
|
||||
$this->entityManager->persist($organization);
|
||||
$this->entityManager->flush();
|
||||
$this->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName());
|
||||
return $this->redirectToRoute('organization_index');
|
||||
|
|
@ -144,10 +163,10 @@ class OrganizationController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route(path: '/view/{id}', name: 'show', methods: ['GET'])]
|
||||
public function view($id)
|
||||
public function view($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$organization = $this->entityManager->getRepository(Organizations::class)->find($id);
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if (!$organization) {
|
||||
$this->addFlash('error', self::NOT_FOUND);
|
||||
|
|
@ -178,7 +197,7 @@ class OrganizationController extends AbstractController
|
|||
|
||||
$apps = $this->organizationsService->appsAccess($allApps, $orgApps);
|
||||
|
||||
$actions = $this->entityManager->getRepository(Actions::class)->findBy(['Organization' => $organization]);
|
||||
$actions = $this->entityManager->getRepository(Actions::class)->findBy(['Organization' => $organization], limit: 15);
|
||||
$activities = $this->actionService->formatActivities($actions);
|
||||
|
||||
$this->actionService->createAction("View Organization", $actingUser, $organization, $organization->getName());
|
||||
|
|
@ -189,15 +208,15 @@ class OrganizationController extends AbstractController
|
|||
'users' => $users,
|
||||
'applications' => $apps,
|
||||
'activities' => $activities,
|
||||
]);
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/delete/{id}', name: 'delete', methods: ['POST'])]
|
||||
public function delete($id)
|
||||
public function delete($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->entityManager->getRepository(Organizations::class)->find($id);
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
|
@ -212,25 +231,27 @@ class OrganizationController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route(path: '/deactivate/{id}', name: 'deactivate', methods: ['POST'])]
|
||||
public function deactivate($id){
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->entityManager->getRepository(Organizations::class)->find($id);
|
||||
if (!$organization) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$organization->setIsActive(false);
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||
$this->entityManager->persist($organization);
|
||||
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName());
|
||||
return $this->redirectToRoute('organization_index');
|
||||
public function deactivate($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$organization->setIsActive(false);
|
||||
// $this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||
$this->entityManager->persist($organization);
|
||||
$this->actionService->createAction("Deactivate Organization", $actingUser, $organization, $organization->getName());
|
||||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
|
||||
#[Route(path: '/activate/{id}', name: 'activate', methods: ['POST'])]
|
||||
public function activate($id){
|
||||
public function activate($id): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$organization = $this->entityManager->getRepository(Organizations::class)->find($id);
|
||||
$organization = $this->organizationsRepository->find($id);
|
||||
if (!$organization) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
|
@ -240,5 +261,74 @@ class OrganizationController extends AbstractController
|
|||
return $this->redirectToRoute('organization_index');
|
||||
}
|
||||
|
||||
// API endpoint to fetch organization data for Tabulator
|
||||
#[Route(path: '/data', name: 'data', methods: ['GET'])]
|
||||
public function data(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
|
||||
|
||||
$page = max(1, (int)$request->query->get('page', 1));
|
||||
$size = max(1, (int)$request->query->get('size', 10));
|
||||
|
||||
// $sorters = $request->query->all('sorters');
|
||||
// $filters = $request->query->all('filters');
|
||||
|
||||
|
||||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
$qb = $this->organizationsRepository->createQueryBuilder('o')
|
||||
->where('o.isDeleted = :del')->setParameter('del', false);
|
||||
|
||||
// // Example: apply filters (basic equals/like)
|
||||
// foreach ($filters as $f) {
|
||||
// if (!isset($f['field'], $f['type'])) { continue; }
|
||||
// $param = 'p_' . $f['field'];
|
||||
// if ($f['type'] === 'like' || $f['type'] === 'contains') {
|
||||
// $qb->andWhere("LOWER(o.{$f['field']}) LIKE :$param")
|
||||
// ->setParameter($param, '%' . mb_strtolower((string)$f['value']) . '%');
|
||||
// } elseif ($f['type'] === '=') {
|
||||
// $qb->andWhere("o.{$f['field']} = :$param")
|
||||
// ->setParameter($param, $f['value']);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Example: apply sorters
|
||||
// foreach ($sorters as $s) {
|
||||
// if (!isset($s['field'], $s['dir'])) { continue; }
|
||||
// $dir = strtolower($s['dir']) === 'desc' ? 'DESC' : 'ASC';
|
||||
// $qb->addOrderBy('o.' . $s['field'], $dir);
|
||||
// }
|
||||
|
||||
// Count total
|
||||
$countQb = clone $qb;
|
||||
$total = (int)$countQb->select('COUNT(o.id)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
// Pagination
|
||||
$offset = ($page - 1) * $size;
|
||||
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||
|
||||
// Map to array
|
||||
$data = array_map(function (Organizations $org) {
|
||||
return [
|
||||
'id' => $org->getId(),
|
||||
'name' => $org->getName(),
|
||||
'email' => $org->getEmail(),
|
||||
'logoUrl' => $org->getLogoUrl() ?: null,
|
||||
'active' => $org->isActive(),
|
||||
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
// Tabulator expects: data, last_page (total pages), or total row count depending on config
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
'last_page' => $lastPage,
|
||||
'total' => $total, // optional, useful for debugging
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,20 +3,21 @@
|
|||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Apps;
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Form\UserForm;
|
||||
use App\Repository\OrganizationsRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Repository\UsersOrganizationsRepository;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\UserOrganizationAppService;
|
||||
use App\Service\UserOrganizationService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Asset\Packages;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
|
@ -28,9 +29,14 @@ class UserController extends AbstractController
|
|||
private const ACCESS_DENIED = 'Access denied';
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserService $userService,
|
||||
private readonly ActionService $actionService, private readonly UserOrganizationAppService $userOrganizationAppService, private readonly UserOrganizationService $userOrganizationService,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserService $userService,
|
||||
private readonly ActionService $actionService,
|
||||
private readonly UserOrganizationAppService $userOrganizationAppService,
|
||||
private readonly UserOrganizationService $userOrganizationService,
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly UsersOrganizationsRepository $uoRepository,
|
||||
private readonly OrganizationsRepository $organizationRepository,
|
||||
)
|
||||
{
|
||||
}
|
||||
|
|
@ -43,24 +49,15 @@ class UserController extends AbstractController
|
|||
$user = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findUsersWithOrganization();
|
||||
$uo = $this->uoRepository->findUsersWithOrganization();
|
||||
$noOrgUsers = $this->userService->formatNoOrgUsersAsAssoc(
|
||||
$this->entityManager->getRepository(User::class)->findUsersWithoutOrganization());
|
||||
$this->userRepository->findUsersWithoutOrganization());
|
||||
$usersByOrganization = $this->userService->groupByOrganization($uo);
|
||||
$usersByOrganization += $noOrgUsers;
|
||||
|
||||
//Log action
|
||||
$this->actionService->createAction("View all users", $user, null, "All");
|
||||
|
||||
} elseif ($this->isGranted('ROLE_ADMIN')) {
|
||||
$orgIds = $this->userService->getAdminOrganizationsIds($user);
|
||||
if (empty($orgIds)) {
|
||||
$usersByOrganization = [];
|
||||
} else {
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findUsersWithOrganization($orgIds);
|
||||
$usersByOrganization = $this->userService->groupByOrganization($uo);
|
||||
$this->actionService->createAction("View all users for organizations", $user, null, implode(", ", $orgIds));
|
||||
}
|
||||
} else {
|
||||
$usersByOrganization = [];
|
||||
}
|
||||
|
|
@ -76,18 +73,18 @@ class UserController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser)) {
|
||||
$user = $this->entityManager->getRepository(User::class)->find($id);
|
||||
$user = $this->userRepository->find($id);
|
||||
try {
|
||||
$orgId = $request->query->get('organizationId');
|
||||
if ($orgId) {
|
||||
$orgs = $this->entityManager->getRepository(Organizations::class)->findBy(['id' => $orgId]);
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user, 'organization' => $orgs]);
|
||||
$orgs = $this->organizationRepository->findBy(['id' => $orgId]);
|
||||
$uo = $this->uoRepository->findBy(['users' => $user, 'organization' => $orgs]);
|
||||
if (!$uo) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uoActive = $uo[0]->isActive();
|
||||
} else {
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user, 'isActive' => true]);
|
||||
$uo = $this->uoRepository->findBy(['users' => $user, 'isActive' => true]);
|
||||
foreach ($uo as $u) {
|
||||
$orgs[] = $u->getOrganization();
|
||||
}
|
||||
|
|
@ -116,7 +113,7 @@ class UserController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser)) {
|
||||
$user = $this->entityManager->getRepository(User::class)->find($id);
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
|
@ -134,7 +131,7 @@ class UserController extends AbstractController
|
|||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
if ($request->get('organizationId')) {
|
||||
$org = $this->entityManager->getRepository(Organizations::class)->find($request->get('organizationId'));
|
||||
$org = $this->organizationRepository->find($request->get('organizationId'));
|
||||
if ($org) {
|
||||
$this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier());
|
||||
}
|
||||
|
|
@ -172,13 +169,14 @@ class UserController extends AbstractController
|
|||
|
||||
if ($picture) {
|
||||
$this->userService->handleProfilePicture($user, $picture);
|
||||
} else {
|
||||
$user->setPictureUrl("");
|
||||
}
|
||||
// else {
|
||||
// $user->setPictureUrl("");
|
||||
// }
|
||||
//FOR TEST PURPOSES, SETTING A DEFAULT RANDOM PASSWORD
|
||||
$user->setPassword($this->userService->generateRandomPassword());
|
||||
if ($orgId) {
|
||||
$org = $this->entityManager->getRepository(Organizations::class)->find($orgId);
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if ($org) {
|
||||
$uo = new UsersOrganizations();
|
||||
$uo->setUsers($user);
|
||||
|
|
@ -211,13 +209,16 @@ class UserController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$user = $this->entityManager->getRepository(User::class)->find($id);
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$user->setIsActive(false);
|
||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($user, $actingUser);
|
||||
if($this->userService->isUserConnected($user->getUserIdentifier())){
|
||||
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
||||
}
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->actionService->createAction("Deactivate user", $actingUser, null, $user->getUserIdentifier());
|
||||
|
|
@ -234,7 +235,7 @@ class UserController extends AbstractController
|
|||
$this->denyAccessUnlessGranted('ROLE_ADMIN');
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$user = $this->entityManager->getRepository(User::class)->find($id);
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
|
@ -257,15 +258,15 @@ class UserController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$orgId = $request->get('organizationId');
|
||||
$org = $this->entityManager->getRepository(Organizations::class)->find($orgId);
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$user = $this->entityManager->getRepository(User::class)->find($id);
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $user,
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||
'organization' => $org,
|
||||
'isActive' => true]);
|
||||
if (!$uo) {
|
||||
|
|
@ -277,7 +278,7 @@ class UserController extends AbstractController
|
|||
$this->entityManager->flush();
|
||||
$this->actionService->createAction("Deactivate user in organization", $actingUser, $org, $org->getName() . " for user " . $user->getUserIdentifier());
|
||||
|
||||
return $this->redirectToRoute('user_index');
|
||||
return new Response('', Response::HTTP_NO_CONTENT); //204
|
||||
}
|
||||
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
|
|
@ -290,15 +291,15 @@ class UserController extends AbstractController
|
|||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true)) {
|
||||
$orgId = $request->get('organizationId');
|
||||
$org = $this->entityManager->getRepository(Organizations::class)->find($orgId);
|
||||
$org = $this->organizationRepository->find($orgId);
|
||||
if (!$org) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$user = $this->entityManager->getRepository(User::class)->find($id);
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $user,
|
||||
$uo = $this->uoRepository->findOneBy(['users' => $user,
|
||||
'organization' => $org,
|
||||
'isActive' => false]);
|
||||
if (!$uo) {
|
||||
|
|
@ -315,12 +316,12 @@ class UserController extends AbstractController
|
|||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
#[Route('/delete/{id}', name: 'delete', methods: ['GET'])]
|
||||
#[Route('/delete/{id}', name: 'delete', methods: ['GET', 'POST'])]
|
||||
public function delete(int $id, Request $request): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_SUPER_ADMIN");
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
$user = $this->entityManager->getRepository(User::class)->find($id);
|
||||
$user = $this->userRepository->find($id);
|
||||
if (!$user) {
|
||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
||||
}
|
||||
|
|
@ -328,10 +329,14 @@ class UserController extends AbstractController
|
|||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($user, $actingUser);
|
||||
$user->setIsDeleted(true);
|
||||
if($this->userService->isUserConnected($user)){
|
||||
$this->userService->revokeUserTokens($user->getUserIdentifier());
|
||||
}
|
||||
$this->entityManager->persist($user);
|
||||
$this->entityManager->flush();
|
||||
$this->actionService->createAction("Delete user", $actingUser, null, $user->getUserIdentifier());
|
||||
return $this->redirectToRoute('user_index');
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT); //204
|
||||
}
|
||||
|
||||
#[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])]
|
||||
|
|
@ -354,7 +359,7 @@ class UserController extends AbstractController
|
|||
throw $this->createNotFoundException('Default role not found');
|
||||
}
|
||||
|
||||
if (in_array($roleUser->getId(), $selectedRolesIds)) {
|
||||
if (!empty($selectedRolesIds)) {
|
||||
$this->userOrganizationAppService->syncRolesForUserOrganizationApp(
|
||||
$uo,
|
||||
$application,
|
||||
|
|
@ -367,12 +372,230 @@ class UserController extends AbstractController
|
|||
|
||||
$user = $uo->getUsers();
|
||||
return $this->redirectToRoute('user_show', [
|
||||
'user' => $user,
|
||||
'id' => $user->getId(),
|
||||
'organizationId'=> $uo->getOrganization()->getId()
|
||||
'user' => $user,
|
||||
'id' => $user->getId(),
|
||||
'organizationId' => $uo->getOrganization()->getId()
|
||||
]);
|
||||
}
|
||||
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
/*
|
||||
* AJAX endpoint for user listing with pagination
|
||||
* Get all the users that aren´t deleted and are active
|
||||
*/
|
||||
#[Route(path: '/data', name: 'data', methods: ['GET'])]
|
||||
public function data(Request $request): JsonResponse
|
||||
{
|
||||
$this->denyAccessUnlessGranted("ROLE_ADMIN");
|
||||
|
||||
$page = max(1, (int)$request->query->get('page', 1));
|
||||
$size = max(1, (int)$request->query->get('size', 10));
|
||||
|
||||
// Get filter parameters
|
||||
$filters = $request->query->all('filter', []);
|
||||
|
||||
$repo = $this->userRepository;
|
||||
|
||||
// Base query
|
||||
$qb = $repo->createQueryBuilder('u')
|
||||
->where('u.isDeleted = :del')->setParameter('del', false);
|
||||
|
||||
// Apply filters
|
||||
if (!empty($filters['name'])) {
|
||||
$qb->andWhere('u.surname LIKE :name')
|
||||
->setParameter('name', '%' . $filters['name'] . '%');
|
||||
}
|
||||
if (!empty($filters['prenom'])) {
|
||||
$qb->andWhere('u.name LIKE :prenom')
|
||||
->setParameter('prenom', '%' . $filters['prenom'] . '%');
|
||||
}
|
||||
if (!empty($filters['email'])) {
|
||||
$qb->andWhere('u.email LIKE :email')
|
||||
->setParameter('email', '%' . $filters['email'] . '%');
|
||||
}
|
||||
|
||||
$countQb = clone $qb;
|
||||
$total = (int)$countQb->select('COUNT(u.id)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
// Pagination
|
||||
$offset = ($page - 1) * $size;
|
||||
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||
|
||||
// Map to array
|
||||
$data = array_map(function (User $user) {
|
||||
return [
|
||||
'id' => $user->getId(),
|
||||
'pictureUrl' => $user->getPictureUrl(),
|
||||
'name' => $user->getSurname(),
|
||||
'prenom' => $user->getName(),
|
||||
'email' => $user->getEmail(),
|
||||
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||
'statut' => $user->isActive(),
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
'last_page' => $lastPage,
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route(path: '/indexTest', name: 'indexTest', methods: ['GET'])]
|
||||
public function indexTest(): Response
|
||||
{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$totalUsers = $this->userRepository->count(['isDeleted' => false, 'isActive' => true]);
|
||||
return $this->render('user/indexTest.html.twig', [
|
||||
'users' => $totalUsers
|
||||
]);
|
||||
}
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
}
|
||||
|
||||
/*
|
||||
* AJAX endpoint for new users listing
|
||||
* Get the 5 most recently created users for an organization
|
||||
*/
|
||||
#[Route(path: '/data/new', name: 'dataNew', methods: ['GET'])]
|
||||
public function dataNew(Request $request): JsonResponse
|
||||
{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$orgId = $request->query->get('orgId');
|
||||
$uos = $this->uoRepository->findBy(['organization' => $orgId, 'isActive' =>true], limit: 5, orderBy: ['createdAt' => 'DESC']);
|
||||
|
||||
|
||||
// Map to array (keep isConnected)
|
||||
$data = array_map(function (UsersOrganizations $uo) {
|
||||
$user = $uo->getUsers();
|
||||
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||||
return [
|
||||
'pictureUrl' => $user->getPictureUrl(),
|
||||
'email' => $user->getEmail(),
|
||||
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||
'initials' => strtoupper($initials),
|
||||
];
|
||||
}, $uos);
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* AJAX endpoint for admin users listing
|
||||
* Get all admin users for an organization
|
||||
*/
|
||||
|
||||
#[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])]
|
||||
public function dataAdmin(Request $request): JsonResponse
|
||||
{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$orgId = $request->query->get('orgId');
|
||||
$uos = $this->uoRepository->findBy(['organization' => $orgId]);
|
||||
$roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']);
|
||||
$users = [];
|
||||
foreach ($uos as $uo) {
|
||||
if ($this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin])) {
|
||||
$users[] = $uo;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Map to array (keep isConnected)
|
||||
$data = array_map(function (UsersOrganizations $uo) {
|
||||
$user = $uo->getUsers();
|
||||
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||||
return [
|
||||
'pictureUrl' => $user->getPictureUrl(),
|
||||
'email' => $user->getEmail(),
|
||||
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||
'initials' => strtoupper($initials),
|
||||
];
|
||||
}, $users);
|
||||
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* AJAX endpoint for All users in an organization
|
||||
*/
|
||||
#[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])]
|
||||
public function dataUserOrganization(Request $request): JsonResponse
|
||||
{
|
||||
$actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier());
|
||||
|
||||
if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) {
|
||||
$orgId = $request->query->get('orgId');
|
||||
$page = max(1, (int)$request->query->get('page', 1));
|
||||
$size = max(1, (int)$request->query->get('size', 10));
|
||||
|
||||
// Optional: read Tabulator remote sort/filter payloads
|
||||
// $sorters = $request->query->all('sorters') ?? [];
|
||||
// $filters = $request->query->all('filters') ?? [];
|
||||
|
||||
$repo = $this->uoRepository;
|
||||
|
||||
// Base query
|
||||
$qb = $repo->createQueryBuilder('uo')
|
||||
->where('uo.organization = :orgId')
|
||||
->setParameter('orgId', $orgId);
|
||||
$countQb = clone $qb;
|
||||
$total = (int)$countQb->select('COUNT(uo.id)')->getQuery()->getSingleScalarResult();
|
||||
|
||||
// Pagination
|
||||
$offset = ($page - 1) * $size;
|
||||
$rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult();
|
||||
|
||||
// Map to array
|
||||
$data = array_map(function (UsersOrganizations $uo) {
|
||||
$user = $uo->getUsers();
|
||||
return [
|
||||
'pictureUrl' => $user->getPictureUrl(),
|
||||
'name' => $user->getSurname(),
|
||||
'prenom' => $user->getName(),
|
||||
'email' => $user->getEmail(),
|
||||
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||
'statut' => $uo->isActive(),
|
||||
'showUrl' => $this->generateUrl('user_show', [
|
||||
'id' => $user->getId(),
|
||||
'organizationId' => $uo->getOrganization()->getId(),
|
||||
]),
|
||||
'id' => $user->getId(),
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
// Return Tabulator-compatible response
|
||||
$lastPage = (int)ceil($total / $size);
|
||||
|
||||
return $this->json([
|
||||
'data' => $data,
|
||||
'last_page' => $lastPage,
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
throw $this->createAccessDeniedException(self::ACCESS_DENIED);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace App\Entity;
|
|||
use App\Repository\AppsRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: AppsRepository::class)]
|
||||
|
|
@ -24,7 +25,7 @@ class Apps
|
|||
#[ORM\Column(length: 255)]
|
||||
private ?string $logo_url = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
|
|
@ -33,7 +34,7 @@ class Apps
|
|||
#[ORM\Column(options: ['default' => true])]
|
||||
private ?bool $isActive = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $descriptionSmall = null;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ class Roles
|
|||
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
}
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $pictureUrl = null;
|
||||
|
||||
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
// src/Security/UserChecker.php
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\UsersOrganizations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
|
||||
|
||||
class UserChecker implements UserCheckerInterface
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function checkPreAuth(UserInterface $user): void
|
||||
{
|
||||
// runs before password is checked
|
||||
}
|
||||
|
||||
public function checkPostAuth(UserInterface $user): void
|
||||
{
|
||||
// runs after credentials are validated
|
||||
if (method_exists($user, 'isDeleted') && $user->isDeleted()) {
|
||||
throw new CustomUserMessageAccountStatusException('Votre compte a été supprimé.');
|
||||
}
|
||||
|
||||
// check if the user account is active
|
||||
if (method_exists($user, 'isActive') && !$user->isActive()) {
|
||||
throw new CustomUserMessageAccountStatusException('Votre compte est désactivé.');
|
||||
}
|
||||
|
||||
//check if the user is in an organization
|
||||
$uo = $this->entityManager->getRepository(UsersOrganizations::class)->findOneBy(['users' => $user, 'isActive' => true]);
|
||||
if (!$uo) {
|
||||
throw new CustomUserMessageAccountStatusException('Vous n\'êtes pas relié à une organisation. veuillez contacter un administrateur.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ readonly class ActionService
|
|||
if ($diffInSeconds < 60 * 60) { // less than 1 hour
|
||||
return '#247208';
|
||||
}
|
||||
return '#C76633';
|
||||
return '#cc664c';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,44 +4,31 @@ namespace App\Service;
|
|||
|
||||
use App\Entity\Apps;
|
||||
use App\Entity\Organizations;
|
||||
use App\Entity\Roles;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use SebastianBergmann\CodeCoverage\Util\DirectoryCouldNotBeCreatedException;
|
||||
use App\Service\AwsService;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
|
||||
class OrganizationsService
|
||||
{
|
||||
private string $logoDirectory;
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
string $logoDirectory
|
||||
|
||||
public function __construct(
|
||||
string $logoDirectory, private readonly AwsService $awsService
|
||||
)
|
||||
{
|
||||
$this->logoDirectory = $logoDirectory;
|
||||
}
|
||||
|
||||
public function handleLogo(Organizations $organization, $logoFile)
|
||||
public function handleLogo(Organizations $organization, $logoFile): void
|
||||
{
|
||||
$extension = $logoFile->guessExtension();
|
||||
|
||||
$customFilename = $organization->getName() . '_'. date('dmyHis') . "." . $extension;
|
||||
$customFilename = $organization->getName() . '_' . date('dmyHis') . "." . $extension;
|
||||
|
||||
$uploadDirectory = $this->logoDirectory;
|
||||
|
||||
if (!is_dir($uploadDirectory) && !mkdir($uploadDirectory, 0755, true) && !is_dir($uploadDirectory)) {
|
||||
throw new DirectoryCouldNotBeCreatedException(sprintf('Directory "%s" was not created', $uploadDirectory));
|
||||
}
|
||||
try {
|
||||
|
||||
// Move the file to the upload directory
|
||||
$logoFile->move($uploadDirectory, $customFilename);
|
||||
|
||||
// Update user entity with the file path (relative to public directory)
|
||||
$organization->setLogoUrl('uploads/logos/' . $customFilename);
|
||||
|
||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $logoFile, $customFilename, $extension, 'logo/');
|
||||
$organization->setLogoUrl('logo/' . $customFilename);
|
||||
} catch (FileException $e) {
|
||||
// Handle upload error
|
||||
throw new FileException('File upload failed: ' . $e->getMessage());
|
||||
throw new FileException('Failed to upload logo to S3: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +47,7 @@ class OrganizationsService
|
|||
$result = [];
|
||||
foreach ($appsAll as $app) {
|
||||
$result[] = [
|
||||
'entity' => $app, // Keep the full entity for Twig
|
||||
'entity' => $app, // Keep the full entity for Twig
|
||||
'hasAccess' => in_array($app->getId(), $orgAppIds, true),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ use App\Entity\User;
|
|||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Service\ActionService;
|
||||
use App\Service\UserService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
class UserOrganizationAppService
|
||||
{
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService)
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -28,6 +30,9 @@ class UserOrganizationAppService
|
|||
$grouped = [];
|
||||
|
||||
foreach ($userOrgApps as $uoa) {
|
||||
if(!$uoa->getRole()->getName() === 'USER') {
|
||||
continue; // Skip USER role
|
||||
}
|
||||
$app = $uoa->getApplication();
|
||||
$appId = $app->getId();
|
||||
$roleEntity = $uoa->getRole();
|
||||
|
|
@ -35,9 +40,9 @@ class UserOrganizationAppService
|
|||
if (!isset($grouped[$appId])) {
|
||||
$grouped[$appId] = [
|
||||
'uoId' => $uoa->getUserOrganization()->getId(),
|
||||
'application' => $app, // you can still pass entity here
|
||||
'roles' => [], // selected roles for display
|
||||
'rolesArray' => [], // all possible roles
|
||||
'application' => $app,
|
||||
'roles' => [],
|
||||
'rolesArray' => [],
|
||||
'selectedRoleIds' => [],
|
||||
];
|
||||
}
|
||||
|
|
@ -49,11 +54,21 @@ class UserOrganizationAppService
|
|||
$grouped[$appId]['selectedRoleIds'][] = $roleEntity->getId();
|
||||
}
|
||||
|
||||
// roles are the same for all apps → load once, inject into each appGroup
|
||||
// Load all possible roles once
|
||||
$allRoles = $this->entityManager->getRepository(Roles::class)->findAll();
|
||||
|
||||
foreach ($grouped as &$appGroup) {
|
||||
foreach ($allRoles as $role) {
|
||||
// exclude SUPER ADMIN from assignable roles if current user is just ADMIN
|
||||
if ($this->security->isGranted('ROLE_ADMIN') && !$this->security->isGranted('ROLE_SUPER_ADMIN')
|
||||
&& $role->getName() === 'SUPER ADMIN') {
|
||||
continue;
|
||||
}
|
||||
// exclude USER role from assignable roles
|
||||
if ($role->getName() === 'USER') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$appGroup['rolesArray'][] = [
|
||||
'id' => $role->getId(),
|
||||
'name' => $role->getName(),
|
||||
|
|
@ -85,24 +100,45 @@ class UserOrganizationAppService
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes user roles for a specific application within an organization.
|
||||
*
|
||||
* This method handles the complete lifecycle of user-application role assignments:
|
||||
* - Activates/deactivates existing role links based on selection
|
||||
* - Creates new role assignments for newly selected roles
|
||||
* - Updates the user's global Symfony security roles when ADMIN/SUPER_ADMIN roles are assigned
|
||||
*
|
||||
* @param UsersOrganizations $uo The user-organization relationship
|
||||
* @param Apps $application The target application
|
||||
* @param array $selectedRoleIds Array of role IDs that should be active for this user-app combination
|
||||
* @param User $actingUser The user performing this action (for audit logging)
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception If role entities cannot be found or persisted
|
||||
*/
|
||||
public function syncRolesForUserOrganizationApp(
|
||||
UsersOrganizations $uo,
|
||||
Apps $application,
|
||||
array $selectedRoleIds,
|
||||
User $actingUser
|
||||
): void {
|
||||
$repo = $this->entityManager->getRepository(UserOrganizatonApp::class);
|
||||
$currentLinks = $repo->findBy([
|
||||
|
||||
// Fetch existing UserOrganizationApp links for this user and application
|
||||
$uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy([
|
||||
'userOrganization' => $uo,
|
||||
'application' => $application,
|
||||
]);
|
||||
|
||||
$currentRoleIds = [];
|
||||
foreach ($currentLinks as $uoa) {
|
||||
// Process existing role links - activate or deactivate based on selection
|
||||
foreach ($uoas as $uoa) {
|
||||
$roleId = $uoa->getRole()->getId();
|
||||
$currentRoleIds[] = $roleId;
|
||||
$roleName = $uoa->getRole()->getName();
|
||||
|
||||
if (in_array($roleId, $selectedRoleIds)) {
|
||||
if (in_array((string) $roleId, $selectedRoleIds, true)) {
|
||||
// Role is selected - ensure it's active
|
||||
if (!$uoa->isActive()) {
|
||||
$uoa->setIsActive(true);
|
||||
$this->entityManager->persist($uoa);
|
||||
|
|
@ -110,10 +146,19 @@ class UserOrganizationAppService
|
|||
"Re-activate user role for application",
|
||||
$actingUser,
|
||||
$uo->getOrganization(),
|
||||
"App: {$application->getName()}, Role: {$uoa->getRole()->getName()} for user {$uo->getUsers()->getUserIdentifier()}"
|
||||
"App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}"
|
||||
);
|
||||
// Sync Admins roles to user's global Symfony security roles
|
||||
if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) {
|
||||
$this->userService->syncUserRoles($uo->getUsers(), $roleName, true);
|
||||
}
|
||||
// Ensure ADMIN role is assigned if SUPER ADMIN is activated
|
||||
if ($roleName === 'SUPER ADMIN') {
|
||||
$this->ensureAdminRoleForSuperAdmin($uoa);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Role is not selected - ensure it's inactive
|
||||
if ($uoa->isActive()) {
|
||||
$uoa->setIsActive(false);
|
||||
$this->entityManager->persist($uoa);
|
||||
|
|
@ -122,23 +167,37 @@ class UserOrganizationAppService
|
|||
"Deactivate user role for application",
|
||||
$actingUser,
|
||||
$uo->getOrganization(),
|
||||
"App: {$application->getName()}, Role: {$uoa->getRole()->getName()} for user {$uo->getUsers()->getUserIdentifier()}"
|
||||
"App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}"
|
||||
);
|
||||
// Sync Admins roles to user's global Symfony security roles
|
||||
if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) {
|
||||
$this->userService->syncUserRoles($uo->getUsers(), $roleName, false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add missing roles
|
||||
// Create new role assignments for roles that don't exist yet
|
||||
foreach ($selectedRoleIds as $roleId) {
|
||||
if (!in_array($roleId, $currentRoleIds)) {
|
||||
$role = $this->entityManager->getRepository(Roles::class)->find($roleId);
|
||||
if ($role) {
|
||||
// Create new user-organization-application role link
|
||||
$newUoa = new UserOrganizatonApp();
|
||||
$newUoa->setUserOrganization($uo);
|
||||
$newUoa->setApplication($application);
|
||||
$newUoa->setRole($role);
|
||||
$newUoa->setIsActive(true);
|
||||
|
||||
// Sync Admins roles to user's global Symfony security roles
|
||||
if (in_array($role->getName(), ['ADMIN', 'SUPER ADMIN'], true)) {
|
||||
$this->userService->syncUserRoles($uo->getUsers(), $role->getName(), true);
|
||||
}
|
||||
// Ensure ADMIN role is assigned if SUPER ADMIN is activated
|
||||
if ($role->getName() === 'SUPER ADMIN') {
|
||||
$this->ensureAdminRoleForSuperAdmin($newUoa);
|
||||
}
|
||||
$this->entityManager->persist($newUoa);
|
||||
$this->actionService->createAction("New user role for application",
|
||||
$actingUser,
|
||||
|
|
@ -147,8 +206,34 @@ class UserOrganizationAppService
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute the role Admin to the user if the user has the role Super Admin
|
||||
*
|
||||
* @param UserOrganizatonApp $uoa
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ensureAdminRoleForSuperAdmin(UserOrganizatonApp $uoa): void
|
||||
{
|
||||
$uoaAdmin = $this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy([
|
||||
'userOrganization' => $uoa->getUserOrganization(),
|
||||
'application' => $uoa->getApplication(),
|
||||
'role' => $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN'])
|
||||
]);
|
||||
if(!$uoaAdmin) {
|
||||
$uoaAdmin = new UserOrganizatonApp();
|
||||
$uoaAdmin->setUserOrganization($uoa->getUserOrganization());
|
||||
$uoaAdmin->setApplication($uoa->getApplication());
|
||||
$uoaAdmin->setRole($this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']));
|
||||
$uoaAdmin->setIsActive(true);
|
||||
$this->entityManager->persist($uoaAdmin);
|
||||
}
|
||||
// If the ADMIN role link exists but is inactive, activate it
|
||||
if ($uoaAdmin && !$uoaAdmin->isActive()) {
|
||||
$uoaAdmin->setIsActive(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use App\Entity\Roles;
|
|||
use App\Entity\User;
|
||||
use App\Entity\UserOrganizatonApp;
|
||||
use App\Entity\UsersOrganizations;
|
||||
use App\Service\AwsService;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
|
|
@ -26,7 +27,7 @@ class UserService
|
|||
|
||||
public function __construct(private readonly EntityManagerInterface $entityManager,
|
||||
private readonly Security $security,
|
||||
string $profileDirectory
|
||||
string $profileDirectory, private readonly AwsService $awsService
|
||||
)
|
||||
{
|
||||
$this->profileDirectory = $profileDirectory;
|
||||
|
|
@ -78,6 +79,7 @@ class UserService
|
|||
|
||||
/**
|
||||
* Check if the user have the rights to access the page
|
||||
* Self check can be skipped when checking access for the current user
|
||||
*
|
||||
* @param User $user
|
||||
* @param bool $skipSelfCheck
|
||||
|
|
@ -194,11 +196,13 @@ class UserService
|
|||
|
||||
$orgId = $org->getId();
|
||||
$orgName = $org->getName();
|
||||
$orgLogo = $org->getLogoUrl();
|
||||
|
||||
if (!isset($grouped[$orgId])) {
|
||||
$grouped[$orgId] = [
|
||||
'id' => $orgId,
|
||||
'name' => $orgName,
|
||||
'logo' => $orgLogo,
|
||||
'users' => [],
|
||||
];
|
||||
}
|
||||
|
|
@ -246,25 +250,32 @@ class UserService
|
|||
|
||||
// Create custom filename: userNameUserSurname_ddmmyyhhmmss
|
||||
$customFilename = $user->getName() . $user->getSurname() . '_' . date('dmyHis') . '.' . $extension;
|
||||
try{
|
||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename , $extension, 'profile/');
|
||||
|
||||
// Define upload directory
|
||||
$uploadDirectory = $this->profileDirectory;
|
||||
// Create directory if it doesn't exist
|
||||
if (!is_dir($uploadDirectory) && !mkdir($uploadDirectory, 0755, true) && !is_dir($uploadDirectory)) {
|
||||
throw new DirectoryCouldNotBeCreatedException(sprintf('Directory "%s" was not created', $uploadDirectory));
|
||||
}
|
||||
try {
|
||||
|
||||
// Move the file to the upload directory
|
||||
$picture->move($uploadDirectory, $customFilename);
|
||||
|
||||
// Update user entity with the file path (relative to public directory)
|
||||
$user->setPictureUrl('uploads/profile/' . $customFilename);
|
||||
|
||||
$user->setPictureUrl('profile/'.$customFilename);
|
||||
} catch (FileException $e) {
|
||||
// Handle upload error
|
||||
throw new FileException('File upload failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// // Define upload directory
|
||||
// $uploadDirectory = $this->profileDirectory;
|
||||
// // Create directory if it doesn't exist
|
||||
// if (!is_dir($uploadDirectory) && !mkdir($uploadDirectory, 0755, true) && !is_dir($uploadDirectory)) {
|
||||
// throw new DirectoryCouldNotBeCreatedException(sprintf('Directory "%s" was not created', $uploadDirectory));
|
||||
// }
|
||||
// try {
|
||||
//
|
||||
// // Move the file to the upload directory
|
||||
// $picture->move($uploadDirectory, $customFilename);
|
||||
//
|
||||
// // Update user entity with the file path (relative to public directory)
|
||||
// $user->setPictureUrl('uploads/profile/' . $customFilename);
|
||||
//
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,12 +292,91 @@ class UserService
|
|||
$user = $uo->getUsers();
|
||||
|
||||
$users[] = [
|
||||
'entity' => $user,
|
||||
'entity' => $user,
|
||||
'connected' => $this->isUserConnected($user->getUserIdentifier()),
|
||||
'isActive' => (bool)$uo->isActive(),
|
||||
'isActive' => (bool)$uo->isActive(),
|
||||
];
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user's role synchronization
|
||||
*
|
||||
* @param User $user
|
||||
* @param string $role
|
||||
* @param boolean $add
|
||||
* @return void
|
||||
*/
|
||||
public function syncUserRoles(User $user, string $role, bool $add): void
|
||||
{
|
||||
$roleFormatted = $this->formatRoleString($role);
|
||||
|
||||
if ($add) {
|
||||
// Add the main role if not already present
|
||||
if (!in_array($roleFormatted, $user->getRoles(), true)) {
|
||||
$user->setRoles(array_merge($user->getRoles(), [$roleFormatted]));
|
||||
}
|
||||
|
||||
// If SUPER ADMIN is given → ensure ADMIN is also present
|
||||
if ($roleFormatted === 'ROLE_SUPER_ADMIN' && !in_array('ROLE_ADMIN', $user->getRoles(), true)) {
|
||||
$user->setRoles(array_merge($user->getRoles(), ['ROLE_ADMIN']));
|
||||
}
|
||||
} else {
|
||||
// Remove the role if present and not used elsewhere
|
||||
if (in_array($roleFormatted, $user->getRoles(), true)) {
|
||||
$uos = $this->entityManager->getRepository(UsersOrganizations::class)
|
||||
->findBy(['users' => $user, 'isActive' => true]);
|
||||
|
||||
$hasRole = false;
|
||||
foreach ($uos as $uo) {
|
||||
$uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)
|
||||
->findBy([
|
||||
'userOrganization' => $uo,
|
||||
'isActive' => true,
|
||||
'role' => $this->entityManager->getRepository(Roles::class)
|
||||
->findOneBy(['name' => $role]),
|
||||
]);
|
||||
|
||||
if ($uoa) {
|
||||
$hasRole = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only remove globally if no other app gives this role
|
||||
if (!$hasRole) {
|
||||
$roles = $user->getRoles();
|
||||
$roles = array_filter($roles, fn($r) => $r !== $roleFormatted);
|
||||
$user->setRoles($roles);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format role string to match the ROLE_ convention
|
||||
*/
|
||||
public function formatRoleString(string $role): string
|
||||
{
|
||||
$role = str_replace(' ', '_', trim($role));
|
||||
$role = strtoupper($role);
|
||||
if (str_starts_with($role, 'ROLE_')) {
|
||||
return $role;
|
||||
}
|
||||
return 'ROLE_' . $role;
|
||||
}
|
||||
|
||||
public function revokeUserTokens(String $userIdentifier)
|
||||
{
|
||||
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
|
||||
'userIdentifier' => $userIdentifier,
|
||||
'revoked' => false
|
||||
]);
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$token->revoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,27 @@
|
|||
{% block body %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h3><img width=10% src="{{ asset(application.logoUrl) }}" alt="Logo {{ application.title }}">
|
||||
{{ application.name }}</h3>
|
||||
<div class="card no-header-bg">
|
||||
|
||||
<div class="card-header d-flex gap-2 mt-2">
|
||||
<img class="rounded-circle " style="width:50px; height:50px;" src="{{ asset(application.logoUrl) }}"
|
||||
alt="Logo application">
|
||||
<div class="card-title">
|
||||
<h1>{{ application.name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body d-flex flex-column align-items-center">
|
||||
<p class="card-text">{{ application.description|raw }}</p>
|
||||
<div>
|
||||
<a href="http://{{ application.subDomain }}.solutions-easy.moi" class="btn btn-primary me-2">Accéder à
|
||||
l'application</a>
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('application_edit', {'id': application.id}) }}" class="btn btn-secondary">Modifier
|
||||
l'application</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column align-items-center">
|
||||
<p class="card-text">{{ application.description }}</p>
|
||||
<div>
|
||||
<a href="http://{{ application.subDomain }}.solutions-easy.moi" class="btn btn-primary me-2">Accéder à l'application</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,24 +1,54 @@
|
|||
{% block body %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card ">
|
||||
<div class="card-header d-flex gap-2">
|
||||
<img class="rounded-circle " style="width:50px; height:50px;" src="{{ asset(application.entity.logoUrl) }}"
|
||||
alt="Logo application">
|
||||
<div class="card-title">
|
||||
<h3><img width=10% src="{{ asset(application.entity.logoUrl) }}" alt="Logo application">
|
||||
{{ application.entity.name }}</h3>
|
||||
<h1>{{ application.entity.name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column align-items-center">
|
||||
<p class="card-text">{{ application.entity.descriptionSmall }}</p>
|
||||
{% if application.hasAccess %}
|
||||
<div >
|
||||
<a href="http://{{ application.entity.subDomain }}.solutions-easy.moi" class="btn btn-primary me-2">Y
|
||||
accéder</a>
|
||||
<a href="#" class="btn btn-secondary">Gérer l'application</a>
|
||||
</div>
|
||||
<p class="card-text">{{ application.entity.descriptionSmall|raw }}</p>
|
||||
|
||||
{% if application.hasAccess %}
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<form method="POST"
|
||||
action="{{ path('application_remove', {'id': application.entity.id}) }}"
|
||||
data-controller="application"
|
||||
data-application-application-value="{{ application.entity.name }}"
|
||||
data-application-organization-value="{{ organization.name|capitalize }}"
|
||||
data-action="submit->application#handleRemoveSubmit"
|
||||
style="display: inline-block;">
|
||||
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
||||
<button class="btn btn-secondary" type="submit" data-application-target="submitBtn">
|
||||
Retirer l'accès
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div>
|
||||
<a href="http://{{ application.entity.subDomain }}.solutions-easy.moi"
|
||||
class="btn btn-primary me-2">Y accéder</a>
|
||||
{# TODO: regarder comment gérer l'attribution de droit d'accès au utilisateur à une appli client pour pune organization #}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-primary">Demander l'accès</a>
|
||||
{#TODO: page d'accès#}
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<form method="POST"
|
||||
action="{{ path('application_authorize', {'id': application.entity.id}) }}"
|
||||
data-controller="application"
|
||||
data-application-application-value="{{ application.entity.name }}"
|
||||
data-application-organization-value="{{ organization.name|capitalize }}"
|
||||
data-action="submit->application#handleAuthorizeSubmit"
|
||||
style="display: inline-block;">
|
||||
<input type="hidden" name="organizationId" value="{{ organization.id }}">
|
||||
<button class="btn btn-secondary" type="submit" data-application-target="submitBtn">
|
||||
Autoriser l'accès
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-primary">Demander l'accès</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card">
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
<h2>Modifier l'application</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<form method="post" action="{{ path('application_edit', {'id': apps.id}) }}" data-controller="application">
|
||||
<input name="name" type="text" value="{{ apps.name }}" class="form-control mb-3" required>
|
||||
|
||||
{# Description (full) #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div id="toolbar-description" class="border-0">
|
||||
<button class="ql-bold"></button>
|
||||
<button class="ql-italic"></button>
|
||||
<button class="ql-underline"></button>
|
||||
<button class="ql-strike"></button>
|
||||
<button class="ql-blockquote"></button>
|
||||
<button class="ql-code-block"></button>
|
||||
<select class="ql-header">
|
||||
<option value="1"></option>
|
||||
<option value="2"></option>
|
||||
<option selected></option>
|
||||
</select>
|
||||
<select class="ql-size">
|
||||
<option value="small"></option>
|
||||
<option selected></option>
|
||||
<option value="large"></option>
|
||||
<option value="huge"></option>
|
||||
</select>
|
||||
<select class="ql-color"></select>
|
||||
<select class="ql-background"></select>
|
||||
<select class="ql-align"></select>
|
||||
<button class="ql-link"></button>
|
||||
<button class="ql-image"></button>
|
||||
<button class="ql-clean"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="editor-description" class="form-control border-0" style="min-height: 200px;">
|
||||
{{ apps.description|raw }}
|
||||
</div>
|
||||
<textarea name="description" data-application-target="hidden" class="d-none">{{ apps.description|raw }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{# Description Small #}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<div id="toolbar-descriptionSmall" class="border-0">
|
||||
<button class="ql-bold"></button>
|
||||
<button class="ql-italic"></button>
|
||||
<button class="ql-underline"></button>
|
||||
<select class="ql-header">
|
||||
<option value="2"></option>
|
||||
<option selected></option>
|
||||
</select>
|
||||
<button class="ql-clean"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="editor-descriptionSmall" class="form-control border-0" style="min-height: 120px;">
|
||||
{{ apps.descriptionSmall|raw }}
|
||||
</div>
|
||||
<textarea name="descriptionSmall" data-application-target="hidden" class="d-none">{{ apps.descriptionSmall|raw }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<input type="file" name="logo" class="form-control mb-3 mt-3">
|
||||
<button type="submit" class="btn btn-primary mt-3">Enregistrer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -5,19 +5,21 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="row m-5">
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">Bienvenue sur la suite Easy</h1>
|
||||
<p class="lead">Ici, vous pouvez trouver toutes nos applications à un seul endroit !</p>
|
||||
</div>
|
||||
|
||||
{% for application in applications %}
|
||||
<div class="col-6 mb-3">
|
||||
{% include 'application/InformationCard.html.twig' with {
|
||||
application: application
|
||||
} %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="row m-5">
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">Bienvenue sur la suite Easy</h1>
|
||||
<p class="lead">Ici, vous pouvez trouver toutes nos applications à un seul endroit !</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for application in applications %}
|
||||
<div class="col-6 mb-3">
|
||||
{% include 'application/InformationCard.html.twig' with {
|
||||
application: application
|
||||
} %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -13,10 +13,12 @@
|
|||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% block importmap %}{{ importmap('app') }}{% endblock %}
|
||||
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body data-application="{{application}}">
|
||||
|
|
|
|||
|
|
@ -21,21 +21,21 @@
|
|||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav w-auto m-auto">
|
||||
<li class="nav-item nav-search d-none d-lg-block">
|
||||
<div class="input-group">
|
||||
<div id="navbar-search-icon" class="input-group-prepend hover-cursor">
|
||||
<span id="search">
|
||||
<i style="width:22px; height:22px">{{ ux_icon('ix:project-arrow-diagonal-top-right', {height: '22px', width: '22px'}) }}</i>
|
||||
</span>
|
||||
</div>
|
||||
<select id="change-project" class="form-control">
|
||||
<option>Projet 1</option>
|
||||
<option>Projet 2</option>
|
||||
</select>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{# <ul class="navbar-nav w-auto m-auto">#}
|
||||
{# <li class="nav-item nav-search d-none d-lg-block">#}
|
||||
{# <div class="input-group">#}
|
||||
{# <div id="navbar-search-icon" class="input-group-prepend hover-cursor">#}
|
||||
{# <span id="search">#}
|
||||
{# <i style="width:22px; height:22px">{{ ux_icon('ix:project-arrow-diagonal-top-right', {height: '22px', width: '22px'}) }}</i>#}
|
||||
{# </span>#}
|
||||
{# </div>#}
|
||||
{# <select id="change-project" class="form-control">#}
|
||||
{# <option>Projet 1</option>#}
|
||||
{# <option>Projet 2</option>#}
|
||||
{# </select>#}
|
||||
{# </div>#}
|
||||
{# </li>#}
|
||||
{# </ul>#}
|
||||
<ul class="navbar-nav navbar-nav-right">
|
||||
<li class="nav-item d-flex">
|
||||
<img id="logo_orga" class="m-auto" src="{{asset('logo_org/logo-sudalys.png')}}" alt="logo organisation">
|
||||
|
|
@ -64,7 +64,12 @@
|
|||
<a id="profileDropdown" class="nav-link count-indicator dropdown-toggle m-auto" href="#" data-bs-toggle="dropdown">
|
||||
<div id="profil" class="rounded-circle bg-secondary d-flex">
|
||||
{% if app.user %}
|
||||
<p class="text-light m-auto">{{ app.user.email|first|capitalize }}</p>
|
||||
{# {% if app.user.pictureUrl is defined %}#}
|
||||
{# <img src="{{ aws_url ~ app.user.pictureUrl }}" alt="User profile pic"#}
|
||||
{# class="rounded-circle" style="width:40px; height:40px;">#}
|
||||
{# {% else %}#}
|
||||
<p class="text-light m-auto">{{ app.user.email|first|capitalize }}</p>
|
||||
{# {% endif %}#}
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
|
|
@ -88,7 +93,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<a class="dropdown-item" style="padding-left: 8px;" href="{{ path('sso_logout') }}">
|
||||
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
|
||||
<i class="me-2">{{ ux_icon('material-symbols:logout', {height: '20px', width: '20px'}) }}</i>
|
||||
Deconnexion
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<p>Aucune activité récente.</p>
|
||||
{% else %}
|
||||
{% set sortedActivities = activities|sort((a, b) => a.date <=> b.date)|reverse %}
|
||||
<ul class="list-group">
|
||||
<ul class="list-group gap-2">
|
||||
{% for activity in sortedActivities%}
|
||||
{% include 'user/organization/userActivity.html.twig' with {
|
||||
activityTime: activity.date,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class=" col-md-10 m-auto p-5">
|
||||
<div class="card">
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card p-3 m-3">
|
||||
<div class="card-header border-0">
|
||||
<h2>Modifier l'organisation</h2>
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
{# <a href="{{ path('organization_delete', {'id': organization.id}) }}" class="btn btn-danger">Supprimer</a>#}
|
||||
|
|
|
|||
|
|
@ -3,60 +3,39 @@
|
|||
{% block title %} Gestion des organisations {% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="w-100 h-100 p-5 m-auto" data-controller="organization">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1>Gestion des organisations</h1>
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('organization_new') }}" class="btn btn-primary">Ajouter une organisation</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if organizations|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center">Aucune organisation trouvée.</td>
|
||||
<td colspan="4" class="text-center">
|
||||
<a href="{{ path('organization_new') }}" class="btn btn-primary">Créer une organisation</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<table class="table align-middle shadow">
|
||||
<thead class="table-light shadow-sm">
|
||||
<tr>
|
||||
<th>Logo</th>
|
||||
<th>Nom</th>
|
||||
<th>Email</th>
|
||||
<th>Visualiser</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for organization in organizations %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if organization.logoUrl %}
|
||||
<img src="{{ asset(organization.logoUrl) }}" alt="Organization logo"
|
||||
class="rounded-circle" style="width:40px; height:40px;">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ organization.name }}</td>
|
||||
<td>{{ organization.email }}</td>
|
||||
<td>
|
||||
{% if organization.active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('organization_show', {'id': organization.id}) }}"
|
||||
class="p-3 align-middle color-primary">
|
||||
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card p-3 m-3 border-0">
|
||||
<div class="card-header d-flex justify-content-between align-items-center border-0">
|
||||
<div class="card-title">
|
||||
<h1>Gestion des organisations</h1>
|
||||
</div>
|
||||
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('organization_new') }}" class="btn btn-primary">Ajouter une organisation</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body ">
|
||||
{% if organizationsData|length == 0 %}
|
||||
|
||||
{# style présent juste pour créer de l'espace #}
|
||||
<div class="div text-center my-5 py-5">
|
||||
<h1 class="my-5 ty-5"> Aucune organisation trouvée. </h1>
|
||||
<a href="{{ path('organization_new') }}" class="btn btn-primary">Créer une organisation</a>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div id="tabulator-org" data-controller="organization"
|
||||
data-organization-data-value="{{ organizationsData|json_encode(constant("JSON_UNESCAPED_UNICODE"))|e("html_attr") }}"
|
||||
data-organization-aws-value="{{ aws_url }}"></div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -3,11 +3,14 @@
|
|||
{% block title %}Ajouter une organisation{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class=" col-md-10 m-auto p-5">
|
||||
<div class="card">
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
<h1>Ajouter une organisation</h1>
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card p-3 m-3">
|
||||
<div class="card-header border-0">
|
||||
<div class="card-title d-flex justify-content-between align-items-center">
|
||||
<h1>Ajouter une organisation</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ path('organization_new') }}" enctype="multipart/form-data">
|
||||
{{ form_start(form) }}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md-12 m-auto p-5">
|
||||
<div class="col d-flex justify-content-between align-items-center ">
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="col d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex ">
|
||||
{% if organization.logoUrl %}
|
||||
<img src="{{ asset(organization.logoUrl) }}" alt="Organization logo"
|
||||
<img src="{{ aws_url ~ organization.logoUrl }}" alt="Organization logo"
|
||||
class="rounded-circle" style="width:40px; height:40px;">
|
||||
{% endif %}
|
||||
<h1 class="mb-4 ms-3">{{ organization.name|title }} - Dashboard</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<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}) }}"
|
||||
onsubmit="return confirm('Vous allez supprimer cette organisation, êtes vous sûre?');"
|
||||
style="display: inline-block;">
|
||||
<button class="btn btn-danger" type="submit">Supprimer l'organisation</button>
|
||||
<button class="btn btn-secondary" type="submit">Supprimer l'organisation</button>
|
||||
</form>
|
||||
{% if organization.active %}
|
||||
<form method="POST" action="{{ path('organization_deactivate', {'id': organization.id}) }}"
|
||||
|
|
@ -39,35 +39,61 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
{# USER ROW #}
|
||||
{# single row so that activity and users tabs are next to each other#}
|
||||
<div class="row">
|
||||
{# User tables #}
|
||||
<div class="col-9">
|
||||
<div class="row mb-4">
|
||||
<div class="col mb-3 mb-sm-0">
|
||||
{% include 'user/userListSmall.html.twig' with {
|
||||
title: 'Nouveaux utilisateurs',
|
||||
users: newUsers,
|
||||
empty_message: 'Aucun nouveaux utilisateurs trouvé.',
|
||||
organizationId: organization.id
|
||||
} %}
|
||||
<div class="row mb-3 d-flex gap-2 ">
|
||||
<div class="col mb-3 card">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
Nouveaux utilisateurs
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="tabulator-userListSmall" data-controller="user"
|
||||
data-user-aws-value="{{ aws_url }}"
|
||||
data-user-new-value="true"
|
||||
data-user-list-small-value="true"
|
||||
data-user-org-id-value="{{ organization.id }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-3 mb-sm-0">
|
||||
{% include 'user/userListSmall.html.twig' with {
|
||||
title: 'Administrateurs',
|
||||
users: adminUsers,
|
||||
empty_message: 'Aucun administrateur trouvé.'
|
||||
} %}
|
||||
<div class="col mb-3 card">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
Administrateurs
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="tabulator-userListSmallAdmin" data-controller="user"
|
||||
data-user-aws-value="{{ aws_url }}"
|
||||
data-user-admin-value="true"
|
||||
data-user-list-small-value="true"
|
||||
data-user-org-id-value="{{ organization.id }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# <div class="m-auto"> #}
|
||||
{% include 'user/userList.html.twig' with {
|
||||
title: 'Mes utilisateurs',
|
||||
organizationId: organization.id,
|
||||
empty_message: 'Aucun utilisateurs trouvé.'
|
||||
} %}
|
||||
{# </div> #}
|
||||
<div class="row mb-3 card">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
Mes utilisateurs
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="tabulator-userListOrganization" data-controller="user"
|
||||
data-user-aws-value="{{ aws_url }}"
|
||||
data-user-statut-value="true"
|
||||
data-user-list-organization-value="true"
|
||||
data-user-org-id-value="{{ organization.id }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# APPLICATION ROW #}
|
||||
<div class="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 {
|
||||
|
|
@ -77,7 +103,7 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Activities col#}
|
||||
<div class="col-3 m-auto">
|
||||
{% include 'organization/activity.html.twig' with {
|
||||
title: 'Activités récentes',
|
||||
|
|
@ -86,9 +112,12 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{# Ne pas enlever le 2ème /div#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
{% block body %}
|
||||
{% set roles = uoa.roles %}
|
||||
|
||||
<div class="card col-6 mb-4 me-4">
|
||||
<div class="card-header">
|
||||
<div class="d-flex">
|
||||
{% if uoa.application.logoUrl %}
|
||||
<img src="{{ asset(uoa.application.logoUrl) }}" alt="Logo {{ uoa.application.name }}"
|
||||
class="rounded-circle me-2" style="width:40px; height:40px;">
|
||||
{% endif %}
|
||||
<h1 class="mb-0">{{ uoa.application.name|title }}</h1>
|
||||
{# TODO: compare style with/without border#}
|
||||
<div class="card col-6">
|
||||
<div class="card-header d-flex gap-2">
|
||||
{% if uoa.application.logoUrl %}
|
||||
<img src="{{ asset(uoa.application.logoUrl) }}" alt="Logo {{ uoa.application.name }}"
|
||||
class="rounded-circle " style="width:50px; height:50px;">
|
||||
{% endif %}
|
||||
<div class="card-title">
|
||||
<h1>{{ uoa.application.name|title }}</h1>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<p><b> Description : </b>{{ uoa.application.description|default('Aucune description disponible.') }}</p>
|
||||
{# TODO: pb avec le |raw retour à la ligne#}
|
||||
{# maybe remove Description label #}
|
||||
<p><b> Description : </b>{{ uoa.application.descriptionSmall|default('Aucune description disponible.')|raw }}</p>
|
||||
</div>
|
||||
{% if is_granted('ROLE_ADMIN') %}
|
||||
{# TODO: Can be turn into an ajax function#}
|
||||
<form method="POST"
|
||||
action="{{ path('user_application_role', { id : uoa.uoId }) }}"
|
||||
onsubmit="return confirm('Attention, si le role utilisateur ' +
|
||||
'n\'est pas attribué, l\'utilisateur ne pourra plus accéder à l\'application. Êtes-vous sûr ?');"
|
||||
data-controller="user"
|
||||
data-user-roles-array-value="{{ uoa.rolesArray|json_encode }}"
|
||||
data-user-selected-role-ids-value="{{ uoa.selectedRoleIds|json_encode }}">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<div class=" col-md-10 m-auto p-5">
|
||||
<div class="card">
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
<h2>Modifier l'utilisateur</h2>
|
||||
{# <a href="{{ path('user_delete', {'id': user.id}) }}" class="btn btn-danger">Supprimer</a>#}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card p-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="card-title">
|
||||
<h2>Modifier l'utilisateur</h2>
|
||||
</div>
|
||||
<a href="{{ path('user_delete', {'id': user.id}) }}" class="btn btn-danger m-3">Supprimer</a>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
{{ form_start(form, {'action': path('user_edit', {'id': user.id}), 'method': 'PUT'}) }}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@
|
|||
{% block title %}User Profile{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="w-100 h-100 p-5 m-auto" data-controller="user">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
{# TODO: check for border-0#}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h1>Gestion Utilisateurs</h1>
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<a href="{{ path('user_new') }}" class="btn btn-primary">Ajouter un utilisateur</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{#TODO: Remove/Adapt userList depending on design review #}
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') or is_granted('ROLE_ADMIN') %}
|
||||
{% if usersByOrganization|length == 0 %}
|
||||
<div class="alert alert-info">
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
{% include 'user/userList.html.twig' with {
|
||||
title: org.name,
|
||||
organizationId: org.id|default(null),
|
||||
logo: org.logo|default(null),
|
||||
users: org.users
|
||||
} %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}User Profile{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card p-3 m-3 border-0">
|
||||
<div class="card-header border-0">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 ">
|
||||
<h1>Gestion Utilisateurs</h1>
|
||||
<a href="{{ path('user_new') }}" class="btn btn-primary">Ajouter un utilisateur</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if users|length == 0 %}
|
||||
<div class="alert alert-info">
|
||||
<h4>Aucun utilisateur trouvé</h4>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border-0">
|
||||
<div class="card-body">
|
||||
<div id="tabulator-userList" data-controller="user"
|
||||
data-user-aws-value="{{ aws_url }}"
|
||||
data-user-list-value="true">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="alert alert-warning">
|
||||
<h4>Accès limité</h4>
|
||||
<p>Vous n'avez pas les permissions nécessaires pour voir la liste des utilisateurs.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
@ -3,19 +3,21 @@
|
|||
{% block title %}Ajouter un utilisateur{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class=" col-md-10 m-auto p-5">
|
||||
<div class="card">
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
<h1>Ajouter un utilisateur</h1>
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card p-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="card-title">
|
||||
<h1>Ajouter un utilisateur</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{{ path('user_new') }}" enctype="multipart/form-data">
|
||||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
{% if organizationId is defined %}
|
||||
<div class="form-group">
|
||||
<input hidden type="text" value="{{ organizationId }}" name="organizationId">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input hidden type="text" value="{{ organizationId }}" name="organizationId">
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
{{ form_end(form) }}
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
|
||||
|
||||
{% block body %}
|
||||
|
||||
<div class="col-md-10 m-auto p-5">
|
||||
<div class="card">
|
||||
{% for uo in userOrganization %}
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
<h3>
|
||||
Modification de : <b>{{ user.name|capitalize }} {{ user.surname|capitalize }} </b> chez
|
||||
<b> {{ uo.organization.name }}</b>
|
||||
</h3>
|
||||
<a href="{{ path('user_organization_deactivate', {'id': uo.uoId}) }}" class="btn btn-danger">Supprimer</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ path('user_organization_edit', {'id' : uo.uoId}) }}"
|
||||
data-controller="user"
|
||||
data-user-roles-array-value="{{ rolesArray|json_encode }}"
|
||||
data-user-selected-role-ids-value="{{ selectedRoleIds|json_encode }}"
|
||||
data-user-applications-array-value="{{ appsArray|json_encode }}"
|
||||
data-user-selected-application-ids-value="{{ selectedAppIds|json_encode }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-group mb-3">
|
||||
<label for="roles">Roles</label>
|
||||
<select class="choices" data-type="select-multiple" id="roles" name="roles[]" multiple>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="applications">Applications</label>
|
||||
<select class="choices" data-type="select-multiple" id="applications" name="applications[]"
|
||||
multiple>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# bouton d'envoie du formulaire#}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Modifier</button>
|
||||
{# <a href="{{ path('user_organization_list') }}" class="btn btn-secondary">Annuler</a>#}
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
{% block body %}
|
||||
|
||||
|
||||
<div class="card col-4 mt-3 me-3" >
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
{# Affichage du nom de l'organisation et de l'icône de flèche #}
|
||||
<div class="d-flex user-org-card"
|
||||
style="cursor:pointer;" data-bs-toggle="collapse"
|
||||
data-bs-target="#org-details-{{ organization.id }}" aria-expanded="false"
|
||||
aria-controls="org-details-{{ organization.id }}">
|
||||
<h2 class=" pe-2">{{ organization.name|capitalize }}</h2>
|
||||
<i class="pt-2" id="arrow-icon-{{ organization.id }}">
|
||||
{{ ux_icon('fa6-regular:circle-down', {height: '25px', width: '25px'}) }}
|
||||
</i>
|
||||
</div>
|
||||
{% if is_granted("ROLE_ADMIN") %}
|
||||
<a href="{{ path('user_organization_edit', {'id': uoId}) }}" class="btn btn-primary" >Modifier</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Information principale sur l'utilisateur dans l'organisation#}
|
||||
<div class="card-body">
|
||||
{# Affichage du plus haut role #}
|
||||
<p><b>Role:</b>
|
||||
{% if roles|length > 0 %}
|
||||
{% set firstRole = roles[0] %}
|
||||
{% if firstRole.name == "SUPER ADMIN" %}
|
||||
<span class="badge bg-danger">Super Administrateur</span>
|
||||
{% elseif firstRole.name == "ADMIN" %}
|
||||
<span class="badge bg-danger">Administrateur</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ firstRole.name|capitalize }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Aucun rôle
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{# Affichage des applications dont l'utilisateur à accès #}
|
||||
<div class="d-flex">
|
||||
{% if apps is not empty %}
|
||||
{% for app in apps %}
|
||||
<img src="{{ asset(app.logoUrl) }}" alt="Logo {{ app.name }}" class="img-fluid ms-2" style="height: 30px; width: 30px;">
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
Aucune application associée.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Détails supplémentaires sur l'organisation #}
|
||||
<div class="collapse card-body border-top" id="org-details-{{ organization.id }}">
|
||||
{% if roles|length > 1 %}
|
||||
<p><b>Autres rôles:</b>
|
||||
{% for role in roles|slice(1) %}
|
||||
{% if role.name == "SUPER ADMIN"%}
|
||||
<span class="badge bg-danger">Super Administrateur</span>
|
||||
{% elseif role.name == "ADMIN" %}
|
||||
<span class="badge bg-danger">Administrateur</span>
|
||||
{% else %}
|
||||
<span class="badge bg-primary">{{ role.name|capitalize }}</span>
|
||||
{% endif %}
|
||||
{% if not loop.last %} - {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p><b>Membre depuis:</b> {{ organization.createdAt|date('d/m/Y') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block javascript %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var collapseEl = document.getElementById('org-details-{{ organization.id }}');
|
||||
var arrowEl = document.getElementById('arrow-icon-{{ organization.id }}');
|
||||
if (collapseEl && arrowEl) {
|
||||
collapseEl.addEventListener('show.bs.collapse', function () {
|
||||
arrowEl.innerHTML = `{{ ux_icon('fa6-regular:circle-up', {height: '25px', width: '25px'})|e('js') }}`;
|
||||
});
|
||||
collapseEl.addEventListener('hide.bs.collapse', function () {
|
||||
arrowEl.innerHTML = `{{ ux_icon('fa6-regular:circle-down', {height: '25px', width: '25px'})|e('js') }}`;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
@ -2,45 +2,61 @@
|
|||
|
||||
{% block body %}
|
||||
|
||||
<div class="col-md-10 m-auto p-5">
|
||||
<div class="w-100 h-100 p-5 m-auto">
|
||||
<div class="card p-3 m-3 border-0">
|
||||
|
||||
|
||||
{% if is_granted("ROLE_ADMIN") %}
|
||||
<div class="col d-flex justify-content-between align-items-center ">
|
||||
<h1 class="mb-4">Gestion Utilisateur</h1>
|
||||
<div>
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('user_delete', {'id': user.id}) }}" class="btn btn-danger">Supprimer</a>
|
||||
|
||||
{% if user.active %}
|
||||
<a href="{{ path('user_deactivate', {'id': user.id}) }}"
|
||||
class="btn btn-danger">Désactiver l'utilisateur</a>
|
||||
{% else %}
|
||||
<a href="{{ path('user_activate', {'id': user.id}) }}" class="btn btn-success">Activer l'utilisateur</a>
|
||||
{% if is_granted("ROLE_ADMIN") %}
|
||||
<div class="card-header border-0 d-flex justify-content-between align-items-center ">
|
||||
<div class="card-title">
|
||||
<h1>Gestion Utilisateur</h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
{% if is_granted("ROLE_SUPER_ADMIN") %}
|
||||
<a href="{{ path('user_delete', {'id': user.id}) }}"
|
||||
class="btn btn-secondary">Supprimer</a>
|
||||
{% if user.active %}
|
||||
<a href="{{ path('user_deactivate', {'id': user.id}) }}"
|
||||
class="btn btn-secondary">Désactiver l'utilisateur</a>
|
||||
{% else %}
|
||||
<a href="{{ path('user_activate', {'id': user.id}) }}" class="btn btn-primary ">Activer
|
||||
l'utilisateur</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
{% include 'user/userInformation.html.twig' %}
|
||||
|
||||
|
||||
<div class="card border-0 no-header-bg ">
|
||||
<div class="card-header">
|
||||
{% if orgs|length >0 %}
|
||||
<div class="card-title">
|
||||
<h1>Vos applications</h1>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-title">
|
||||
<h1>Aucune application</h1>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 card-body">
|
||||
{% for uoa in uoas %}
|
||||
{% include 'user/application/information.html.twig' %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'user/userInformation.html.twig' %}
|
||||
{% if orgs|length >0 %}
|
||||
|
||||
<h1 class="mt-5 mb-4">Vos applications</h1>
|
||||
|
||||
{% else %}
|
||||
<h1 class="mt-5 mb-4">Aucune application</h1>
|
||||
{% endif %}
|
||||
<div class="d-flex ">
|
||||
{% for uoa in uoas %}
|
||||
{% include 'user/application/information.html.twig' %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
|
|
|
|||
|
|
@ -1,39 +1,48 @@
|
|||
{% block body %}
|
||||
|
||||
<div class="card border-0">
|
||||
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex">
|
||||
<img src="{{ asset(user.pictureUrl) }}" alt="user" class="me-3 rounded-circle"
|
||||
style="width: 50px; height: 50px;">
|
||||
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
{% if organizationId is not null %}
|
||||
{% if uoActive %}
|
||||
<form method="post" action="{{ path('user_deactivate_organization', {'id': user.id}) }}"
|
||||
onsubmit="return confirm('Vous allez retirer l\'utilisateur de cette organisation, êtes vous sûre?');">
|
||||
<input type="hidden" name="organizationId" value="{{ organizationId }}">
|
||||
<button class="btn btn-danger" type="submit">Désactiver l'utilisateur de l'organisation</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ path('user_activate_organization', {'id': user.id}) }}"
|
||||
onsubmit="return confirm('Vous allez activer cette utilisateur dans votre organisation, êtes vous sûre?');">
|
||||
<input type="hidden" name="organizationId" value="{{ organizationId }}">
|
||||
<button class="btn btn-primary" type="submit">Activer l'utilisateur de l'organisation</button>
|
||||
</form>
|
||||
<div class="card no-header-bg border-0 ">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-2">
|
||||
{% if user.pictureUrl is not empty %}
|
||||
<img src="{{ aws_url ~ user.pictureUrl }}" alt="user" class="rounded-circle"
|
||||
style="width:40px; height:40px;">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{{ path('user_edit', {'id': user.id, 'organizationId': organizationId}) }}" class="btn btn-primary">Modifier</a>
|
||||
<div class="card-title ">
|
||||
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
{% if organizationId is not null %}
|
||||
{% if uoActive %}
|
||||
<form method="post" action="{{ path('user_deactivate_organization', {'id': user.id}) }}"
|
||||
onsubmit="return confirm('Vous allez retirer l\'utilisateur de cette organisation, êtes vous sûre?');">
|
||||
<input type="hidden" name="organizationId" value="{{ organizationId }}">
|
||||
<button class="btn btn-secondary" type="submit">Désactiver l'utilisateur de l'organisation
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="post" action="{{ path('user_activate_organization', {'id': user.id}) }}"
|
||||
onsubmit="return confirm('Vous allez activer cette utilisateur dans votre organisation, êtes vous sûre?');">
|
||||
<input type="hidden" name="organizationId" value="{{ organizationId }}">
|
||||
<button class="btn btn-primary" type="submit">Activer l'utilisateur de l'organisation
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{{ path('user_edit', {'id': user.id, 'organizationId': organizationId}) }}"
|
||||
class="btn btn-primary">Modifier</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body ">
|
||||
<p><b>Email: </b>{{ user.email }}</p>
|
||||
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
|
||||
à {{ user.lastConnection|date('H:m:s') }} </p>
|
||||
<p><b>Compte crée le: </b>{{ user.createdAt|date('d/m/Y') }}</p>
|
||||
<p><b>Numéro de téléphone: </b>{{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><b>Email: </b>{{ user.email }}</p>
|
||||
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
|
||||
à {{ user.lastConnection|date('H:m:s') }} </p>
|
||||
<p><b>Compte crée le: </b>{{ user.createdAt|date('d/m/Y') }}</p>
|
||||
<p><b>Numéro de téléphone: </b>{{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,33 @@
|
|||
{% block body %}
|
||||
|
||||
<div class="card border-0 p-3 mb-4">
|
||||
<div class="card p-3 mb-3">
|
||||
|
||||
{% if title is defined %}
|
||||
<div class="card-title d-flex justify-content-between align-items-center ">
|
||||
<h3>{{ title }}</h3>
|
||||
{% if organizationId %}
|
||||
<div class="card-header">
|
||||
<div class="card-title d-flex align-items-center ">
|
||||
{% if logo %}
|
||||
<img src="{{ aws_url ~ logo }}" alt="Organization Logo" style="height: 50px; width: 50px;" class="rounded-circle">
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<h3 class="ms-3">{{ title|capitalize }}</h3>
|
||||
{% if organizationId %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
|
||||
<table class="table align-middle ">
|
||||
<thead class="table-light">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Picture</th>
|
||||
<th>Surname</th>
|
||||
<th>Name</th>
|
||||
<th>Profile</th>
|
||||
<th>Nom</th>
|
||||
<th>Prénom</th>
|
||||
<th>Email</th>
|
||||
<th>Statut</th>
|
||||
<th>Visualiser</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -33,7 +40,7 @@
|
|||
<tr>
|
||||
<td>
|
||||
{% if user.entity.pictureUrl %}
|
||||
<img src="{{ asset(user.entity.pictureUrl) }}" alt="User profile pic"
|
||||
<img src="{{ aws_url ~ user.entity.pictureUrl }}" alt="User profile pic"
|
||||
class="rounded-circle"
|
||||
style="width:40px; height:40px;">
|
||||
{% else %}
|
||||
|
|
@ -57,7 +64,7 @@
|
|||
<span class="badge bg-secondary">Inactif</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Désactivé</span>
|
||||
<span class="badge bg-secondary">Désactivé</span>
|
||||
{% endif %}
|
||||
{# if no organization link #}
|
||||
{% else %}
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
{% block body %}
|
||||
|
||||
<div class="card border-0">
|
||||
<div class="card-title p-3 d-flex justify-content-between align-items-center ">
|
||||
<h3>{{ title }}</h3>
|
||||
{% if organizationId is defined %}
|
||||
<a href="{{ path('user_new', {'organizationId': organizationId}) }}" class="btn btn-primary">Ajouter un utilisateur</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table align-middle table-borderless">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Picture</th>
|
||||
<th>Email</th>
|
||||
<th>Visualiser</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if users|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center">{{ empty_message }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if user.pictureUrl %}
|
||||
<img src="{{ asset(user.pictureUrl) }}" alt="User profile pic"
|
||||
class="rounded-circle" style="width:40px; height:40px;">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
{% if organizationId is defined %}
|
||||
<a href="{{ path('user_show', {'id': user.id, 'organizationId': organizationId}) }}"
|
||||
class="p-3 align-middle color-primary">
|
||||
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ path('user_show', {'id': user.id}) }}"
|
||||
class="p-3 align-middle color-primary">
|
||||
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue