506 lines
20 KiB
JavaScript
506 lines
20 KiB
JavaScript
import { useConfig } from '../hooks/useConfig';
|
|
import { useDataContext } from '../hooks/useDataContext';
|
|
import { DataProvider } from '../context/DataContext';
|
|
|
|
import CustomContainer from '../components/CustomContainer';
|
|
import ContentWrapper from '../components/ContentWrapper';
|
|
import LoadingIcon from '../components/LoadingIcon';
|
|
|
|
import { Card, ListGroup, Form, FloatingLabel } from 'react-bootstrap';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import {
|
|
faUser, faIdCard, faEnvelope, faPhone, faHashtag,
|
|
faSeedling, faUserShield, faCalendar,
|
|
faUserSlash, faUserPlus,
|
|
faArrowRightFromBracket,
|
|
faCog,
|
|
faEyeSlash,
|
|
faEye,
|
|
faKey
|
|
} from '@fortawesome/free-solid-svg-icons';
|
|
|
|
import '../css/Perfil.css';
|
|
|
|
import { useState } from 'react';
|
|
import IngresoCard from '../components/Ingresos/IngresoCard';
|
|
import SolicitudCard from '../components/Solicitudes/SolicitudCard';
|
|
import CustomModal from '../components/CustomModal';
|
|
import NewUserForm from '../components/Solicitudes/NewUserForm';
|
|
import NotificationModal from '../components/NotificationModal';
|
|
import { Button, Col, Row } from 'react-bootstrap';
|
|
import AnimatedDropdown from '../components/AnimatedDropdown';
|
|
import { useAuth } from '../hooks/useAuth';
|
|
import { CONSTANTS } from '../util/constants';
|
|
import { useError } from '../context/ErrorContext';
|
|
|
|
const parseDate = (date) => {
|
|
if (!date) return 'NO';
|
|
const d = new Date(date);
|
|
return `${d.getDate().toString().padStart(2, '0')}/${(d.getMonth() + 1).toString().padStart(2, '0')}/${d.getFullYear()}`;
|
|
};
|
|
|
|
const getPFP = (tipo) => {
|
|
const base = '/images/icons/';
|
|
const map = {
|
|
1: 'farmer.svg',
|
|
2: 'green_house.svg',
|
|
0: 'list.svg',
|
|
3: 'join.svg',
|
|
4: 'subvencion4.svg',
|
|
5: 'programmer.svg'
|
|
};
|
|
return base + (map[tipo] || 'farmer.svg');
|
|
};
|
|
|
|
const Perfil = () => {
|
|
const { config, configLoading } = useConfig();
|
|
const { showError } = useError();
|
|
|
|
if (configLoading || !config) return <p className="text-center my-5"><LoadingIcon /></p>;
|
|
|
|
const buildUrl = (base, endpoint, params = {}) => {
|
|
if (!endpoint) return null;
|
|
let url = base + endpoint;
|
|
for (const [key, value] of Object.entries(params)) {
|
|
url = url.replace(`:${key}`, value);
|
|
}
|
|
return url;
|
|
};
|
|
|
|
const reqConfig = {
|
|
baseUrl: `${config.apiConfig.baseUrl}${config.apiConfig.endpoints.users.me}`,
|
|
requestUrl: buildUrl(config.apiConfig.baseUrl, config.apiConfig.endpoints.requests.all),
|
|
changePasswordUrl: buildUrl(config.apiConfig.coreUrl, config.apiConfig.endpoints.auth.changePassword),
|
|
};
|
|
|
|
return (
|
|
<DataProvider config={reqConfig} onError={showError}>
|
|
<PerfilContent config={reqConfig} />
|
|
</DataProvider>
|
|
);
|
|
};
|
|
|
|
const PerfilContent = ({ config }) => {
|
|
const { data, dataLoading, postData } = useDataContext();
|
|
const { logout } = useAuth();
|
|
|
|
const identity = JSON.parse(localStorage.getItem("identity"));
|
|
|
|
const myRequests = data?.requests ?? [];
|
|
const incomes = data?.payments ?? [];
|
|
|
|
const hasCollaborator = data?.hasCollaborator ?? false;
|
|
const hasCollaboratorRequest = data?.hasCollaboratorRequest ?? false;
|
|
const hasGreenHouse = data?.hasGreenhouse ?? false;
|
|
const hasGreenHouseRequest = data?.hasGreenhouseRequest ?? false;
|
|
|
|
const [showAddCollaboratorModal, setShowAddCollaboratorModal] = useState(false);
|
|
const [showRemoveCollaboratorModal, setShowRemoveCollaboratorModal] = useState(false);
|
|
const [feedbackModal, setFeedbackModal] = useState(null);
|
|
const closeFeedback = () => setFeedbackModal(null);
|
|
const [fieldErrors, setFieldErrors] = useState(null);
|
|
|
|
const baseMetadata = {
|
|
displayName: identity.user.displayName,
|
|
username: identity.account.username,
|
|
dni: identity.metadata.dni,
|
|
phone: identity.metadata.phone,
|
|
email: identity.account.email,
|
|
memberNumber: identity.metadata.memberNumber,
|
|
plotNumber: identity.metadata.plotNumber,
|
|
type: identity.metadata.type,
|
|
role: identity.metadata.role
|
|
};
|
|
|
|
const sendSimpleRequest = async (type) => {
|
|
setFieldErrors(null);
|
|
const requestOf = type == 1 ? "baja" : type == 2 ? "adición de colaborador" :
|
|
type == 3 ? "eliminación de colaborador" : type == 4 ? "adición de invernadero" :
|
|
type == 5 ? "eliminación de invernadero" : "desconocido";
|
|
try {
|
|
await postData(config.requestUrl, {
|
|
type,
|
|
status: CONSTANTS.REQUEST_PENDING,
|
|
userId: identity.user.userId,
|
|
name: identity.user.displayName,
|
|
metadata: baseMetadata
|
|
});
|
|
setFeedbackModal({
|
|
title: 'Solicitud enviada',
|
|
message: `Se ha enviado la solicitud de ${requestOf} correctamente.`,
|
|
variant: 'success',
|
|
onClick: closeFeedback
|
|
});
|
|
} catch (err) {
|
|
if (err?.status === 422 && err?.errors) {
|
|
setFieldErrors(err.errors);
|
|
}
|
|
}
|
|
};
|
|
|
|
const [newPasswordData, setNewPasswordData] = useState({
|
|
oldPassword: "",
|
|
newPassword: "",
|
|
confirmNewPassword: "",
|
|
serviceId: identity.account.serviceId
|
|
});
|
|
|
|
const [showOld, setShowOld] = useState(false);
|
|
const [showNew, setShowNew] = useState(false);
|
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
|
|
const handleChange = (e) => {
|
|
setNewPasswordData({
|
|
...newPasswordData,
|
|
[e.target.name]: e.target.value
|
|
});
|
|
setFieldErrors(null);
|
|
}
|
|
|
|
const handleChangePassword = async () => {
|
|
try {
|
|
await postData(config.changePasswordUrl, {
|
|
oldPassword: newPasswordData.oldPassword,
|
|
newPassword: newPasswordData.newPassword,
|
|
serviceId: identity.account.serviceId
|
|
});
|
|
|
|
setNewPasswordData({
|
|
oldPassword: "",
|
|
newPassword: "",
|
|
confirmNewPassword: "",
|
|
serviceId: identity.account.serviceId
|
|
});
|
|
|
|
setFeedbackModal({
|
|
title: 'Contraseña cambiada',
|
|
message: 'Tu contraseña ha sido cambiada correctamente.',
|
|
variant: 'success',
|
|
onClick: () => {
|
|
closeFeedback();
|
|
logout();
|
|
}
|
|
});
|
|
} catch (err) {
|
|
if (err?.status === 422 && err?.errors) {
|
|
setFieldErrors(err.errors);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getFieldError = (field) => fieldErrors?.[field] ?? null;
|
|
|
|
const mappedRequests = myRequests.map(r => ({
|
|
...r,
|
|
type: r.type ?? r.type,
|
|
status: r.status ?? r.status,
|
|
request_createdAt: r.request_createdAt ?? r.createdAt
|
|
}));
|
|
|
|
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
|
|
|
|
return (
|
|
<CustomContainer>
|
|
<ContentWrapper>
|
|
<Row className='gap-2 justify-content-center'>
|
|
<Col xs={12} md={4} className="mb-4">
|
|
<Card className="shadow-sm rounded-4 perfil-card">
|
|
<Card.Header className="bg-secondary text-white rounded-top-4 d-flex align-items-center justify-content-between">
|
|
<div className="d-flex align-items-center">
|
|
<img src={getPFP(identity.metadata.type)} alt="PFP" width={36} className="me-3" />
|
|
<div className="m-0 p-0">
|
|
<Card.Title className="mb-0">{`@${identity.account.username}`}</Card.Title>
|
|
<small>Te uniste el {parseDate(identity.metadata.createdAt)}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatedDropdown
|
|
className="end-0"
|
|
buttonStyle="card-button"
|
|
icon={<FontAwesomeIcon icon={faCog} className="fa-xl" />}
|
|
>
|
|
{({ closeDropdown }) => (
|
|
<>
|
|
{!hasGreenHouse && !hasGreenHouseRequest && (
|
|
<div className="dropdown-item d-flex align-items-center" onClick={() => {
|
|
sendSimpleRequest(CONSTANTS.REQUEST_TYPE_ADD_GREENHOUSE);
|
|
closeDropdown();
|
|
}}>
|
|
<FontAwesomeIcon icon={faSeedling} className="me-2" />Solicitar invernadero
|
|
</div>
|
|
)}
|
|
{!hasCollaborator && !hasCollaboratorRequest && (
|
|
<div className="dropdown-item d-flex align-items-center" onClick={() => {
|
|
setShowAddCollaboratorModal(true);
|
|
setFieldErrors(null);
|
|
closeDropdown();
|
|
}}>
|
|
<FontAwesomeIcon icon={faUserPlus} className="me-2" />Añadir un colaborador
|
|
</div>
|
|
)}
|
|
<hr className="dropdown-divider" />
|
|
{hasGreenHouse && !hasGreenHouseRequest && (
|
|
<div className="dropdown-item d-flex align-items-center text-danger" onClick={() => {
|
|
sendSimpleRequest(CONSTANTS.REQUEST_TYPE_REMOVE_GREENHOUSE);
|
|
closeDropdown();
|
|
}}>
|
|
<FontAwesomeIcon icon={faArrowRightFromBracket} className="me-2" />Dejar invernadero
|
|
</div>
|
|
)}
|
|
{hasCollaborator && !hasCollaboratorRequest && (
|
|
<div className="dropdown-item d-flex align-items-center text-danger" onClick={() => {
|
|
setShowRemoveCollaboratorModal(true);
|
|
closeDropdown();
|
|
}}>
|
|
<FontAwesomeIcon icon={faUserSlash} className="me-2" />Quitar colaborador
|
|
</div>
|
|
)}
|
|
<div className="dropdown-item d-flex align-items-center text-danger" onClick={() => {
|
|
sendSimpleRequest(CONSTANTS.REQUEST_TYPE_UNREGISTER);
|
|
closeDropdown();
|
|
}}>
|
|
<FontAwesomeIcon icon={faUserSlash} className="me-2" />Darse de baja
|
|
</div>
|
|
</>
|
|
)}
|
|
</AnimatedDropdown>
|
|
</Card.Header>
|
|
|
|
|
|
<Card.Body>
|
|
<ListGroup variant="flush" className="border rounded-3">
|
|
<ListGroup.Item><FontAwesomeIcon icon={faUser} className="me-2" />Nombre: <strong>{identity.user.displayName}</strong></ListGroup.Item>
|
|
<ListGroup.Item><FontAwesomeIcon icon={faIdCard} className="me-2" />DNI: <strong>{identity.metadata.dni}</strong></ListGroup.Item>
|
|
<ListGroup.Item><FontAwesomeIcon icon={faEnvelope} className="me-2" />Email: <strong>{identity.account.email}</strong></ListGroup.Item>
|
|
<ListGroup.Item><FontAwesomeIcon icon={faPhone} className="me-2" />Teléfono: <strong>{identity.metadata.phone}</strong></ListGroup.Item>
|
|
<ListGroup.Item>
|
|
<FontAwesomeIcon icon={faHashtag} className="me-2" />Socio Nº: <strong>{identity.metadata.memberNumber}</strong> | Huerto Nº: <strong>{identity.metadata.plotNumber}</strong>
|
|
</ListGroup.Item>
|
|
<ListGroup.Item>
|
|
<FontAwesomeIcon icon={faSeedling} className="me-2" />Tipo de socio: <strong>{['LISTA DE ESPERA', 'HORTELANO', 'HORTELANO + INVERNADERO', 'COLABORADOR', 'SUBVENCION', 'DESARROLLADOR'][identity.metadata.type]}</strong>
|
|
</ListGroup.Item>
|
|
<ListGroup.Item>
|
|
<FontAwesomeIcon icon={faUserShield} className="me-2" />Rol en huertos: <strong>{['USUARIO', 'ADMIN', 'DESARROLLADOR'][identity.metadata.role]}</strong>
|
|
</ListGroup.Item>
|
|
<ListGroup.Item>
|
|
<FontAwesomeIcon icon={faCalendar} className="me-2" />Estado: <strong>{identity.account.status === 1 ? 'ACTIVO' : 'INACTIVO'}</strong>
|
|
</ListGroup.Item>
|
|
</ListGroup>
|
|
</Card.Body>
|
|
</Card>
|
|
</Col>
|
|
|
|
|
|
<Col xs={12} md={7}>
|
|
<h2 className='section-title'>Mis pagos</h2>
|
|
<hr className="section-divider" />
|
|
{incomes.length === 0 && <p className="text-center">No hay pagos registrados.</p>}
|
|
<div className="d-flex flex-wrap gap-3 mb-4">
|
|
{incomes.map(income => (
|
|
<IngresoCard key={income.incomeId} income={income} editable={false} />
|
|
))}
|
|
</div>
|
|
|
|
<h2 className='section-title'>Mis solicitudes</h2>
|
|
<hr className="section-divider" />
|
|
{myRequests.length === 0 && <p className="text-center">No tienes solicitudes registradas.</p>}
|
|
|
|
<div className="d-flex flex-wrap gap-3 mb-4">
|
|
{mappedRequests.map(request => (
|
|
<SolicitudCard key={request.requestId} data={request} editable={false} onProfile={true} />
|
|
))}
|
|
</div>
|
|
|
|
<h2 className='section-title'>Cambio de contraseña</h2>
|
|
<hr className="section-divider" />
|
|
<Form onSubmit={(e) => { e.preventDefault(); handleChangePassword(); }} className="d-flex flex-column gap-3">
|
|
<div className="d-flex flex-column gap-3">
|
|
{/* Contraseña actual */}
|
|
<FloatingLabel controlId="floatingPassword" label={<><FontAwesomeIcon icon={faUser} className="me-2" />Contraseña actual</>}>
|
|
<Form.Control
|
|
required
|
|
onChange={handleChange}
|
|
type={showOld ? "text" : "password"}
|
|
placeholder=""
|
|
name="oldPassword"
|
|
className="rounded-4"
|
|
isInvalid={!!fieldErrors?.oldPassword}
|
|
/>
|
|
<Form.Control.Feedback type="invalid">
|
|
{getFieldError("oldPassword")}
|
|
</Form.Control.Feedback>
|
|
<Button
|
|
variant="link"
|
|
className="show-button position-absolute end-0 top-50 translate-middle-y me-2"
|
|
onClick={() => setShowOld(!showOld)}
|
|
aria-label="Mostrar contraseña"
|
|
tabIndex={-1}
|
|
style={{ zIndex: 2 }}
|
|
>
|
|
<FontAwesomeIcon icon={showOld ? faEyeSlash : faEye} className='fa-lg' />
|
|
</Button>
|
|
</FloatingLabel>
|
|
|
|
{/* Nueva contraseña */}
|
|
<FloatingLabel controlId="floatingNewPassword" label={<><FontAwesomeIcon icon={faUser} className="me-2" />Nueva contraseña</>}>
|
|
<Form.Control
|
|
required
|
|
onChange={handleChange}
|
|
type={showNew ? "text" : "password"}
|
|
placeholder=""
|
|
name="newPassword"
|
|
className="rounded-4"
|
|
isInvalid={!!fieldErrors?.newPassword}
|
|
/>
|
|
<Form.Control.Feedback type="invalid" as="span">
|
|
{getFieldError("newPassword")}
|
|
</Form.Control.Feedback>
|
|
<Button
|
|
variant="link"
|
|
className="show-button position-absolute end-0 top-50 translate-middle-y me-2"
|
|
onClick={() => setShowNew(!showNew)}
|
|
aria-label="Mostrar contraseña"
|
|
tabIndex={-1}
|
|
style={{ zIndex: 2 }}
|
|
>
|
|
<FontAwesomeIcon icon={showNew ? faEyeSlash : faEye} className='fa-lg' />
|
|
</Button>
|
|
</FloatingLabel>
|
|
|
|
{/* Confirmar nueva contraseña */}
|
|
<FloatingLabel controlId="floatingConfirmPassword" label={<><FontAwesomeIcon icon={faUser} className="me-2" />Confirmar nueva contraseña</>}>
|
|
<Form.Control
|
|
required
|
|
onChange={handleChange}
|
|
type={showConfirm ? "text" : "password"}
|
|
placeholder=""
|
|
name="confirmNewPassword"
|
|
className="rounded-4"
|
|
/>
|
|
<Button
|
|
variant="link"
|
|
className="show-button position-absolute end-0 top-50 translate-middle-y me-2"
|
|
onClick={() => setShowConfirm(!showConfirm)}
|
|
aria-label="Mostrar contraseña"
|
|
tabIndex={-1}
|
|
style={{ zIndex: 2 }}
|
|
>
|
|
<FontAwesomeIcon icon={showConfirm ? faEyeSlash : faEye} className='fa-lg' />
|
|
</Button>
|
|
</FloatingLabel>
|
|
</div>
|
|
<Button
|
|
disabled={newPasswordData.newPassword !== newPasswordData.confirmNewPassword ||
|
|
newPasswordData.newPassword === '' || newPasswordData.confirmNewPassword === '' ||
|
|
newPasswordData.oldPassword === ''
|
|
}
|
|
type='submit'
|
|
variant="warning"
|
|
style={{ width: 'fit-content' }}
|
|
className='rounded-4'
|
|
>
|
|
<FontAwesomeIcon icon={faKey} className="me-2" /> Cambiar contraseña
|
|
</Button>
|
|
</Form>
|
|
</Col>
|
|
</Row>
|
|
|
|
<CustomModal
|
|
title="Añadir colaborador"
|
|
show={showAddCollaboratorModal}
|
|
onClose={() => {
|
|
setShowAddCollaboratorModal(false);
|
|
setFieldErrors(null);
|
|
}}
|
|
>
|
|
<NewUserForm
|
|
userType={3}
|
|
plotNumber={identity.metadata.plotNumber}
|
|
fieldErrors={fieldErrors}
|
|
onSubmit={async (formData) => {
|
|
try {
|
|
setFieldErrors(null);
|
|
|
|
await postData(config.requestUrl, {
|
|
type: CONSTANTS.REQUEST_TYPE_ADD_COLLABORATOR,
|
|
status: CONSTANTS.REQUEST_PENDING,
|
|
userId: identity.user.userId,
|
|
name: identity.user.displayName,
|
|
metadata: {
|
|
displayName: formData.displayName,
|
|
username: formData.username,
|
|
dni: formData.dni,
|
|
phone: formData.phone,
|
|
email: formData.email,
|
|
memberNumber: formData.memberNumber,
|
|
plotNumber: formData.plotNumber,
|
|
type: formData.type
|
|
}
|
|
});
|
|
|
|
setShowAddCollaboratorModal(false);
|
|
setFeedbackModal({
|
|
title: 'Solicitud enviada',
|
|
message: 'El colaborador ha sido solicitado correctamente.',
|
|
variant: 'success',
|
|
onClick: closeFeedback
|
|
});
|
|
|
|
} catch (err) {
|
|
if (err?.status === 422 && err?.errors) {
|
|
setFieldErrors(err.errors);
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</CustomModal>
|
|
|
|
|
|
<CustomModal
|
|
title="Eliminar colaborador"
|
|
show={showRemoveCollaboratorModal}
|
|
onClose={() => setShowRemoveCollaboratorModal(false)}
|
|
>
|
|
<p className=' p-3'>¿Estás seguro de que quieres eliminar tu colaborador actual?</p>
|
|
<div className="d-flex justify-content-end gap-2 mt-3 p-3">
|
|
<Button variant="secondary" onClick={() => setShowRemoveCollaboratorModal(false)}>Cancelar</Button>
|
|
<Button
|
|
variant="warning"
|
|
onClick={async () => {
|
|
try {
|
|
sendSimpleRequest(CONSTANTS.REQUEST_TYPE_REMOVE_COLLABORATOR);
|
|
setFeedbackModal({
|
|
title: "Solicitud enviada",
|
|
message: "Se ha solicitado la eliminación del colaborador.",
|
|
variant: "success",
|
|
onClick: closeFeedback
|
|
});
|
|
setShowRemoveCollaboratorModal(false);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}}
|
|
>
|
|
Confirmar
|
|
</Button>
|
|
</div>
|
|
</CustomModal>
|
|
|
|
{feedbackModal && (
|
|
<NotificationModal
|
|
show={true}
|
|
onClose={closeFeedback}
|
|
title={feedbackModal.title}
|
|
message={feedbackModal.message}
|
|
variant={feedbackModal.variant}
|
|
buttons={[{ label: "Aceptar", variant: feedbackModal.variant, onClick: feedbackModal.onClick }]}
|
|
/>
|
|
)}
|
|
|
|
</ContentWrapper>
|
|
</CustomContainer>
|
|
);
|
|
};
|
|
|
|
export default Perfil;
|