Merge branch 'dev/organization/picture-feature-1' into 'develop'
Dev/organization/picture feature 1 See merge request easy-solutions/apps/easyportal!9
This commit is contained in:
commit
85e3de647c
|
|
@ -72,7 +72,7 @@ export default class extends Controller {
|
||||||
formatterParams: {
|
formatterParams: {
|
||||||
height: "50px",
|
height: "50px",
|
||||||
width: "50px",
|
width: "50px",
|
||||||
urlPrefix: "",
|
urlPrefix: "/",
|
||||||
urlSuffix: "",
|
urlSuffix: "",
|
||||||
},
|
},
|
||||||
width: 100,
|
width: 100,
|
||||||
|
|
|
||||||
|
|
@ -88,24 +88,27 @@ export default class extends Controller {
|
||||||
headerSort: false,
|
headerSort: false,
|
||||||
formatter: (cell) => {
|
formatter: (cell) => {
|
||||||
const data = cell.getRow().getData();
|
const data = cell.getRow().getData();
|
||||||
const url = cell.getValue();
|
let url = cell.getValue(); // use 'let' so we can modify it
|
||||||
|
|
||||||
|
// 1. GENERATE INITIALS
|
||||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
const initials = `${first(data.name)}${first(data.prenom)}`;
|
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||||
|
|
||||||
// wrapper is for centering and circle clipping
|
// 2. CREATE WRAPPER
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = "avatar-wrapper";
|
wrapper.className = "avatar-wrapper";
|
||||||
// same size for both cases
|
|
||||||
wrapper.style.width = "40px";
|
wrapper.style.width = "40px";
|
||||||
wrapper.style.height = "40px";
|
wrapper.style.height = "40px";
|
||||||
wrapper.style.display = "flex";
|
wrapper.style.display = "flex";
|
||||||
wrapper.style.alignItems = "center";
|
wrapper.style.alignItems = "center";
|
||||||
wrapper.style.justifyContent = "center";
|
wrapper.style.justifyContent = "center";
|
||||||
wrapper.style.borderRadius = "50%";
|
wrapper.style.borderRadius = "50%";
|
||||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
wrapper.style.overflow = "hidden";
|
||||||
|
|
||||||
if (!url) {
|
// Helper to render fallback initials
|
||||||
wrapper.style.background = "#6c757d"; // gray background
|
const renderFallback = () => {
|
||||||
|
wrapper.innerHTML = ""; // clear any broken img
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.className = "avatar-initials";
|
span.className = "avatar-initials";
|
||||||
span.style.color = "#fff";
|
span.style.color = "#fff";
|
||||||
|
|
@ -113,31 +116,36 @@ export default class extends Controller {
|
||||||
span.style.fontSize = "14px";
|
span.style.fontSize = "14px";
|
||||||
span.textContent = initials || "•";
|
span.textContent = initials || "•";
|
||||||
wrapper.appendChild(span);
|
wrapper.appendChild(span);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. IF NO URL, RENDER FALLBACK IMMEDIATELY
|
||||||
|
if (!url) {
|
||||||
|
renderFallback();
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image case: make it fill the same wrapper
|
// --- THE FIX: HANDLE RELATIVE PATHS ---
|
||||||
|
// If the path doesn't start with 'http' or '/', add a leading '/'
|
||||||
|
// This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg'
|
||||||
|
if (!url.startsWith('http') && !url.startsWith('/')) {
|
||||||
|
url = '/' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. CREATE IMAGE
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.alt = initials || "avatar";
|
img.alt = initials || "avatar";
|
||||||
img.style.width = "100%";
|
img.style.width = "100%";
|
||||||
img.style.height = "100%";
|
img.style.height = "100%";
|
||||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
img.style.objectFit = "cover";
|
||||||
|
|
||||||
|
// 5. ERROR HANDLING (triggers your fallback)
|
||||||
|
img.onerror = () => {
|
||||||
|
console.warn("Image failed to load:", url); // Debug log to see the wrong path
|
||||||
|
renderFallback();
|
||||||
|
};
|
||||||
|
|
||||||
wrapper.appendChild(img);
|
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;
|
return wrapper;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -305,23 +313,27 @@ export default class extends Controller {
|
||||||
headerSort: false,
|
headerSort: false,
|
||||||
formatter: (cell) => {
|
formatter: (cell) => {
|
||||||
const data = cell.getRow().getData();
|
const data = cell.getRow().getData();
|
||||||
const url = cell.getValue();
|
let url = cell.getValue(); // use 'let' so we can modify it
|
||||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
|
||||||
const initials = `${data.initials}`;
|
|
||||||
|
|
||||||
|
// 1. GENERATE INITIALS
|
||||||
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
|
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||||
|
|
||||||
|
// 2. CREATE WRAPPER
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = "avatar-wrapper";
|
wrapper.className = "avatar-wrapper";
|
||||||
// same size for both cases
|
|
||||||
wrapper.style.width = "40px";
|
wrapper.style.width = "40px";
|
||||||
wrapper.style.height = "40px";
|
wrapper.style.height = "40px";
|
||||||
wrapper.style.display = "flex";
|
wrapper.style.display = "flex";
|
||||||
wrapper.style.alignItems = "center";
|
wrapper.style.alignItems = "center";
|
||||||
wrapper.style.justifyContent = "center";
|
wrapper.style.justifyContent = "center";
|
||||||
wrapper.style.borderRadius = "50%";
|
wrapper.style.borderRadius = "50%";
|
||||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
wrapper.style.overflow = "hidden";
|
||||||
|
|
||||||
if (!url) {
|
// Helper to render fallback initials
|
||||||
wrapper.style.background = "#6c757d"; // gray background
|
const renderFallback = () => {
|
||||||
|
wrapper.innerHTML = ""; // clear any broken img
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.className = "avatar-initials";
|
span.className = "avatar-initials";
|
||||||
span.style.color = "#fff";
|
span.style.color = "#fff";
|
||||||
|
|
@ -329,31 +341,36 @@ export default class extends Controller {
|
||||||
span.style.fontSize = "14px";
|
span.style.fontSize = "14px";
|
||||||
span.textContent = initials || "•";
|
span.textContent = initials || "•";
|
||||||
wrapper.appendChild(span);
|
wrapper.appendChild(span);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. IF NO URL, RENDER FALLBACK IMMEDIATELY
|
||||||
|
if (!url) {
|
||||||
|
renderFallback();
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image case: make it fill the same wrapper
|
// --- THE FIX: HANDLE RELATIVE PATHS ---
|
||||||
|
// If the path doesn't start with 'http' or '/', add a leading '/'
|
||||||
|
// This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg'
|
||||||
|
if (!url.startsWith('http') && !url.startsWith('/')) {
|
||||||
|
url = '/' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. CREATE IMAGE
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.alt = initials || "avatar";
|
img.alt = initials || "avatar";
|
||||||
img.style.width = "100%";
|
img.style.width = "100%";
|
||||||
img.style.height = "100%";
|
img.style.height = "100%";
|
||||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
img.style.objectFit = "cover";
|
||||||
|
|
||||||
|
// 5. ERROR HANDLING (triggers your fallback)
|
||||||
|
img.onerror = () => {
|
||||||
|
console.warn("Image failed to load:", url); // Debug log to see the wrong path
|
||||||
|
renderFallback();
|
||||||
|
};
|
||||||
|
|
||||||
wrapper.appendChild(img);
|
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;
|
return wrapper;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -411,23 +428,27 @@ export default class extends Controller {
|
||||||
headerSort: false,
|
headerSort: false,
|
||||||
formatter: (cell) => {
|
formatter: (cell) => {
|
||||||
const data = cell.getRow().getData();
|
const data = cell.getRow().getData();
|
||||||
const url = cell.getValue();
|
let url = cell.getValue(); // use 'let' so we can modify it
|
||||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
|
||||||
const initials = `${data.initials}`;
|
|
||||||
|
|
||||||
|
// 1. GENERATE INITIALS
|
||||||
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
|
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||||
|
|
||||||
|
// 2. CREATE WRAPPER
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = "avatar-wrapper";
|
wrapper.className = "avatar-wrapper";
|
||||||
// same size for both cases
|
|
||||||
wrapper.style.width = "40px";
|
wrapper.style.width = "40px";
|
||||||
wrapper.style.height = "40px";
|
wrapper.style.height = "40px";
|
||||||
wrapper.style.display = "flex";
|
wrapper.style.display = "flex";
|
||||||
wrapper.style.alignItems = "center";
|
wrapper.style.alignItems = "center";
|
||||||
wrapper.style.justifyContent = "center";
|
wrapper.style.justifyContent = "center";
|
||||||
wrapper.style.borderRadius = "50%";
|
wrapper.style.borderRadius = "50%";
|
||||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
wrapper.style.overflow = "hidden";
|
||||||
|
|
||||||
if (!url) {
|
// Helper to render fallback initials
|
||||||
wrapper.style.background = "#6c757d"; // gray background
|
const renderFallback = () => {
|
||||||
|
wrapper.innerHTML = ""; // clear any broken img
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.className = "avatar-initials";
|
span.className = "avatar-initials";
|
||||||
span.style.color = "#fff";
|
span.style.color = "#fff";
|
||||||
|
|
@ -435,31 +456,36 @@ export default class extends Controller {
|
||||||
span.style.fontSize = "14px";
|
span.style.fontSize = "14px";
|
||||||
span.textContent = initials || "•";
|
span.textContent = initials || "•";
|
||||||
wrapper.appendChild(span);
|
wrapper.appendChild(span);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. IF NO URL, RENDER FALLBACK IMMEDIATELY
|
||||||
|
if (!url) {
|
||||||
|
renderFallback();
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image case: make it fill the same wrapper
|
// --- THE FIX: HANDLE RELATIVE PATHS ---
|
||||||
|
// If the path doesn't start with 'http' or '/', add a leading '/'
|
||||||
|
// This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg'
|
||||||
|
if (!url.startsWith('http') && !url.startsWith('/')) {
|
||||||
|
url = '/' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. CREATE IMAGE
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.alt = initials || "avatar";
|
img.alt = initials || "avatar";
|
||||||
img.style.width = "100%";
|
img.style.width = "100%";
|
||||||
img.style.height = "100%";
|
img.style.height = "100%";
|
||||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
img.style.objectFit = "cover";
|
||||||
|
|
||||||
|
// 5. ERROR HANDLING (triggers your fallback)
|
||||||
|
img.onerror = () => {
|
||||||
|
console.warn("Image failed to load:", url); // Debug log to see the wrong path
|
||||||
|
renderFallback();
|
||||||
|
};
|
||||||
|
|
||||||
wrapper.appendChild(img);
|
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;
|
return wrapper;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -539,23 +565,27 @@ export default class extends Controller {
|
||||||
headerSort: false,
|
headerSort: false,
|
||||||
formatter: (cell) => {
|
formatter: (cell) => {
|
||||||
const data = cell.getRow().getData();
|
const data = cell.getRow().getData();
|
||||||
const url = cell.getValue();
|
let url = cell.getValue(); // use 'let' so we can modify it
|
||||||
|
|
||||||
|
// 1. GENERATE INITIALS
|
||||||
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : "");
|
||||||
const initials = `${first(data.name)}${first(data.prenom)}`;
|
const initials = `${first(data.name)}${first(data.prenom)}`;
|
||||||
|
|
||||||
|
// 2. CREATE WRAPPER
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
wrapper.className = "avatar-wrapper";
|
wrapper.className = "avatar-wrapper";
|
||||||
// same size for both cases
|
|
||||||
wrapper.style.width = "40px";
|
wrapper.style.width = "40px";
|
||||||
wrapper.style.height = "40px";
|
wrapper.style.height = "40px";
|
||||||
wrapper.style.display = "flex";
|
wrapper.style.display = "flex";
|
||||||
wrapper.style.alignItems = "center";
|
wrapper.style.alignItems = "center";
|
||||||
wrapper.style.justifyContent = "center";
|
wrapper.style.justifyContent = "center";
|
||||||
wrapper.style.borderRadius = "50%";
|
wrapper.style.borderRadius = "50%";
|
||||||
wrapper.style.overflow = "hidden"; // ensure image clips to circle
|
wrapper.style.overflow = "hidden";
|
||||||
|
|
||||||
if (!url) {
|
// Helper to render fallback initials
|
||||||
wrapper.style.background = "#6c757d"; // gray background
|
const renderFallback = () => {
|
||||||
|
wrapper.innerHTML = ""; // clear any broken img
|
||||||
|
wrapper.style.background = "#6c757d";
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
span.className = "avatar-initials";
|
span.className = "avatar-initials";
|
||||||
span.style.color = "#fff";
|
span.style.color = "#fff";
|
||||||
|
|
@ -563,31 +593,36 @@ export default class extends Controller {
|
||||||
span.style.fontSize = "14px";
|
span.style.fontSize = "14px";
|
||||||
span.textContent = initials || "•";
|
span.textContent = initials || "•";
|
||||||
wrapper.appendChild(span);
|
wrapper.appendChild(span);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. IF NO URL, RENDER FALLBACK IMMEDIATELY
|
||||||
|
if (!url) {
|
||||||
|
renderFallback();
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image case: make it fill the same wrapper
|
// --- THE FIX: HANDLE RELATIVE PATHS ---
|
||||||
|
// If the path doesn't start with 'http' or '/', add a leading '/'
|
||||||
|
// This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg'
|
||||||
|
if (!url.startsWith('http') && !url.startsWith('/')) {
|
||||||
|
url = '/' + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. CREATE IMAGE
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.alt = initials || "avatar";
|
img.alt = initials || "avatar";
|
||||||
img.style.width = "100%";
|
img.style.width = "100%";
|
||||||
img.style.height = "100%";
|
img.style.height = "100%";
|
||||||
img.style.objectFit = "cover"; // keep aspect and cover circle
|
img.style.objectFit = "cover";
|
||||||
|
|
||||||
|
// 5. ERROR HANDLING (triggers your fallback)
|
||||||
|
img.onerror = () => {
|
||||||
|
console.warn("Image failed to load:", url); // Debug log to see the wrong path
|
||||||
|
renderFallback();
|
||||||
|
};
|
||||||
|
|
||||||
wrapper.appendChild(img);
|
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;
|
return wrapper;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
parameters:
|
parameters:
|
||||||
aws_url: '%env(AWS_ENDPOINT)%'
|
aws_url: '%env(AWS_ENDPOINT)%'
|
||||||
aws_public_url: '%env(AWS_ENDPOINT)%'
|
aws_public_url: '%env(AWS_ENDPOINT)%'
|
||||||
|
aws_bucket: '%env(S3_PORTAL_BUCKET)%'
|
||||||
logos_directory: '%kernel.project_dir%/public/uploads/logos'
|
logos_directory: '%kernel.project_dir%/public/uploads/logos'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,7 @@ class OrganizationController extends AbstractController
|
||||||
try {
|
try {
|
||||||
$organization->setIsActive(false);
|
$organization->setIsActive(false);
|
||||||
$organization->setIsDeleted(true);
|
$organization->setIsDeleted(true);
|
||||||
|
$this->organizationsService->deleteLogo($organization);
|
||||||
// Deactivate all associated UsersOrganizations
|
// Deactivate all associated UsersOrganizations
|
||||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, null, $organization);
|
||||||
|
|
||||||
|
|
@ -344,12 +345,12 @@ class OrganizationController extends AbstractController
|
||||||
|
|
||||||
// Map to array
|
// Map to array
|
||||||
$data = array_map(function (Organizations $org) {
|
$data = array_map(function (Organizations $org) {
|
||||||
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $org->getLogoUrl();
|
|
||||||
return [
|
return [
|
||||||
'id' => $org->getId(),
|
'id' => $org->getId(),
|
||||||
'name' => $org->getName(),
|
'name' => $org->getName(),
|
||||||
'email' => $org->getEmail(),
|
'email' => $org->getEmail(),
|
||||||
'logoUrl' => $picture ?: null,
|
'logoUrl' => $org->getLogoUrl() ?: null,
|
||||||
'active' => $org->isActive(),
|
'active' => $org->isActive(),
|
||||||
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
'showUrl' => $this->generateUrl('organization_show', ['id' => $org->getId()]),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -128,12 +128,15 @@ class UserController extends AbstractController
|
||||||
'isActive' => true,
|
'isActive' => true,
|
||||||
]);
|
]);
|
||||||
if (!$uoList) {
|
if (!$uoList) {
|
||||||
$this->loggerService->logEntityNotFound('UsersOrganization', [
|
$data['rolesArray'] = $this->userService->getRolesArrayForUser($actingUser, true);
|
||||||
'user_id' => $user->getId(),
|
return $this->render('user/show.html.twig', [
|
||||||
'organization_id' => $orgId],
|
'user' => $user,
|
||||||
$actingUser->getId());
|
'organizationId' => $orgId ?? null,
|
||||||
$this->addFlash('error', "L'utilisateur n'est pas actif dans une organisation.");
|
'uoActive' => $uoActive ?? null,
|
||||||
throw $this->createNotFoundException(self::NOT_FOUND);
|
'apps' => $apps ?? [],
|
||||||
|
'data' => $data ?? [],
|
||||||
|
'canEdit' => false,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
|
// Charger les liens UserOrganizationApp (UOA) actifs pour les UO trouvées
|
||||||
|
|
@ -168,7 +171,11 @@ class UserController extends AbstractController
|
||||||
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user,$this->isGranted('ROLE_ADMIN'), $singleUo, $organization);
|
$canEdit = $this->userService->canEditRolesCheck($actingUser, $user,$this->isGranted('ROLE_ADMIN'), $singleUo, $organization);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->errorLogger->error($e->getMessage());
|
$this->loggerService->logError('error while loading user information', [
|
||||||
|
'target_user_id' => $id,
|
||||||
|
'acting_user_id' => $actingUser->getId(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
$this->addFlash('error', 'Une erreur est survenue lors du chargement des informations utilisateur.');
|
$this->addFlash('error', 'Une erreur est survenue lors du chargement des informations utilisateur.');
|
||||||
$referer = $request->headers->get('referer');
|
$referer = $request->headers->get('referer');
|
||||||
return $this->redirect($referer ?? $this->generateUrl('app_index'));
|
return $this->redirect($referer ?? $this->generateUrl('app_index'));
|
||||||
|
|
@ -183,7 +190,7 @@ class UserController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/edit/{id}', name: 'edit', methods: ['GET', 'POST'])]
|
#[Route('/edit/{id}', name: 'edit', methods: ['GET','POST'])]
|
||||||
public function edit(int $id, Request $request): Response
|
public function edit(int $id, Request $request): Response
|
||||||
{
|
{
|
||||||
$this->denyAccessUnlessGranted('ROLE_USER');
|
$this->denyAccessUnlessGranted('ROLE_USER');
|
||||||
|
|
@ -199,13 +206,12 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
$form = $this->createForm(UserForm::class, $user);
|
$form = $this->createForm(UserForm::class, $user);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
|
||||||
// Handle user edit
|
// Handle user edit
|
||||||
$picture = $form->get('pictureUrl')->getData();
|
$picture = $form->get('pictureUrl')->getData();;
|
||||||
$this->userService->formatUserData($user, $picture);
|
$this->userService->formatUserData($user, $picture);
|
||||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
|
|
||||||
$this->entityManager->persist($user);
|
$this->entityManager->persist($user);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
|
@ -571,6 +577,7 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
$user->setIsActive(false);
|
$user->setIsActive(false);
|
||||||
$user->setIsDeleted(true);
|
$user->setIsDeleted(true);
|
||||||
|
$this->userService->deleteProfilePicture($user);
|
||||||
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
$user->setModifiedAt(new \DateTimeImmutable('now'));
|
||||||
// Deactivate all org links
|
// Deactivate all org links
|
||||||
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
$this->userOrganizationService->deactivateAllUserOrganizationLinks($actingUser, $user);
|
||||||
|
|
@ -731,10 +738,9 @@ class UserController extends AbstractController
|
||||||
|
|
||||||
// Map to array
|
// Map to array
|
||||||
$data = array_map(function (User $user) {
|
$data = array_map(function (User $user) {
|
||||||
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
|
|
||||||
return [
|
return [
|
||||||
'id' => $user->getId(),
|
'id' => $user->getId(),
|
||||||
'pictureUrl' => $picture,
|
'pictureUrl' => $user->getPictureUrl(),
|
||||||
'name' => $user->getSurname(),
|
'name' => $user->getSurname(),
|
||||||
'prenom' => $user->getName(),
|
'prenom' => $user->getName(),
|
||||||
'email' => $user->getEmail(),
|
'email' => $user->getEmail(),
|
||||||
|
|
@ -786,10 +792,9 @@ class UserController extends AbstractController
|
||||||
// Map to array (keep isConnected)
|
// Map to array (keep isConnected)
|
||||||
$data = array_map(function (UsersOrganizations $uo) {
|
$data = array_map(function (UsersOrganizations $uo) {
|
||||||
$user = $uo->getUsers();
|
$user = $uo->getUsers();
|
||||||
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
|
|
||||||
$initials = $user->getName()[0] . $user->getSurname()[0];
|
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||||||
return [
|
return [
|
||||||
'pictureUrl' => $picture,
|
'pictureUrl' =>$user->getPictureUrl(),
|
||||||
'email' => $user->getEmail(),
|
'email' => $user->getEmail(),
|
||||||
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||||
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||||
|
|
@ -829,10 +834,9 @@ class UserController extends AbstractController
|
||||||
// Map to array (keep isConnected)
|
// Map to array (keep isConnected)
|
||||||
$data = array_map(function (UsersOrganizations $uo) {
|
$data = array_map(function (UsersOrganizations $uo) {
|
||||||
$user = $uo->getUsers();
|
$user = $uo->getUsers();
|
||||||
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
|
|
||||||
$initials = $user->getName()[0] . $user->getSurname()[0];
|
$initials = $user->getName()[0] . $user->getSurname()[0];
|
||||||
return [
|
return [
|
||||||
'pictureUrl' => $picture,
|
'pictureUrl' => $user->getPictureUrl(),
|
||||||
'email' => $user->getEmail(),
|
'email' => $user->getEmail(),
|
||||||
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()),
|
||||||
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]),
|
||||||
|
|
|
||||||
|
|
@ -30,25 +30,86 @@ class OrganizationsService
|
||||||
|
|
||||||
public function handleLogo(Organizations $organization, $logoFile): void
|
public function handleLogo(Organizations $organization, $logoFile): void
|
||||||
{
|
{
|
||||||
|
// 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures')
|
||||||
|
$destinationDir = 'uploads/organization_logos';
|
||||||
|
|
||||||
|
// 2. Create the directory if it doesn't exist
|
||||||
|
if (!file_exists($destinationDir)) {
|
||||||
|
// 0755 is the standard permission (Owner: read/write/exec, Others: read/exec)
|
||||||
|
if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) {
|
||||||
|
throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir));
|
||||||
|
}
|
||||||
|
}
|
||||||
$extension = $logoFile->guessExtension();
|
$extension = $logoFile->guessExtension();
|
||||||
|
|
||||||
$customFilename = $organization->getName() . '_' . date('dmyHis') . "." . $extension;
|
// Sanitize the filename to remove special characters/spaces to prevent filesystem errors
|
||||||
|
$safeName = preg_replace('/[^a-zA-Z0-9]/', '', $organization->getName());
|
||||||
|
$customFilename = $safeName . '_' . date('dmyHis') . '.' . $extension;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $logoFile, $customFilename, $extension, 'logo/');
|
// 4. Move the file to the destination directory
|
||||||
$this->loggerService->logAWSAction('Upload organization logo', [
|
// The move() method is standard in Symfony/Laravel UploadedFile objects
|
||||||
'organization_id' => $organization->getId(),
|
$logoFile->move($destinationDir, $customFilename);
|
||||||
'filename' => $customFilename,
|
|
||||||
'bucket' => $_ENV['S3_PORTAL_BUCKET'],
|
// 5. Update the user entity with the relative path
|
||||||
|
// Ensure you store the path relative to your public folder usually
|
||||||
|
$organization->setLogoUrl($destinationDir . '/' . $customFilename);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 6. Log the critical error as requested
|
||||||
|
$this->loggerService->logError('File upload failed',[
|
||||||
|
'target_organization_id' => $organization->getId(),
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file_name' => $customFilename,
|
||||||
]);
|
]);
|
||||||
$organization->setLogoUrl('logo/' . $customFilename);
|
|
||||||
} catch (FileException $e) {
|
// Optional: Re-throw the exception if you want the controller/user to know the upload failed
|
||||||
$this->loggerService->logError('Failed to upload organization logo to S3', [
|
throw new FileException('File upload failed.');
|
||||||
'organization_id' => $organization->getId(),
|
}
|
||||||
'error' => $e->getMessage(),
|
}
|
||||||
'bucket' => $_ENV['S3_PORTAL_BUCKET'],
|
|
||||||
|
public function deleteLogo(Organizations $organization): void
|
||||||
|
{
|
||||||
|
// 1. Get the current picture path from the user entity
|
||||||
|
$currentPicturePath = $organization->getLogoUrl();
|
||||||
|
|
||||||
|
// If the user doesn't have a picture, simply return (nothing to delete)
|
||||||
|
if (!$currentPicturePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Check if the file exists on the server before trying to delete
|
||||||
|
// Note: Ensure $currentPicturePath is relative to the script execution or absolute.
|
||||||
|
if (file_exists($currentPicturePath)) {
|
||||||
|
|
||||||
|
// 3. Delete the file
|
||||||
|
if (!unlink($currentPicturePath)) {
|
||||||
|
throw new \Exception(sprintf('Could not unlink "%s"', $currentPicturePath));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Optional: Log a warning if the DB had a path but the file was missing
|
||||||
|
$this->loggerService->logError('File not found on disk during deletion request', [
|
||||||
|
'target_organization_id' => $organization->getId(),
|
||||||
|
'missing_path' => $currentPicturePath
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update the user entity to remove the reference
|
||||||
|
$organization->setLogoUrl("");
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 5. Log the critical error
|
||||||
|
// We log it, but strictly speaking, we might still want to nullify the DB
|
||||||
|
// if the file is corrupted/un-deletable to prevent broken images on the frontend.
|
||||||
|
$this->loggerService->logError('File deletion failed', [
|
||||||
|
'target_organization_id' => $organization->getId(),
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file_path' => $currentPicturePath,
|
||||||
]);
|
]);
|
||||||
throw new FileException('Failed to upload logo to S3: ' . $e->getMessage());
|
|
||||||
|
// Re-throw if you want to stop execution/show error to user
|
||||||
|
throw new \RuntimeException('Unable to remove profile picture.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use RuntimeException;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||||
use App\Event\UserCreatedEvent;
|
use App\Event\UserCreatedEvent;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||||
|
|
||||||
class UserService
|
class UserService
|
||||||
|
|
@ -173,28 +174,91 @@ class UserService
|
||||||
return ['none' => $group];
|
return ['none' => $group];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handleProfilePicture(User $user, $picture): void
|
public function handleProfilePicture(User $user, UploadedFile $picture): void
|
||||||
{
|
{
|
||||||
// Get file extension
|
// 1. Define the destination directory (adjust path as needed, e.g., 'public/uploads/profile_pictures')
|
||||||
$extension = $picture->guessExtension();
|
$destinationDir = 'uploads/profile_pictures';
|
||||||
|
|
||||||
// Create custom filename: userNameUserSurname_dmyHis
|
// 2. Create the directory if it doesn't exist
|
||||||
$customFilename = $user->getName() . $user->getSurname() . '_' . date('dmyHis') . '.' . $extension;
|
if (!file_exists($destinationDir)) {
|
||||||
try {
|
// 0755 is the standard permission (Owner: read/write/exec, Others: read/exec)
|
||||||
$this->awsService->PutDocObj($_ENV['S3_PORTAL_BUCKET'], $picture, $customFilename, $extension, 'profile/');
|
if (!mkdir($destinationDir, 0755, true) && !is_dir($destinationDir)) {
|
||||||
$this->loggerService->logAWSAction(
|
throw new \RuntimeException(sprintf('Directory "%s" was not created', $destinationDir));
|
||||||
'Profile picture uploaded to S3', [
|
}
|
||||||
'user_id' => $user->getId(),
|
|
||||||
'filename' => $customFilename,
|
|
||||||
]);
|
|
||||||
$user->setPictureUrl('profile/' . $customFilename);
|
|
||||||
} catch (FileException $e) {
|
|
||||||
// Handle upload error
|
|
||||||
throw new FileException('File upload failed: ' . $e->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
// 3. Get extension and create a safe filename
|
||||||
//
|
$extension = $picture->guessExtension();
|
||||||
|
|
||||||
|
// Sanitize the filename to remove special characters/spaces to prevent filesystem errors
|
||||||
|
$safeName = preg_replace('/[^a-zA-Z0-9]/', '', $user->getName() . $user->getSurname());
|
||||||
|
$customFilename = $safeName . '_' . date('dmyHis') . '.' . $extension;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 4. Move the file to the destination directory
|
||||||
|
// The move() method is standard in Symfony/Laravel UploadedFile objects
|
||||||
|
$picture->move($destinationDir, $customFilename);
|
||||||
|
|
||||||
|
// 5. Update the user entity with the relative path
|
||||||
|
// Ensure you store the path relative to your public folder usually
|
||||||
|
$user->setPictureUrl($destinationDir . '/' . $customFilename);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 6. Log the critical error as requested
|
||||||
|
$this->loggerService->logError('File upload failed',[
|
||||||
|
'target_user_id' => $user->getId(),
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file_name' => $customFilename,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Optional: Re-throw the exception if you want the controller/user to know the upload failed
|
||||||
|
throw new FileException('File upload failed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteProfilePicture(User $user): void
|
||||||
|
{
|
||||||
|
// 1. Get the current picture path from the user entity
|
||||||
|
$currentPicturePath = $user->getPictureUrl();
|
||||||
|
|
||||||
|
// If the user doesn't have a picture, simply return (nothing to delete)
|
||||||
|
if (!$currentPicturePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Check if the file exists on the server before trying to delete
|
||||||
|
// Note: Ensure $currentPicturePath is relative to the script execution or absolute.
|
||||||
|
if (file_exists($currentPicturePath)) {
|
||||||
|
|
||||||
|
// 3. Delete the file
|
||||||
|
if (!unlink($currentPicturePath)) {
|
||||||
|
throw new \Exception(sprintf('Could not unlink "%s"', $currentPicturePath));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Optional: Log a warning if the DB had a path but the file was missing
|
||||||
|
$this->loggerService->logError('File not found on disk during deletion request', [
|
||||||
|
'target_user_id' => $user->getId(),
|
||||||
|
'missing_path' => $currentPicturePath
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Update the user entity to remove the reference
|
||||||
|
$user->setPictureUrl("");
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 5. Log the critical error
|
||||||
|
// We log it, but strictly speaking, we might still want to nullify the DB
|
||||||
|
// if the file is corrupted/un-deletable to prevent broken images on the frontend.
|
||||||
|
$this->loggerService->logError('File deletion failed', [
|
||||||
|
'target_user_id' => $user->getId(),
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file_path' => $currentPicturePath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Re-throw if you want to stop execution/show error to user
|
||||||
|
throw new \RuntimeException('Unable to remove profile picture.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -297,7 +361,7 @@ class UserService
|
||||||
{
|
{
|
||||||
$formatted = array_map(function (UsersOrganizations $uo) {
|
$formatted = array_map(function (UsersOrganizations $uo) {
|
||||||
$user = $uo->getUsers();
|
$user = $uo->getUsers();
|
||||||
$picture = $this->awsService->getPublicUrl($_ENV['S3_PORTAL_BUCKET']) . $user->getPictureUrl();
|
|
||||||
if ($uo->getStatut() === "INVITED") {
|
if ($uo->getStatut() === "INVITED") {
|
||||||
$statut = "INVITED";
|
$statut = "INVITED";
|
||||||
// if user invited but not accepted in 1 hour, set statut to EXPIRED
|
// if user invited but not accepted in 1 hour, set statut to EXPIRED
|
||||||
|
|
@ -311,7 +375,7 @@ class UserService
|
||||||
$statut = $uo->isActive() ? "ACTIVE" : "INACTIVE";
|
$statut = $uo->isActive() ? "ACTIVE" : "INACTIVE";
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
'pictureUrl' => $picture,
|
'pictureUrl' => $user->getPictureUrl(),
|
||||||
'name' => $user->getSurname(),
|
'name' => $user->getSurname(),
|
||||||
'prenom' => $user->getName(),
|
'prenom' => $user->getName(),
|
||||||
'email' => $user->getEmail(),
|
'email' => $user->getEmail(),
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="col d-flex justify-content-between align-items-center">
|
<div class="col d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex ">
|
<div class="d-flex ">
|
||||||
{% if organization.logoUrl %}
|
{% if organization.logoUrl %}
|
||||||
<img src="{{ aws_url ~ organization.logoUrl }}" alt="Organization logo"
|
<img src="{{ asset(organization.logoUrl) }}" alt="Organization logo"
|
||||||
class="rounded-circle" style="width:40px; height:40px;">
|
class="rounded-circle" style="width:40px; height:40px;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1 class="mb-4 ms-3">{{ organization.name|title }} - Dashboard</h1>
|
<h1 class="mb-4 ms-3">{{ organization.name|title }} - Dashboard</h1>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
{{ form_start(form, {'action': path('user_edit', {'id': user.id}), 'method': 'PUT'}) }}
|
{{ form_start(form, {'action': path('user_edit', {'id': user.id}), 'method': 'POST'}) }}
|
||||||
{{ form_widget(form) }}
|
{{ form_widget(form) }}
|
||||||
<input hidden type="text" value="{{ organizationId }}" name="organizationId">
|
<input hidden type="text" value="{{ organizationId }}" name="organizationId">
|
||||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
{% if user.pictureUrl is not empty %}
|
{% if user.pictureUrl is not empty %}
|
||||||
<img src="{{ aws_url ~ user.pictureUrl }}" alt="user" class="rounded-circle"
|
<img src="{{asset(user.pictureUrl)}}" alt="user" class="rounded-circle"
|
||||||
style="width:40px; height:40px;">
|
style="width:40px; height:40px;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-title ">
|
<div class="card-title ">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue