feat: implement collaborator reactivation logic, add access denied handling, and improve metric filtering
This commit is contained in:
@@ -52,6 +52,7 @@ export default function ClientDashboardLayout({
|
||||
const { goTo } = useNavigation();
|
||||
|
||||
const [client, setClient] = useState<OrganizationClientView | null>(null);
|
||||
const [isCheckingAccess, setIsCheckingAccess] = useState(true);
|
||||
const [clientName, setClientName] = useState<string>("");
|
||||
const [, setClientEmail] = useState<string>("");
|
||||
const [clientAvatar, setClientAvatar] = useState<string>("");
|
||||
@@ -106,6 +107,7 @@ export default function ClientDashboardLayout({
|
||||
}
|
||||
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
setIsCheckingAccess(true);
|
||||
|
||||
findClientById({
|
||||
id: clientId,
|
||||
@@ -119,9 +121,15 @@ export default function ClientDashboardLayout({
|
||||
setClientAvatar(client.avatar);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.desc?.includes("No tienes permisos") || error?.desc?.includes("no tiene permisos")) {
|
||||
setClient(null);
|
||||
goTo("/admin/access-denied");
|
||||
return;
|
||||
}
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
setIsCheckingAccess(false);
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
};
|
||||
@@ -131,7 +139,8 @@ export default function ClientDashboardLayout({
|
||||
<Content>
|
||||
<Suspense fallback={<SuspenseLoading />}>
|
||||
<Secure>
|
||||
<SectionContainer style={{ top: "70px" }}>
|
||||
{isCheckingAccess && <SuspenseLoading />}
|
||||
{client && <SectionContainer style={{ top: "70px" }}>
|
||||
<div className={style.clientProfileHeader}>
|
||||
<Avatar
|
||||
name="userProfileAvatar"
|
||||
@@ -195,7 +204,7 @@ export default function ClientDashboardLayout({
|
||||
</div>
|
||||
</div>
|
||||
<Menu />
|
||||
</SectionContainer>
|
||||
</SectionContainer>}
|
||||
</Secure>
|
||||
</Suspense>
|
||||
</Content>
|
||||
|
||||
+6
-1
@@ -193,8 +193,8 @@ export default function OrganizationProfile() {
|
||||
const filter = useFilterStore();
|
||||
const alert = useAlert();
|
||||
const SessionInfo = useSessionStore();
|
||||
const appointmentsView = useAppointmentsViewStore();
|
||||
const { goTo } = useNavigation();
|
||||
const appointmentsView = useAppointmentsViewStore();
|
||||
|
||||
const [windowWidth] = useWindowSize();
|
||||
|
||||
@@ -317,6 +317,7 @@ export default function OrganizationProfile() {
|
||||
employeeId: appointmentsView.collaboratorId
|
||||
? appointmentsView.collaboratorId
|
||||
: collaboratorId,
|
||||
sessionUser: SessionInfo.userId,
|
||||
};
|
||||
|
||||
const filteredFilter = Object.fromEntries(
|
||||
@@ -338,6 +339,10 @@ export default function OrganizationProfile() {
|
||||
]);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.desc?.includes("No tienes permisos") || error?.desc?.includes("no tiene permisos")) {
|
||||
goTo("/admin/access-denied");
|
||||
return;
|
||||
}
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
+5
@@ -14,6 +14,7 @@ import {
|
||||
CollaboratorSchedulesView,
|
||||
UpdateScheduleParams,
|
||||
UpdateEmployeeParams,
|
||||
RemoveCollaboratorParams,
|
||||
} from "@models/Collaborators.model";
|
||||
import { CollaboratorView, FindCollaboratorByEmailParams } from "@models/User.model";
|
||||
import { ApiVoidResult } from "@models/Api.VoidResult.type";
|
||||
@@ -128,3 +129,7 @@ export const updateSchedulesOverrides = async (data: UpdateSchedulesOverridesPar
|
||||
export const deleteSchedulesOverrides = async (data: DeleteSchedulesOverridesParams): Promise<ApiVoidResult> => {
|
||||
return ApiRequest.post<ApiVoidResult>("schedules-overrides/delete", data);
|
||||
};
|
||||
|
||||
export const removeCollaborator = async (data: RemoveCollaboratorParams): Promise<ApiVoidResult> => {
|
||||
return ApiRequest.post<ApiVoidResult>("employees/remove-collaborator", data);
|
||||
};
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
/* ── Removal card (collapsible) ─────────────────────────────── */
|
||||
|
||||
.removalCard {
|
||||
margin-top: 32px;
|
||||
padding: 0;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 42, 127, 0.06) 0%,
|
||||
rgba(255, 74, 42, 0.04) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 74, 42, 0.18);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.removalCard::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--wine-red) 0%,
|
||||
var(--red) 100%
|
||||
);
|
||||
border-radius: 16px 16px 0 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Toggle button (always visible header) ──────────────────── */
|
||||
|
||||
.removalToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 20px 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-family: "Kanit", serif;
|
||||
text-align: left;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.removalToggle:hover {
|
||||
background: rgba(255, 42, 127, 0.03);
|
||||
}
|
||||
|
||||
.removalToggleLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.removalToggleRight {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.removalToggleText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ── Icon circle ────────────────────────────────────────────── */
|
||||
|
||||
.removalIconCircle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 42, 127, 0.12) 0%,
|
||||
rgba(255, 74, 42, 0.10) 100%
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--wine-red);
|
||||
}
|
||||
|
||||
/* ── Typography ─────────────────────────────────────────────── */
|
||||
|
||||
.removalTitle {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--black);
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.removalSubtitle {
|
||||
font-size: 13px;
|
||||
color: var(--black-light);
|
||||
opacity: 0.7;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.removalExpandLabel {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--wine-red);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.removalChevron {
|
||||
font-size: 22px;
|
||||
color: var(--wine-red);
|
||||
transition: transform 0.22s ease;
|
||||
}
|
||||
|
||||
/* ── Expanded body ──────────────────────────────────────────── */
|
||||
|
||||
.removalBody {
|
||||
padding: 0 24px 24px 24px;
|
||||
animation: removalSlideIn 0.22s ease;
|
||||
}
|
||||
|
||||
@keyframes removalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.removalDivider {
|
||||
height: 1px;
|
||||
background: rgba(255, 74, 42, 0.12);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* ── Description & list (expanded content) ──────────────────── */
|
||||
|
||||
.removalDescription {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 15px;
|
||||
color: var(--black-light);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.removalList {
|
||||
margin: 0 0 22px 0;
|
||||
padding: 0 0 0 22px;
|
||||
font-size: 14.5px;
|
||||
color: var(--black-light);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.removalList li {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.removalList li::marker {
|
||||
color: var(--wine-red);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ── Action row ─────────────────────────────────────────────── */
|
||||
|
||||
.removalActions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.removeButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 11px 28px;
|
||||
border: none;
|
||||
border-radius: 100px;
|
||||
font-family: "Kanit", serif;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.22s ease,
|
||||
box-shadow 0.22s ease,
|
||||
transform 0.15s ease;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 42, 127, 0.10) 0%,
|
||||
rgba(255, 74, 42, 0.08) 100%
|
||||
);
|
||||
color: var(--red);
|
||||
border: 1.5px solid rgba(255, 74, 42, 0.30);
|
||||
box-shadow: 0 1px 3px rgba(255, 42, 127, 0.06);
|
||||
}
|
||||
|
||||
.removeButton:hover {
|
||||
background: var(--red);
|
||||
color: var(--white);
|
||||
border-color: var(--red);
|
||||
box-shadow: 0 4px 14px rgba(255, 74, 42, 0.30);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.removeButton:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(255, 74, 42, 0.20);
|
||||
}
|
||||
|
||||
.removeButtonIcon {
|
||||
font-size: 18px;
|
||||
transition: color 0.22s ease;
|
||||
}
|
||||
+229
-4
@@ -1,18 +1,40 @@
|
||||
"use client";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import Location from "@components/Location/Location";
|
||||
import { useLocationStore } from "@store/Location.Store";
|
||||
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
|
||||
import { useSessionStore } from "@store/Sesion.Store";
|
||||
import { useAlert } from "@store/Alert.Store";
|
||||
import { useConfirmStore } from "@store/Confirm.Store";
|
||||
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
|
||||
import LocalPoliceOutlinedIcon from "@mui/icons-material/LocalPoliceOutlined";
|
||||
import MaterialAlert from "@mui/material/Alert";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import MuiButton from "@mui/material/Button";
|
||||
import Radio from "@mui/material/Radio";
|
||||
import RadioGroup from "@mui/material/RadioGroup";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import turnosXpressTheme from "@core/app/theme/turnosXpress";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import { PopoverPicker } from "@core/app/components/ColorPicker/ColorPicker";
|
||||
import { findCollaboratorsById, updateCollaborator } from "../../Collaborators.Service";
|
||||
import {
|
||||
findCollaboratorsById,
|
||||
updateCollaborator,
|
||||
loadCollaboratorsByCompanyId,
|
||||
removeCollaborator,
|
||||
} from "../../Collaborators.Service";
|
||||
import { ApiError } from "@models/Server.Error.model";
|
||||
import { UpdateEmployeeParams } from "@models/Collaborators.model";
|
||||
import { UpdateEmployeeParams, CompanyEmployeesView, EmployeeRoles } from "@models/Collaborators.model";
|
||||
import { useNavigation } from "@hooks/goto";
|
||||
import style from "./edit.module.css";
|
||||
import PersonOffOutlinedIcon from "@mui/icons-material/PersonOffOutlined";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
|
||||
type UrlData = {
|
||||
id: string;
|
||||
@@ -27,9 +49,19 @@ export default function OrganizationEdit() {
|
||||
const eventHandler = useEventHandlerStore();
|
||||
const SessionInfo = useSessionStore();
|
||||
const alert = useAlert();
|
||||
const confirm = useConfirmStore();
|
||||
const { goTo } = useNavigation();
|
||||
|
||||
const [collaboratorName, setCollaboratorName] = useState<string>("");
|
||||
const [collaboratorUserId, setCollaboratorUserId] = useState<string>("");
|
||||
const [collaboratorColor, setCollaboratorColor] = useState<string>("");
|
||||
const [collaborators, setCollaborators] = useState<CompanyEmployeesView[]>([]);
|
||||
const [isOwner, setIsOwner] = useState<boolean>(false);
|
||||
const [replacementDialogOpen, setReplacementDialogOpen] = useState<boolean>(false);
|
||||
const [selectedReplacement, setSelectedReplacement] = useState<string>("");
|
||||
const [removalCollapsed, setRemovalCollapsed] = useState<boolean>(true);
|
||||
|
||||
const toggleRemoval = useCallback(() => setRemovalCollapsed((prev) => !prev), []);
|
||||
|
||||
useEffect(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
@@ -58,6 +90,7 @@ export default function OrganizationEdit() {
|
||||
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
loadCollaborator();
|
||||
loadCollaborators();
|
||||
}, [collaboratorName]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -83,6 +116,21 @@ export default function OrganizationEdit() {
|
||||
}
|
||||
}, [eventHandler.eventType]);
|
||||
|
||||
const loadCollaborators = () => {
|
||||
loadCollaboratorsByCompanyId({ companyId: id })
|
||||
.then((list) => {
|
||||
setCollaborators(list);
|
||||
const current = list.find((c) => c.id === cid);
|
||||
setIsOwner(
|
||||
current !== undefined &&
|
||||
current.roles.includes(EmployeeRoles.OWNER)
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
// Non-critical — removal button still works, server validates
|
||||
});
|
||||
};
|
||||
|
||||
const loadCollaborator = () => {
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
findCollaboratorsById({ id: cid })
|
||||
@@ -92,6 +140,7 @@ export default function OrganizationEdit() {
|
||||
}
|
||||
|
||||
setCollaboratorName(collaborator.fullName);
|
||||
setCollaboratorUserId(collaborator.userId);
|
||||
setCollaboratorColor(collaborator.calendarColor);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -102,6 +151,65 @@ export default function OrganizationEdit() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveClick = () => {
|
||||
if (isOwner) {
|
||||
alert.showError("No se puede eliminar al propietario de la organización");
|
||||
return;
|
||||
}
|
||||
|
||||
if (String(SessionInfo.userId) === collaboratorUserId) {
|
||||
alert.showError("No puedes eliminarte a ti mismo de la organización");
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates = collaborators.filter(
|
||||
(c) => c.id !== cid
|
||||
);
|
||||
|
||||
if (candidates.length > 0) {
|
||||
setReplacementDialogOpen(true);
|
||||
setSelectedReplacement("");
|
||||
} else {
|
||||
confirm.show(
|
||||
"¿Estás seguro de que deseas remover este colaborador de la organización?",
|
||||
handleConfirmRemove,
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmReplace = () => {
|
||||
setReplacementDialogOpen(false);
|
||||
confirm.show(
|
||||
"¿Estás seguro de que deseas remover este colaborador de la organización? Los turnos futuros serán transferidos al colaborador seleccionado.",
|
||||
handleConfirmRemove,
|
||||
() => {}
|
||||
);
|
||||
};
|
||||
|
||||
const handleConfirmRemove = () => {
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
|
||||
removeCollaborator({
|
||||
employeeId: cid,
|
||||
companyId: id,
|
||||
sessionUser: SessionInfo.userId,
|
||||
...(selectedReplacement
|
||||
? { replacementEmployeeId: selectedReplacement }
|
||||
: {}),
|
||||
})
|
||||
.then(() => {
|
||||
alert.showSuccess("Colaborador removido correctamente");
|
||||
goTo("/admin/org/profile/" + id + "/collaborators");
|
||||
})
|
||||
.catch((error: ApiError) => {
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Location />
|
||||
@@ -119,7 +227,124 @@ export default function OrganizationEdit() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ marginBottom: "10px" }}> </div>
|
||||
{!isOwner && (
|
||||
<div className={style.removalCard}>
|
||||
<button
|
||||
type="button"
|
||||
className={style.removalToggle}
|
||||
onClick={toggleRemoval}
|
||||
aria-expanded={!removalCollapsed}
|
||||
aria-controls="removal-details"
|
||||
>
|
||||
<div className={style.removalToggleLeft}>
|
||||
<div className={style.removalIconCircle}>
|
||||
<PersonOffOutlinedIcon fontSize="medium" />
|
||||
</div>
|
||||
<div className={style.removalToggleText}>
|
||||
<h3 className={style.removalTitle}>
|
||||
Remover colaborador
|
||||
</h3>
|
||||
{!removalCollapsed && (
|
||||
<span className={style.removalSubtitle}>
|
||||
Acción irreversible — revisá los efectos antes de continuar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.removalToggleRight}>
|
||||
{removalCollapsed ? (
|
||||
<>
|
||||
<span className={style.removalExpandLabel}>
|
||||
Mostrar opciones de remoción
|
||||
</span>
|
||||
<ExpandMoreIcon className={style.removalChevron} />
|
||||
</>
|
||||
) : (
|
||||
<ExpandLessIcon className={style.removalChevron} />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{!removalCollapsed && (
|
||||
<div id="removal-details" className={style.removalBody}>
|
||||
<div className={style.removalDivider} />
|
||||
|
||||
<p className={style.removalDescription}>
|
||||
Al remover este colaborador de la organización:
|
||||
</p>
|
||||
|
||||
<ul className={style.removalList}>
|
||||
<li>Dejará de pertenecer a la organización.</li>
|
||||
<li>
|
||||
Los turnos futuros y repeticiones activas serán
|
||||
transferidos a otro colaborador.
|
||||
</li>
|
||||
<li>El historial de turnos permanecerá preservado.</li>
|
||||
<li>
|
||||
Ya no recibirá nuevas reservas ni turnos asignados.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className={style.removalActions}>
|
||||
<button
|
||||
className={style.removeButton}
|
||||
type="button"
|
||||
onClick={handleRemoveClick}
|
||||
>
|
||||
<PersonOffOutlinedIcon className={style.removeButtonIcon} />
|
||||
Remover colaborador
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ThemeProvider theme={turnosXpressTheme}>
|
||||
<Dialog
|
||||
open={replacementDialogOpen}
|
||||
onClose={() => setReplacementDialogOpen(false)}
|
||||
>
|
||||
<DialogTitle>Seleccionar reemplazo</DialogTitle>
|
||||
<DialogContent>
|
||||
<p style={{ marginBottom: "16px", color: "#666" }}>
|
||||
Este colaborador tiene turnos futuros asignados.
|
||||
Seleccione un colaborador de reemplazo para transferir los turnos.
|
||||
</p>
|
||||
<FormControl component="fieldset">
|
||||
<RadioGroup
|
||||
value={selectedReplacement}
|
||||
onChange={(e) => setSelectedReplacement(e.target.value)}
|
||||
>
|
||||
{collaborators
|
||||
.filter((c) => c.id !== cid)
|
||||
.map((collaborator) => (
|
||||
<FormControlLabel
|
||||
key={collaborator.id}
|
||||
value={collaborator.id}
|
||||
control={<Radio />}
|
||||
label={`${collaborator.fullName} (${collaborator.email})`}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MuiButton onClick={() => setReplacementDialogOpen(false)}>
|
||||
Cancelar
|
||||
</MuiButton>
|
||||
<MuiButton
|
||||
onClick={handleConfirmReplace}
|
||||
disabled={!selectedReplacement}
|
||||
color="success"
|
||||
variant="contained"
|
||||
autoFocus
|
||||
>
|
||||
Aceptar
|
||||
</MuiButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import Menu from "@components/Menu/Menu";
|
||||
import { useOrganizationHeaderStore } from "@core/Store/OrganizationHeader.Store";
|
||||
import Content from "@core/app/components/Content/Content";
|
||||
import SuspenseLoading from "@core/app/components/SuspendeLoading/SuspenseLoading";
|
||||
import { useNavigation } from "@core/app/hooks/goto";
|
||||
|
||||
type UrlData = {
|
||||
id: string;
|
||||
@@ -18,24 +19,33 @@ type UrlData = {
|
||||
|
||||
export default function OrgLayout({ children }: { children: React.ReactNode }) {
|
||||
const [organization, setOrganization] = useState<MyOranizationsView | undefined>();
|
||||
const [isCheckingAccess, setIsCheckingAccess] = useState(true);
|
||||
const data = useParams<UrlData>();
|
||||
const organizationId = data.id;
|
||||
const SessionInfo = useSessionStore();
|
||||
const organizationHeader = useOrganizationHeaderStore();
|
||||
const { goTo } = useNavigation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!SessionInfo.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadOrganizationsById({ id: organizationId, sessionUser: SessionInfo.userId }).then(
|
||||
(organization) => {
|
||||
setIsCheckingAccess(true);
|
||||
loadOrganizationsById({ id: organizationId, sessionUser: SessionInfo.userId })
|
||||
.then((organization) => {
|
||||
setOrganization(organization);
|
||||
organizationHeader.setHeaderColor(organization.headerColor);
|
||||
organizationHeader.setHeaderFontColor(organization.headerFontColor);
|
||||
organizationHeader.setHeaderFontShadowColor(organization.headerFontShadowColor);
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
setOrganization(undefined);
|
||||
goTo("/admin/access-denied");
|
||||
})
|
||||
.finally(() => {
|
||||
setIsCheckingAccess(false);
|
||||
});
|
||||
}, [SessionInfo.userId]);
|
||||
|
||||
return (
|
||||
@@ -43,9 +53,14 @@ export default function OrgLayout({ children }: { children: React.ReactNode }) {
|
||||
<Content>
|
||||
<Suspense fallback={<SuspenseLoading />}>
|
||||
<Secure>
|
||||
{isCheckingAccess && <SuspenseLoading />}
|
||||
{organization && <OrganizationHeader data={organization} />}
|
||||
<OrganizationContent>{children}</OrganizationContent>
|
||||
<Menu />
|
||||
{organization && (
|
||||
<>
|
||||
<OrganizationContent>{children}</OrganizationContent>
|
||||
<Menu />
|
||||
</>
|
||||
)}
|
||||
</Secure>
|
||||
</Suspense>
|
||||
</Content>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
.page {
|
||||
align-items: center;
|
||||
background: radial-gradient(circle at top, rgba(255, 47, 125, 0.24), transparent 30%), linear-gradient(160deg, #3b0076 0%, #4b0097 48%, #8b2cff 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: calc(100vh - 56px);
|
||||
padding: 48px 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 28px 70px rgba(20, 0, 50, 0.28);
|
||||
max-width: 720px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.accent {
|
||||
background: linear-gradient(90deg, #ff2f7d, #ff6b35, #7c3aed);
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.content {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding: 44px 42px 38px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.iconWrap {
|
||||
align-items: center;
|
||||
background: linear-gradient(145deg, rgba(255, 47, 125, 0.12), rgba(124, 58, 237, 0.12));
|
||||
border: 1px solid rgba(255, 47, 125, 0.2);
|
||||
border-radius: 24px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
color: #ff2f7d;
|
||||
display: flex;
|
||||
height: 76px;
|
||||
justify-content: center;
|
||||
width: 76px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
background: rgba(255, 47, 125, 0.1);
|
||||
border-radius: 999px;
|
||||
color: #b91c5c;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 7px 13px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #2b174f;
|
||||
font-size: clamp(28px, 4vw, 38px);
|
||||
line-height: 1.05;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #4b5563;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
margin: 0;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.primaryAction,
|
||||
.secondaryAction {
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
font-weight: 900;
|
||||
justify-content: center;
|
||||
min-height: 46px;
|
||||
padding: 0 20px;
|
||||
text-decoration: none;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
.primaryAction {
|
||||
background: linear-gradient(135deg, #ff2f7d, #7c3aed);
|
||||
box-shadow: 0 14px 28px rgba(124, 58, 237, 0.28);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.secondaryAction {
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(124, 58, 237, 0.18);
|
||||
color: #4b148c;
|
||||
}
|
||||
|
||||
.primaryAction:hover,
|
||||
.secondaryAction:hover {
|
||||
box-shadow: 0 18px 34px rgba(43, 23, 79, 0.18);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.content {
|
||||
padding: 34px 24px 30px;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.primaryAction,
|
||||
.secondaryAction {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
|
||||
import HomeOutlinedIcon from "@mui/icons-material/HomeOutlined";
|
||||
import GroupsOutlinedIcon from "@mui/icons-material/GroupsOutlined";
|
||||
import style from "./access-denied.module.css";
|
||||
|
||||
export default function AdminAccessDeniedPage() {
|
||||
return (
|
||||
<div className={style.page}>
|
||||
<div className={style.card}>
|
||||
<div className={style.accent} />
|
||||
<div className={style.content}>
|
||||
<div className={style.iconWrap}>
|
||||
<ShieldOutlinedIcon sx={{ fontSize: 42 }} />
|
||||
</div>
|
||||
<span className={style.eyebrow}>Acceso restringido</span>
|
||||
<h1 className={style.title}>No tenés permisos para acceder</h1>
|
||||
<p className={style.description}>
|
||||
Esta sección pertenece a una organización en la que ya no tenés permisos activos.
|
||||
Si creés que se trata de un error, comunicate con un administrador de la organización.
|
||||
</p>
|
||||
<div className={style.actions}>
|
||||
<Link className={style.primaryAction} href="/admin/org">
|
||||
<GroupsOutlinedIcon sx={{ fontSize: 19, mr: 0.75 }} />
|
||||
Ir a Mis organizaciones
|
||||
</Link>
|
||||
<Link className={style.secondaryAction} href="/landing/dashboard">
|
||||
<HomeOutlinedIcon sx={{ fontSize: 19, mr: 0.75 }} />
|
||||
Volver al dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user