Files
huertos-bellavista-web/src/pages/Perfil.jsx
2026-01-31 01:56:38 +01:00

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 : <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;