only change password, docs and mail left

This commit is contained in:
Jose
2026-01-30 22:38:15 +01:00
parent f7070fd91a
commit c0bcbfb547
21 changed files with 473 additions and 341 deletions

View File

@@ -4,7 +4,6 @@ import AnimatedDropdown from '../../components/AnimatedDropdown';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEdit, faTrash, faEllipsisVertical } from '@fortawesome/free-solid-svg-icons';
import '../../css/AnuncioCard.css';
import { renderErrorAlert } from '../../util/alertHelpers';
import {
EditorProvider,
Editor,
@@ -37,7 +36,7 @@ const formatDateTime = (iso) => {
};
};
const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onCancel, error, onClearError }) => {
const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onCancel }) => {
const createMode = isNew;
const [editMode, setEditMode] = useState(createMode);
const [showFullBody, setShowFullBody] = useState(false);
@@ -59,20 +58,17 @@ const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onC
}, [anuncio, editMode]);
const handleEdit = () => {
if (onClearError) onClearError();
setEditMode(true);
};
const handleDelete = () => typeof onDelete === 'function' && onDelete(anuncio.announceId);
const handleCancel = () => {
if (onClearError) onClearError();
if (createMode && onCancel) return onCancel();
setEditMode(false);
};
const handleSave = () => {
if (onClearError) onClearError();
const sanitizedBody = DOMPurify.sanitize(formData.body);
formData.body = sanitizedBody;
const updated = { ...anuncio, ...formData };
@@ -133,8 +129,6 @@ const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onC
</Card.Header>
<Card.Body className="py-3">
{(editMode || createMode) && renderErrorAlert(error)}
{editMode || createMode ? (
<EditorProvider>
<Editor

View File

@@ -19,7 +19,6 @@ import { useTheme } from '../../hooks/useTheme';
import '../../css/IngresoCard.css';
import { CONSTANTS } from '../../util/constants';
import { DateParser } from '../../util/parsers/dateParser';
import { renderErrorAlert } from '../../util/alertHelpers';
import { getNowAsLocalDatetime } from '../../util/date';
import SpanishDateTimePicker from '../SpanishDateTimePicker';
@@ -38,7 +37,7 @@ const getPFP = (tipo) => {
return base + (map[tipo] || 'farmer.svg');
};
const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCancel, error, onClearError }) => {
const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCancel, fieldErrors }) => {
const createMode = isNew;
const [editMode, setEditMode] = useState(createMode);
const { theme } = useTheme();
@@ -52,6 +51,8 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
createdAt: gasto.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
const getFieldError = (field) => fieldErrors?.[field] ?? null;
useEffect(() => {
if (!editMode) {
setFormData({
@@ -71,13 +72,11 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
const handleDelete = () => typeof onDelete === 'function' && onDelete(gasto.expenseId);
const handleCancel = () => {
if (onClearError) onClearError();
if (isNew && typeof onCancel === 'function') return onCancel();
setEditMode(false);
};
const handleSave = () => {
if (onClearError) onClearError();
const newExpense = { ...gasto, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(newExpense);
if (typeof onUpdate === 'function') return onUpdate(newExpense, gasto.expenseId);
@@ -91,12 +90,18 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
<div className="d-flex flex-column">
<span className="fw-bold">
{editMode ? (
<Form.Control
className="themed-input"
size="sm"
value={formData.concept}
onChange={(e) => handleChange('concept', e.target.value.toUpperCase())}
/>
<>
<Form.Control
className="themed-input"
size="sm"
isInvalid={!!fieldErrors?.concept}
value={formData.concept}
onChange={(e) => handleChange('concept', e.target.value.toUpperCase())}
/>
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("concept")}
</Form.Control.Feedback>
</>
) : formData.concept}
</span>
<small>
@@ -132,13 +137,17 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
<Card.Body>
{(editMode || createMode) && renderErrorAlert(error)}
<Card.Text className="mb-2">
<FontAwesomeIcon icon={faMoneyBillWave} className="me-2" />
<strong>Importe:</strong>{' '}
{editMode ? (
<Form.Control className="themed-input" size="sm" type="number" step="0.01" value={formData.amount} onChange={(e) => handleChange('amount', parseFloat(e.target.value))} style={{ maxWidth: '150px', display: 'inline-block' }} />
<>
<Form.Control className="themed-input" size="sm" type="number" step="0.01" isInvalid={!!fieldErrors?.amount} value={formData.amount}
onChange={(e) => handleChange('amount', parseFloat(e.target.value))} style={{ maxWidth: '150px', display: 'inline-block' }} />
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("amount")}
</Form.Control.Feedback>
</>
) : `${formData.amount.toFixed(2)}`}
</Card.Text>
@@ -146,7 +155,13 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
<FontAwesomeIcon icon={faTruck} className="me-2" />
<strong>Proveedor:</strong>{' '}
{editMode ? (
<Form.Control className="themed-input" size="sm" type="text" value={formData.supplier} onChange={(e) => handleChange('supplier', e.target.value)} />
<>
<Form.Control className="themed-input" size="sm" type="text" isInvalid={!!fieldErrors?.supplier}
alue={formData.supplier} onChange={(e) => handleChange('supplier', e.target.value)} />
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("supplier")}
</Form.Control.Feedback>
</>
) : formData.supplier}
</Card.Text>
@@ -154,7 +169,13 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
<FontAwesomeIcon icon={faReceipt} className="me-2" />
<strong>Factura:</strong>{' '}
{editMode ? (
<Form.Control className="themed-input" size="sm" type="text" value={formData.invoice} onChange={(e) => handleChange('invoice', e.target.value)} />
<>
<Form.Control className="themed-input" size="sm" type="text" isInvalid={!!fieldErrors?.invoice}
value={formData.invoice} onChange={(e) => handleChange('invoice', e.target.value)} />
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("invoice")}
</Form.Control.Feedback>
</>
) : formData.invoice}
</Card.Text>
@@ -190,7 +211,8 @@ GastoCard.propTypes = {
onCreate: PropTypes.func,
onUpdate: PropTypes.func,
onDelete: PropTypes.func,
onCancel: PropTypes.func
onCancel: PropTypes.func,
fieldErrors: PropTypes.object
};
export default GastoCard;

View File

@@ -18,7 +18,6 @@ import { CONSTANTS } from '../../util/constants';
import '../../css/IngresoCard.css';
import { useTheme } from '../../hooks/useTheme';
import { DateParser } from '../../util/parsers/dateParser';
import { renderErrorAlert } from '../../util/alertHelpers';
import { getNowAsLocalDatetime } from '../../util/date';
import SpanishDateTimePicker from '../SpanishDateTimePicker';
@@ -50,9 +49,8 @@ const IngresoCard = ({
onCancel,
className = '',
editable = true,
error,
onClearError,
members = []
members = [],
fieldErrors
}) => {
const createMode = isNew;
const [editMode, setEditMode] = useState(createMode);
@@ -69,6 +67,8 @@ const IngresoCard = ({
createdAt: income.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
const getFieldError = (field) => fieldErrors?.[field] ?? null;
useEffect(() => {
if (!editMode) {
setFormData({
@@ -103,13 +103,11 @@ const IngresoCard = ({
setFormData(prev => ({ ...prev, [field]: value }));
const handleCancel = () => {
if (onClearError) onClearError();
if (isNew && typeof onCancel === 'function') return onCancel();
setEditMode(false);
};
const handleSave = () => {
if (onClearError) onClearError();
const newIncome = { ...income, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(newIncome);
if (typeof onUpdate === 'function') return onUpdate(newIncome, income.incomeId);
@@ -130,12 +128,18 @@ const IngresoCard = ({
<div className="d-flex flex-column">
<span className="fw-bold">
{editMode ? (
<Form.Control
className="themed-input"
size="sm"
value={formData.concept}
onChange={(e) => handleChange('concept', e.target.value.toUpperCase())}
/>
<>
<Form.Control
className="themed-input"
size="sm"
value={formData.concept}
isInvalid={!!fieldErrors?.concept}
onChange={(e) => handleChange('concept', e.target.value.toUpperCase())}
/>
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("concept")}
</Form.Control.Feedback>
</>
) : formData.concept}
</span>
@@ -161,7 +165,7 @@ const IngresoCard = ({
>
{({ closeDropdown }) => (
<>
<div className="dropdown-item d-flex align-items-center" onClick={() => { setEditMode(true); onClearError && onClearError(); closeDropdown(); }}>
<div className="dropdown-item d-flex align-items-center" onClick={() => { setEditMode(true); closeDropdown(); }}>
<FontAwesomeIcon icon={faEdit} className="me-2" />Editar
</div>
<div className="dropdown-item d-flex align-items-center text-danger" onClick={() => { handleDelete(); closeDropdown(); }}>
@@ -175,8 +179,6 @@ const IngresoCard = ({
</Card.Header>
<Card.Body>
{(editMode || createMode) && renderErrorAlert(error)}
<Card.Text className="mb-2">
<FontAwesomeIcon icon={faUser} className="me-2" />
<strong>Socio:</strong>{' '}
@@ -236,15 +238,21 @@ const IngresoCard = ({
<FontAwesomeIcon icon={faMoneyBillWave} className="me-2" />
<strong>Importe:</strong>{' '}
{editMode ? (
<Form.Control
className="themed-input"
size="sm"
type="number"
step="0.01"
value={formData.amount}
onChange={(e) => handleChange('amount', parseFloat(e.target.value))}
style={{ maxWidth: '150px', display: 'inline-block' }}
/>
<>
<Form.Control
className="themed-input"
size="sm"
type="number"
step="0.01"
isInvalid={!!fieldErrors?.amount}
value={formData.amount}
onChange={(e) => handleChange('amount', parseFloat(e.target.value))}
style={{ maxWidth: '150px', display: 'inline-block' }}
/>
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("amount")}
</Form.Control.Feedback>
</>
) : `${formData.amount.toFixed(2)}`}
</Card.Text>
@@ -299,9 +307,8 @@ IngresoCard.propTypes = {
onCancel: PropTypes.func,
className: PropTypes.string,
editable: PropTypes.bool,
error: PropTypes.string,
onClearError: PropTypes.func,
members: PropTypes.array
members: PropTypes.array,
fieldErrors: PropTypes.object
};
export default IngresoCard;

View File

@@ -20,7 +20,6 @@ import TipoSocioDropdown from './TipoSocioDropdown';
import { getNowAsLocalDatetime } from '../../util/date';
import { generateSecurePassword } from '../../util/passwordGenerator';
import { DateParser } from '../../util/parsers/dateParser';
import { renderErrorAlert } from '../../util/alertHelpers';
import { useDataContext } from "../../hooks/useDataContext";
import SpanishDateTimePicker from '../SpanishDateTimePicker';
@@ -91,7 +90,7 @@ const getPFP = (tipo) => {
const MotionCard = _motion.create(Card);
const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCancel, onViewIncomes, error, onClearError, positionIfWaitlist }) => {
const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCancel, onViewIncomes, positionIfWaitlist, fieldErrors }) => {
const createMode = isNew;
const [editMode, setEditMode] = useState(isNew);
const [showPassword, setShowPassword] = useState(false);
@@ -116,6 +115,8 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
password: createMode && !editMode ? generateSecurePassword() : null,
});
const getFieldError = (field) => fieldErrors?.[field] ?? null;
useEffect(() => {
if (!editMode) {
setFormData({
@@ -144,7 +145,7 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
try {
if (!(createMode || editMode)) return;
const latestNumber = await getData("http://localhost:8081/v2/huertos/users/latest-number");
const latestNumber = await getData("http://localhost:8081/v2/huertos/users/latest-number", {}, false);
const nuevoNumero = latestNumber + 1;
setLatestNumber(nuevoNumero);
@@ -162,20 +163,17 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
}, [createMode, editMode, getData]);
const handleEdit = () => {
if (onClearError) onClearError();
setEditMode(true);
};
const handleDelete = () => typeof onDelete === "function" && onDelete(identity.user.userId);
const handleCancel = () => {
if (onClearError) onClearError();
if (isNew && typeof onCancel === 'function') return onCancel();
setEditMode(false);
};
const handleSave = () => {
if (onClearError) onClearError();
const newSocio = { ...identity, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(newSocio);
if (typeof onUpdate === 'function') return onUpdate(newSocio, identity.user.userId);
@@ -224,7 +222,13 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
<div className='d-flex flex-column gap-1'>
<Card.Title className="m-0">
{editMode ? (
<Form.Control className="themed-input" size="sm" value={formData.displayName} onChange={(e) => handleChange('displayName', e.target.value)} style={{ maxWidth: '220px' }} />
<>
<Form.Control className="themed-input" size="sm" isInvalid={!!fieldErrors?.displayName}
value={formData.displayName} onChange={(e) => handleChange('displayName', e.target.value)} style={{ maxWidth: '220px' }} />
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("displayName")}
</Form.Control.Feedback>
</>
) : formData.displayName}
</Card.Title>
{editMode ? (
@@ -262,8 +266,6 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
</Card.Header>
<Card.Body>
{(editMode || createMode) && renderErrorAlert(error)}
<ListGroup className="mt-2 border-1 rounded-3 shadow-sm">
{[{
label: 'DNI', clazz: '', icon: faIdCard, value: formData.dni, field: 'dni', type: 'text', maxWidth: '180px'
@@ -279,7 +281,13 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
<ListGroup.Item key={field} className="d-flex justify-content-between align-items-center">
<span><FontAwesomeIcon icon={icon} className="me-2" />{label}</span>
{editMode ? (
<Form.Control className="themed-input" size="sm" type={type} value={value} onChange={(e) => handleChange(field, e.target.value)} style={{ maxWidth }} />
<>
<Form.Control className="themed-input" size="sm" type={type} value={value} isInvalid={!!fieldErrors?.[field]}
onChange={(e) => handleChange(field, e.target.value)} style={{ maxWidth }} />
<Form.Control.Feedback type="invalid" as="span">
{getFieldError(`${field}`)}
</Form.Control.Feedback>
</>
) : (
<strong className={clazz}>{parseNull(value)}</strong>
)}
@@ -289,14 +297,20 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
<ListGroup.Item className="d-flex justify-content-between align-items-center">
<span><FontAwesomeIcon icon={faKey} className="me-2" />CONTRASEÑA</span>
<div className="d-flex align-items-center gap-2" style={{ maxWidth: 'fit-content' }}>
<Form.Control
className="themed-input"
size="sm"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => handleChange('password', e.target.value)}
style={{ maxWidth: '200px' }}
/>
<>
<Form.Control
className="themed-input"
size="sm"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => handleChange('password', e.target.value)}
style={{ maxWidth: '200px' }}
isInvalid={!!fieldErrors?.password}
/>
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("password")}
</Form.Control.Feedback>
</>
<Button
size="sm"
variant="outline-secondary"
@@ -329,7 +343,13 @@ const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCa
)}
</Card.Subtitle>
{editMode ? (
<Form.Control className="themed-input" as="textarea" rows={3} value={formData.notes} onChange={(e) => handleChange('notes', e.target.value)} />
<>
<Form.Control className="themed-input" as="textarea" rows={3} value={formData.notes} isInvalid={!!fieldErrors?.notes}
onChange={(e) => handleChange('notes', e.target.value)} />
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("notes")}
</Form.Control.Feedback>
</>
) : (
<Card.Text>{parseNull(formData.notes)}</Card.Text>
)}
@@ -355,9 +375,9 @@ SocioCard.propTypes = {
onUpdate: PropTypes.func,
onDelete: PropTypes.func,
onViewIncomes: PropTypes.func,
error: PropTypes.string,
onClearError: PropTypes.func,
positionIfWaitlist: PropTypes.number
positionIfWaitlist: PropTypes.number,
fieldErrors: PropTypes.object
};
export default SocioCard;

View File

@@ -1,10 +1,9 @@
import { useState, useEffect, useRef } from 'react';
import { Form, Row, Col, Button } from 'react-bootstrap';
import { useDataContext } from '../../hooks/useDataContext';
import { Alert } from 'react-bootstrap';
import PropTypes from 'prop-types';
const NewUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
const NewUserForm = ({ onSubmit, userType, plotNumber, fieldErrors }) => {
const { getData } = useDataContext();
const fetchedOnce = useRef(false);
@@ -28,7 +27,7 @@ const NewUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
fetchedOnce.current = true;
try {
const latestNumber = await getData("http://localhost:8081/v2/huertos/users/latest-number");
const latestNumber = await getData("http://localhost:8081/v2/huertos/users/latest-number", {}, false);
setForm((prev) => ({
...prev,
memberNumber: latestNumber + 1
@@ -46,12 +45,13 @@ const NewUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
const trimmedName = form.displayName?.trim() ?? "";
const nuevoUsername = trimmedName
? trimmedName.split(' ')[0].toLowerCase() : "";
? trimmedName.split(' ')[0].toLowerCase() + String(form.memberNumber) : "";
if (form.username !== nuevoUsername) {
setForm(prev => ({ ...prev, username: nuevoUsername }));
}
}, [form.memberNumber, form.displayName, form.username]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form.displayName]);
const handleChange = (e) => {
const { name, value, type } = e.target;
@@ -72,10 +72,10 @@ const NewUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
if (onSubmit) onSubmit(form);
};
const getFieldError = (field) => fieldErrors?.[field] ?? null;
return (
<>
{errors.general && <Alert variant="danger" className="my-2">{errors.general}</Alert>}
<Form onSubmit={handleSubmit} className="p-3 px-md-4">
<Row className="gy-3">
@@ -100,10 +100,10 @@ const NewUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
onChange={handleChange}
required={required}
maxLength={maxLength}
isInvalid={!!errors[name]}
isInvalid={!!getFieldError(name)}
/>
<Form.Control.Feedback type="invalid">
{errors[name]}
{getFieldError(name)}
</Form.Control.Feedback>
</Form.Group>
</Col>

View File

@@ -40,42 +40,93 @@ const getPFP = (tipo) => {
const renderDescripcionSolicitud = (data, onProfile) => {
const m = data.metadata;
switch (data.type) {
case 0:
return data.status === 1
? 'Se ha aceptado esta solicitud de alta.'
: `${m?.displayName ?? 'Alguien'} quiere darse de alta.`;
if (onProfile) {
switch (data.type) {
case 0: // Alta
return data.status === 1
? 'Te han dado de alta correctamente.'
: 'Has solicitado darte de alta.';
case 1:
return onProfile
? 'Has solicitado darte de baja.'
: `${data.name ?? 'Alguien'} quiere darse de baja.`;
case 1: // Baja
return data.status === 1
? 'Tu baja ha sido procesada.'
: 'Has solicitado darte de baja.';
case 2:
if (onProfile) {
return [
'Has solicitado añadir un colaborador.',
'Tu solicitud de colaborador ha sido aceptada.',
'Tu solicitud de colaborador ha sido rechazada.'
][data.status] ?? 'Solicitud de colaborador.';
}
return data.status === 0
? `${data.name ?? 'Alguien'} quiere añadir un colaborador.`
: `La solicitud de colaborador ha sido ${
data.status === 1 ? 'aceptada' : 'rechazada'
}.`;
case 2: // Añadir colaborador
return data.status === 1
? 'Tu solicitud de añadir colaborador ha sido aceptada.'
: data.status === 2
? 'Tu solicitud de añadir colaborador ha sido rechazada.'
: 'Has solicitado añadir un colaborador.';
case 3:
return `${data.name ?? 'Alguien'} quiere quitar su colaborador.`;
case 3: // Quitar colaborador
return data.status === 1
? 'Tu solicitud de quitar colaborador ha sido aceptada.'
: data.status === 2
? 'Tu solicitud de quitar colaborador ha sido rechazada.'
: 'Has solicitado quitar tu colaborador.';
case 4:
return `${data.name ?? 'Alguien'} quiere una parcela en el invernadero.`;
case 4: // Añadir parcela invernadero
return data.status === 1
? 'Tu solicitud de añadir parcela en invernadero ha sido aceptada.'
: data.status === 2
? 'Tu solicitud de añadir parcela en invernadero ha sido rechazada.'
: 'Has solicitado añadir una parcela en invernadero.';
case 5:
return `${data.name ?? 'Alguien'} quiere dejar su parcela del invernadero.`;
case 5: // Dejar parcela invernadero
return data.status === 1
? 'Tu solicitud de dejar la parcela en invernadero ha sido aceptada.'
: data.status === 2
? 'Tu solicitud de dejar la parcela en invernadero ha sido rechazada.'
: 'Has solicitado dejar tu parcela en invernadero.';
default:
return 'Tipo de solicitud desconocido.';
default:
return 'Solicitud desconocida.';
}
} else {
// Para administradores o vista general
switch (data.type) {
case 0:
return data.status === 1
? `Se ha aceptado la solicitud de alta de ${m?.displayName ?? data.name}.`
: `${m?.displayName ?? data.name} quiere darse de alta.`;
case 1:
return data.status === 1
? `Se ha procesado la baja de ${m?.displayName ?? data.name}.`
: `${m?.displayName ?? data.name} quiere darse de baja.`;
case 2:
return data.status === 1
? `La solicitud de añadir colaborador de ${m?.displayName ?? data.name} ha sido aceptada.`
: data.status === 2
? `La solicitud de añadir colaborador de ${m?.displayName ?? data.name} ha sido rechazada.`
: `${m?.displayName ?? data.name} quiere añadir un colaborador.`;
case 3:
return data.status === 1
? `La solicitud de quitar colaborador de ${m?.displayName ?? data.name} ha sido aceptada.`
: data.status === 2
? `La solicitud de quitar colaborador de ${m?.displayName ?? data.name} ha sido rechazada.`
: `${m?.displayName ?? data.name} quiere quitar su colaborador.`;
case 4:
return data.status === 1
? `La solicitud de añadir parcela de ${m?.displayName ?? data.name} ha sido aceptada.`
: data.status === 2
? `La solicitud de añadir parcela de ${m?.displayName ?? data.name} ha sido rechazada.`
: `${m?.displayName ?? data.name} quiere una parcela en invernadero.`;
case 5:
return data.status === 1
? `La solicitud de dejar parcela de ${m?.displayName ?? data.name} ha sido aceptada.`
: data.status === 2
? `La solicitud de dejar parcela de ${m?.displayName ?? data.name} ha sido rechazada.`
: `${m?.displayName ?? data.name} quiere dejar su parcela del invernadero.`;
default:
return 'Tipo de solicitud desconocido.';
}
}
};
@@ -148,7 +199,7 @@ const SolicitudCard = ({
</ListGroup.Item>
</ListGroup>
{m && (
{m && !onProfile && (
<>
<Card.Subtitle className="card-subtitle mt-3 mb-2">
Datos asociados a la solicitud

View File

@@ -4,8 +4,8 @@ import { useData } from "../hooks/useData";
export const DataContext = createContext();
export const DataProvider = ({ config, children }) => {
const data = useData(config);
export const DataProvider = ({ config, onError, children }) => {
const data = useData(config, onError);
return (
<DataContext.Provider value={data}>

View File

@@ -0,0 +1,36 @@
import { createContext, useState, useContext } from 'react';
import NotificationModal from '../components/NotificationModal';
const ErrorContext = createContext();
export const ErrorProvider = ({ children }) => {
const [error, setError] = useState(null);
const showError = (err) => {
setError({
title: err.status ? `Error ${err.status}` : "Error",
message: err.message,
variant: 'danger'
});
};
const closeError = () => setError(null);
return (
<ErrorContext.Provider value={{ showError }}>
{children}
{error && (
<NotificationModal
show={true}
onClose={closeError}
title={error.title}
message={error.message}
variant='danger'
buttons={[{ label: "Aceptar", variant: "danger", onClick: closeError }]}
/>
)}
</ErrorContext.Provider>
);
};
export const useError = () => useContext(ErrorContext);

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import axios from "axios";
export const useData = (config) => {
export const useData = (config, onError) => {
const [data, setData] = useState(null);
const [dataLoading, setLoading] = useState(true);
const [dataError, setError] = useState(null);
@@ -13,10 +13,55 @@ export const useData = (config) => {
}
}, [config]);
const getAuthHeaders = () => ({
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("token")}`,
});
const getAuthHeaders = () => {
const token = localStorage.getItem("token");
if (!token) return { "Content-Type": "application/json" };
return {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
};
};
const handleAxiosError = (err) => {
if (err.response && err.response.data) {
const data = err.response.data;
if (data.status === 422 && data.errors) {
return {
status: 422,
errors: data.errors,
path: data.path ?? null,
timestamp: data.timestamp ?? null,
};
}
return {
status: data.status ?? err.response.status,
error: data.error ?? null,
message: data.message ?? err.response.statusText ?? "Error desconocido",
path: data.path ?? null,
timestamp: data.timestamp ?? null,
};
}
if (err.request) {
return {
status: null,
error: "Network Error",
message: "No se pudo conectar al servidor",
path: null,
timestamp: new Date().toISOString(),
};
}
return {
status: null,
error: "Client Error",
message: err.message || "Error desconocido",
path: null,
timestamp: new Date().toISOString(),
};
};
const fetchData = useCallback(async () => {
const current = configRef.current;
@@ -32,69 +77,59 @@ export const useData = (config) => {
});
setData(response.data);
} catch (err) {
setError(err.response?.data);
const error = handleAxiosError(err);
setError(error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (config?.baseUrl) {
fetchData();
}
if (config?.baseUrl) fetchData();
}, [config, fetchData]);
const getData = async (url, params = {}) => {
const response = await axios.get(url, {
headers: getAuthHeaders(),
params,
});
return response.data;
const requestWrapper = async (method, endpoint, payload = null, refresh = false) => {
try {
const headers = getAuthHeaders();
const cfg = { headers };
let response;
if (method === "get") {
if (payload) cfg.params = payload;
response = await axios.get(endpoint, cfg);
} else if (method === "delete") {
if (payload) cfg.data = payload;
response = await axios.delete(endpoint, cfg);
} else {
response = await axios[method](endpoint, payload, cfg);
}
if (refresh) await fetchData();
return response.data;
} catch (err) {
const error = handleAxiosError(err);
if (error.status !== 422 && onError) {
onError(error);
}
setError(error);
throw error;
}
};
const postData = async (endpoint, payload) => {
const headers = {
Authorization: `Bearer ${localStorage.getItem("token")}`,
...(payload instanceof FormData ? {} : { "Content-Type": "application/json" }),
};
const response = await axios.post(endpoint, payload, { headers });
await fetchData();
return response.data;
};
const putData = async (endpoint, payload) => {
const response = await axios.put(endpoint, payload, {
headers: getAuthHeaders(),
});
await fetchData();
return response.data;
};
const deleteData = async (endpoint) => {
const response = await axios.delete(endpoint, {
headers: getAuthHeaders(),
});
await fetchData();
return response.data;
};
const deleteDataWithBody = async (endpoint, payload) => {
const response = await axios.delete(endpoint, {
headers: getAuthHeaders(),
data: payload,
});
await fetchData();
return response.data;
};
const clearError = () => setError(null);
return {
data,
dataLoading,
dataError,
getData,
postData,
putData,
deleteData,
deleteDataWithBody,
clearError,
getData: (url, params, refresh = true) => requestWrapper("get", url, params, refresh),
postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh),
putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh),
deleteData: (url, refresh = true) => requestWrapper("delete", url, null, refresh),
deleteDataWithBody: (url, body, refresh = true) => requestWrapper("delete", url, body, refresh)
};
};

View File

@@ -14,16 +14,19 @@ import 'bootstrap/dist/js/bootstrap.bundle.min.js'
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import './css/index.css'
import { ErrorProvider } from './context/ErrorContext.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<ConfigProvider>
<ThemeProvider>
<AuthProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AuthProvider>
<ErrorProvider>
<AuthProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AuthProvider>
</ErrorProvider>
</ThemeProvider>
</ConfigProvider>
</StrictMode>

View File

@@ -12,15 +12,16 @@ import PaginatedCardGrid from '../components/PaginatedCardGrid';
import AnuncioCard from '../components/Anuncios/AnuncioCard';
import AnunciosFilter from '../components/Anuncios/AnunciosFilter';
import { errorParser } from '../util/parsers/errorParser';
import CustomModal from '../components/CustomModal';
import { Button } from 'react-bootstrap';
import { EditorProvider } from 'react-simple-wysiwyg';
import { useError } from '../context/ErrorContext';
const PAGE_SIZE = 10;
const Anuncios = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>;
@@ -30,17 +31,16 @@ const Anuncios = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError} >
<AnunciosContent reqConfig={reqConfig} />
</DataProvider>
);
};
const AnunciosContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, postData, putData, deleteData } = useDataContext();
const { data, dataLoading, postData, putData, deleteData } = useDataContext();
const [creatingAnuncio, setCreatingAnuncio] = useState(false);
const [tempAnuncio, setTempAnuncio] = useState(null);
const [error, setError] = useState(null);
const [deleteTargetId, setDeleteTargetId] = useState(null);
const {
@@ -103,22 +103,16 @@ const AnunciosContent = ({ reqConfig }) => {
const handleCreateSubmit = async (nuevo) => {
try {
await postData(reqConfig.baseUrl, nuevo);
setError(null);
setCreatingAnuncio(false);
setTempAnuncio(null);
// eslint-disable-next-line no-unused-vars
} catch (err) {
setTempAnuncio({ ...nuevo });
setError(errorParser(err));
}
};
const handleEditSubmit = async (editado, id) => {
try {
await putData(`${reqConfig.baseUrl}/${id}`, editado);
setError(null);
} catch (err) {
setError(errorParser(err));
}
await putData(`${reqConfig.baseUrl}/${id}`, editado);
};
const handleDelete = async (id) => {
@@ -126,7 +120,6 @@ const AnunciosContent = ({ reqConfig }) => {
};
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return (
<CustomContainer>
@@ -154,8 +147,6 @@ const AnunciosContent = ({ reqConfig }) => {
isNew
onCreate={handleCreateSubmit}
onCancel={handleCancelCreate}
error={error}
onClearError={() => setError(null)}
/>
</EditorProvider>
)}
@@ -165,8 +156,6 @@ const AnunciosContent = ({ reqConfig }) => {
anuncio={{...anuncio, idx: idx}}
onUpdate={(a, id) => handleEditSubmit(a, id)}
onDelete={() => handleDelete(anuncio.announceId)}
error={error}
onClearError={() => setError(null)}
/>
)}
/>
@@ -187,7 +176,7 @@ const AnunciosContent = ({ reqConfig }) => {
setSearchTerm("");
setDeleteTargetId(null);
} catch (err) {
setError(errorParser(err));
console.error(err);
}
}}
>

View File

@@ -6,9 +6,11 @@ import CustomContainer from '../components/CustomContainer';
import ContentWrapper from '../components/ContentWrapper';
import LoadingIcon from '../components/LoadingIcon';
import BalanceReport from '../components/Balance/BalanceReport';
import { useError } from '../context/ErrorContext';
const Balance = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading || !config) return <p className="text-center my-5"><LoadingIcon /></p>;
@@ -17,17 +19,16 @@ const Balance = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError}>
<BalanceContent />
</DataProvider>
);
};
const BalanceContent = () => {
const { data, dataLoading, dataError } = useDataContext();
const { data, dataLoading } = useDataContext();
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
if (!data || !data.id) return <p className="text-center my-5">No se encontró el balance.</p>;
return (

View File

@@ -11,9 +11,11 @@ import IfRole from '../components/Auth/IfRole.jsx';
import { CONSTANTS } from '../util/constants.js';
import CustomModal from '../components/CustomModal.jsx';
import { Button } from 'react-bootstrap';
import { useError } from '../context/ErrorContext';
const Documentacion = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>;
@@ -23,14 +25,14 @@ const Documentacion = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError}>
<DocumentacionContent reqConfig={reqConfig} />
</DataProvider>
);
};
const DocumentacionContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, postData, deleteDataWithBody } = useDataContext();
const { data, dataLoading, postData, deleteDataWithBody } = useDataContext();
const [deleteTarget, setDeleteTarget] = useState(null);
const fileUploadRef = useRef();
@@ -74,7 +76,6 @@ const DocumentacionContent = ({ reqConfig }) => {
{dataLoading ? (<LoadingIcon />) : (
<div className="mt-4 d-flex flex-wrap gap-3 justify-content-start">
{dataError && <p className="text-danger">Error al cargar los archivos.</p>}
{data?.length === 0 && <p>No hay documentos todavía.</p>}
{data?.filter(file => file.context === CONSTANTS.CONTEXT_HUERTOS)
.map((file, idx) => (

View File

@@ -16,14 +16,15 @@ import { GastosPDF } from '../components/Gastos/GastosPDF';
import '../css/Ingresos.css';
import { CONSTANTS } from '../util/constants';
import { errorParser } from '../util/parsers/errorParser';
import CustomModal from '../components/CustomModal';
import { Button } from 'react-bootstrap';
import { useError } from '../context/ErrorContext';
const PAGE_SIZE = 10;
const Gastos = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>;
@@ -33,19 +34,19 @@ const Gastos = () => {
};
return (
<DataProvider config={reqConfig}>
<GastosContent reqConfig={reqConfig} />
<DataProvider config={reqConfig} onError={showError}>
<GastosContent reqConfig={reqConfig}/>
</DataProvider>
);
};
const GastosContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, postData, putData, deleteData } = useDataContext();
const { data, dataLoading, postData, putData, deleteData } = useDataContext();
const [showPDFModal, setShowPDFModal] = useState(false);
const [creatingGasto, setCreatingGasto] = useState(false);
const [tempGasto, setTempGasto] = useState(null);
const [error, setError] = useState(null);
const [deleteTargetId, setDeleteTargetId] = useState(null);
const [fieldErrors, setFieldErrors] = useState(null);
const {
filtered,
@@ -94,21 +95,24 @@ const GastosContent = ({ reqConfig }) => {
const handleCreateSubmit = async (nuevo) => {
try {
await postData(reqConfig.baseUrl, nuevo);
setError(null);
setCreatingGasto(false);
setTempGasto(null);
setFieldErrors(null);
} catch (err) {
setTempGasto({ ...nuevo });
setError(errorParser(err));
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
};
const handleEditSubmit = async (editado, id) => {
try {
await putData(`${reqConfig.baseUrl}/${id}`, editado);
setError(null);
} catch (err) {
setError(errorParser(err));
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
};
@@ -119,11 +123,10 @@ const GastosContent = ({ reqConfig }) => {
const handleCancelCreate = () => {
setCreatingGasto(false);
setTempGasto(null);
setError(null);
setFieldErrors(null);
};
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return (
<CustomContainer>
@@ -151,8 +154,7 @@ const GastosContent = ({ reqConfig }) => {
isNew
onCreate={handleCreateSubmit}
onCancel={handleCancelCreate}
error={error}
onClearError={() => setError(null)}
fieldErrors={fieldErrors}
/>
)}
renderCard={(gasto) => (
@@ -161,8 +163,7 @@ const GastosContent = ({ reqConfig }) => {
gasto={gasto}
onUpdate={handleEditSubmit}
onDelete={handleDelete}
error={error}
onClearError={() => setError(null)}
fieldErrors={fieldErrors}
/>
)}
/>
@@ -187,7 +188,7 @@ const GastosContent = ({ reqConfig }) => {
setSearchTerm("");
setDeleteTargetId(null);
} catch (err) {
setError(errorParser(err));
console.error(err);
}
}}
>

View File

@@ -15,16 +15,17 @@ import IngresoCard from '../components/Ingresos/IngresoCard';
import IngresosFilter from '../components/Ingresos/IngresosFilter';
import { IngresosPDF } from '../components/Ingresos/IngresosPDF';
import { CONSTANTS } from '../util/constants';
import { errorParser } from '../util/parsers/errorParser';
import '../css/Ingresos.css';
import CustomModal from '../components/CustomModal';
import { Button } from 'react-bootstrap';
import { useError } from '../context/ErrorContext';
const PAGE_SIZE = 10;
const Ingresos = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>;
@@ -36,22 +37,22 @@ const Ingresos = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError}>
<IngresosContent reqConfig={reqConfig} />
</DataProvider>
);
};
const IngresosContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, postData, putData, deleteData } = useDataContext();
const { data, dataLoading, postData, putData, deleteData } = useDataContext();
const [showPDFModal, setShowPDFModal] = useState(false);
const [creatingIngreso, setCreatingIngreso] = useState(false);
const [tempIngreso, setTempIngreso] = useState(null);
const [error, setError] = useState(null);
const [deleteTargetId, setDeleteTargetId] = useState(null);
const [fieldErrors, setFieldErrors] = useState(null);
const members = data
? Array.from(
? Array.from(
new Map(
data.map(i => [i.memberNumber, {
memberNumber: i.memberNumber,
@@ -60,7 +61,7 @@ const IngresosContent = ({ reqConfig }) => {
}])
).values()
).sort((a, b) => a.memberNumber - b.memberNumber)
: [];
: [];
const {
filtered,
@@ -93,8 +94,8 @@ const IngresosContent = ({ reqConfig }) => {
searchFn: (ingreso, term) => {
const normalized = term.toLowerCase();
return ingreso.concept?.toLowerCase().includes(normalized) ||
String(ingreso.memberNumber).includes(normalized) ||
ingreso.displayName?.toLowerCase().includes(normalized);
String(ingreso.memberNumber).includes(normalized) ||
ingreso.displayName?.toLowerCase().includes(normalized);
}
});
@@ -116,37 +117,38 @@ const IngresosContent = ({ reqConfig }) => {
const handleCancelCreate = () => {
setCreatingIngreso(false);
setTempIngreso(null);
setError(null);
setFieldErrors(null);
};
const handleCreateSubmit = async (nuevo) => {
try {
await postData(reqConfig.rawUrl, nuevo);
setError(null);
setCreatingIngreso(false);
setTempIngreso(null);
setFieldErrors(null);
} catch (err) {
setTempIngreso({ ...nuevo });
setError(errorParser(err));
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
};
const handleEditSubmit = async (editado, id) => {
try {
await putData(`${reqConfig.rawUrl}/${id}`, editado);
setError(null);
} catch (err) {
setError(errorParser(err));
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
};
const handleDelete = async (id) => {
setDeleteTargetId(id);
};
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return (
<CustomContainer>
@@ -173,9 +175,8 @@ const IngresosContent = ({ reqConfig }) => {
isNew
onCreate={handleCreateSubmit}
onCancel={handleCancelCreate}
error={error}
onClearError={() => setError(null)}
members={members}
fieldErrors={fieldErrors}
/>
)}
renderCard={(income) => (
@@ -184,8 +185,7 @@ const IngresosContent = ({ reqConfig }) => {
income={income}
onUpdate={(data, id) => handleEditSubmit(data, id)}
onDelete={() => handleDelete(income.incomeId)}
error={error}
onClearError={() => setError(null)}
fieldErrors={fieldErrors}
/>
)}
/>
@@ -210,7 +210,7 @@ const IngresosContent = ({ reqConfig }) => {
setSearchTerm("");
setDeleteTargetId(null);
} catch (err) {
setError(errorParser(err));
console.log(err);
}
}}
>

View File

@@ -16,9 +16,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPencil } from '@fortawesome/free-solid-svg-icons';
import IfNotAuthenticated from '../components/Auth/IfNotAuthenticated';
import NotificationModal from '../components/NotificationModal';
import { useError } from '../context/ErrorContext';
const ListaEspera = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>;
@@ -29,7 +31,7 @@ const ListaEspera = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError}>
<ListaEsperaContent reqConfig={reqConfig} />
</DataProvider>
);
@@ -37,7 +39,7 @@ const ListaEspera = () => {
const ListaEsperaContent = ({ reqConfig }) => {
const { authStatus } = useAuth();
const { data, dataLoading, dataError, postData } = useDataContext();
const { data, dataLoading, postData } = useDataContext();
const [showWelcomeModal, setShowWelcomeModal] = useState(false);
const [showNewUserFormModal, setShowNewUserFormModal] = useState(false);
@@ -101,7 +103,6 @@ const ListaEsperaContent = ({ reqConfig }) => {
};
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return (
<CustomContainer>

View File

@@ -31,6 +31,7 @@ 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';
@@ -53,6 +54,7 @@ const getPFP = (tipo) => {
const Perfil = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading || !config) return <p className="text-center my-5"><LoadingIcon /></p>;
@@ -75,14 +77,14 @@ const Perfil = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError}>
<PerfilContent config={reqConfig} />
</DataProvider>
);
};
const PerfilContent = ({ config }) => {
const { data, dataLoading, dataError, postData } = useDataContext();
const { data, dataLoading, postData } = useDataContext();
const { logout } = useAuth();
const identity = JSON.parse(localStorage.getItem("identity"));
@@ -99,6 +101,7 @@ const PerfilContent = ({ config }) => {
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,
@@ -113,6 +116,7 @@ const PerfilContent = ({ config }) => {
};
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";
@@ -131,21 +135,16 @@ const PerfilContent = ({ config }) => {
onClick: closeFeedback
});
} catch (err) {
setFeedbackModal({
title: 'Error',
message: err.message,
variant: 'danger',
onClick: closeFeedback
});
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
};
const [validationErrors, setValidationErrors] = useState({});
const [newPasswordData, setNewPasswordData] = useState({
currentPassword: "",
oldPassword: "",
newPassword: "",
confirmNewPassword: ""
serviceId: identity.account.serviceId
});
const [showOld, setShowOld] = useState(false);
@@ -157,28 +156,21 @@ const PerfilContent = ({ config }) => {
...newPasswordData,
[e.target.name]: e.target.value
});
setFieldErrors(null);
}
const handleChangePassword = async () => {
try {
const validOldPassword = await postData(config.loginValidateUrl, {
userId: identity.user.userId,
password: newPasswordData.currentPassword
});
if (!validOldPassword.valid) throw new Error("La contraseña actual es incorrecta.");
if (newPasswordData.newPassword !== newPasswordData.confirmNewPassword) throw new Error("Las contraseñas no coinciden.");
if (newPasswordData.newPassword.length < 8) throw new Error("La nueva contraseña debe tener al menos 8 caracteres.");
const response = await postData(config.changePasswordUrl, {
userId: identity.user.userId,
newPassword: newPasswordData.newPassword
await postData(config.changePasswordUrl, {
oldPassword: newPasswordData.oldPassword,
newPassword: newPasswordData.newPassword,
serviceId: identity.user.account.serviceId
});
if (!response) throw new Error("Error al cambiar la contraseña.");
setNewPasswordData({
currentPassword: "",
oldPassword: "",
newPassword: "",
confirmNewPassword: ""
serviceId: identity.user.account.serviceId
});
setFeedbackModal({
@@ -191,14 +183,13 @@ const PerfilContent = ({ config }) => {
}
});
} catch (err) {
setFeedbackModal({
title: 'Error',
message: err.message,
variant: 'danger',
onClick: closeFeedback
});
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
}
};
const getFieldError = (field) => fieldErrors?.[field] ?? null;
const mappedRequests = myRequests.map(r => ({
...r,
@@ -208,7 +199,6 @@ const PerfilContent = ({ config }) => {
}));
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return (
<CustomContainer>
@@ -243,6 +233,7 @@ const PerfilContent = ({ config }) => {
{!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
@@ -334,7 +325,11 @@ const PerfilContent = ({ config }) => {
placeholder=""
name="currentPassword"
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"
@@ -356,7 +351,11 @@ const PerfilContent = ({ config }) => {
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"
@@ -396,7 +395,6 @@ const PerfilContent = ({ config }) => {
newPasswordData.newPassword === '' || newPasswordData.confirmNewPassword === '' ||
newPasswordData.currentPassword === ''
}
onClick={(e) => { e.preventDefault(); handleChangePassword(); }}
type='submit'
variant="warning"
style={{ width: 'fit-content' }}
@@ -413,17 +411,16 @@ const PerfilContent = ({ config }) => {
show={showAddCollaboratorModal}
onClose={() => {
setShowAddCollaboratorModal(false);
setValidationErrors({});
setFieldErrors(null);
}}
>
<NewUserForm
userType={3}
plotNumber={identity.metadata.plotNumber}
errors={validationErrors}
fieldErrors={fieldErrors}
onSubmit={async (formData) => {
console.log("🚀 Enviando al backend...", formData); // Debug
try {
setValidationErrors({});
setFieldErrors(null);
await postData(config.requestUrl, {
type: CONSTANTS.REQUEST_TYPE_ADD_COLLABORATOR,
@@ -449,9 +446,10 @@ const PerfilContent = ({ config }) => {
onClick: closeFeedback
});
} catch (error) {
console.error("💥 Error al añadir:", error);
setValidationErrors({ general: error.message || "Ha ocurrido un error al procesar la solicitud." });
} catch (err) {
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
}}
/>
@@ -483,13 +481,8 @@ const PerfilContent = ({ config }) => {
onClick: closeFeedback
});
setShowRemoveCollaboratorModal(false);
} catch (err) {
setFeedbackModal({
title: "Error",
message: err.message,
variant: "danger",
onClick: closeFeedback
});
} catch (error) {
console.error(error);
}
}}
>

View File

@@ -15,15 +15,16 @@ import { SociosPDF } from '../components/Socios/SociosPDF';
import PaginatedCardGrid from '../components/PaginatedCardGrid';
import CustomModal from '../components/CustomModal';
import IngresoCard from '../components/Ingresos/IngresoCard';
import { errorParser } from '../util/parsers/errorParser';
import '../css/Socios.css';
import { Button } from 'react-bootstrap';
import { useError } from '../context/ErrorContext';
const PAGE_SIZE = 10;
const Socios = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading || !config) return <p className="text-center my-5"><LoadingIcon /></p>;
@@ -35,14 +36,14 @@ const Socios = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError}>
<SociosContent reqConfig={reqConfig} />
</DataProvider>
);
};
const SociosContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, getData, postData, putData, deleteData } = useDataContext();
const { data, dataLoading, getData, postData, putData, deleteData } = useDataContext();
const [showPDFModal, setShowPDFModal] = useState(false);
const [creatingSocio, setCreatingSocio] = useState(false);
@@ -51,9 +52,9 @@ const SociosContent = ({ reqConfig }) => {
const [selectedMemberNumber, setSelectedMemberNumber] = useState(null);
const [incomes, setIncomes] = useState([]);
const [incomesLoading, setIncomesLoading] = useState(false);
const [incomesError, setIncomesError] = useState(null);
const [error, setError] = useState(null);
const [deleteTargetId, setDeleteTargetId] = useState(null);
const [fieldErrors, setFieldErrors] = useState(null);
const [incomeFieldErrors, setIncomeFieldErrors] = useState(null);
const {
filtered,
@@ -122,28 +123,31 @@ const SociosContent = ({ reqConfig }) => {
const handleCancelCreate = () => {
setCreatingSocio(false);
setTempSocio(null);
setError(null);
setFieldErrors(null);
};
const handleCreateSubmit = async (newSocio) => {
try {
newSocio.userName = newSocio.displayName.split(" ")[0].toLowerCase() + newSocio.memberNumber;
await postData(reqConfig.baseUrl, newSocio);
setError(null);
setCreatingSocio(false);
setTempSocio(null);
setFieldErrors(null);
} catch (err) {
setTempSocio({ ...newSocio });
setError(errorParser(err));
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
};
const handleEditSubmit = async (updatedSocio, userId) => {
try {
await putData(`${reqConfig.baseUrl}/${userId}`, updatedSocio);
setError(null);
} catch (err) {
setError(errorParser(err));
if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
}
};
@@ -156,14 +160,14 @@ const SociosContent = ({ reqConfig }) => {
setShowIncomesModal(true);
setIncomes([]);
setIncomesLoading(true);
setIncomesError(null);
setIncomeFieldErrors(null);
try {
const url = reqConfig.incomesUrl.replace(":memberNumber", memberNumber);
const res = await getData(url);
setIncomes(res);
} catch (err) {
setIncomesError(err.message);
console.error(err);
} finally {
setIncomesLoading(false);
}
@@ -174,7 +178,9 @@ const SociosContent = ({ reqConfig }) => {
await putData(`${reqConfig.rawIncomesUrl}/${editado.incomeId}`, editado);
await handleViewIncomes(selectedMemberNumber);
} catch (err) {
console.error("Error actualizando ingreso:", err);
if (err?.status === 422 && err?.errors) {
setIncomeFieldErrors(err.errors);
}
}
};
@@ -182,7 +188,6 @@ const SociosContent = ({ reqConfig }) => {
const closePDFPopup = () => setShowPDFModal(false);
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return (
<CustomContainer>
@@ -197,7 +202,7 @@ const SociosContent = ({ reqConfig }) => {
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
filtersComponent={<SociosFilter filters={filters} onChange={setFilters} />}
onCreate={handleCreate}
//onCreate={handleCreate}
onPDF={showPDFPopup}
/>
@@ -210,8 +215,7 @@ const SociosContent = ({ reqConfig }) => {
isNew
onCreate={handleCreateSubmit}
onCancel={handleCancelCreate}
error={error}
onClearError={() => setError(null)}
fieldErrors={fieldErrors}
/>
)}
renderCard={(identity) => {
@@ -227,9 +231,8 @@ const SociosContent = ({ reqConfig }) => {
onDelete={handleDelete}
onCancel={handleCancelCreate}
onViewIncomes={() => handleViewIncomes(identity.metadata.memberNumber)}
error={error}
onClearError={() => setError(null)}
positionIfWaitlist={position}
fieldErrors={fieldErrors}
/>
);
}}
@@ -247,14 +250,13 @@ const SociosContent = ({ reqConfig }) => {
title={`Ingresos del socio nº ${selectedMemberNumber}`}
>
{incomesLoading && <p className="text-center my-3"><LoadingIcon /></p>}
{incomesError && <p className="text-danger text-center my-3">{incomesError}</p>}
{!incomesLoading && !incomesError && incomes.length === 0 && (
{!incomesLoading && incomes.length === 0 && (
<p className="text-center my-3">Este socio no tiene ingresos registrados.</p>
)}
<div className="d-flex flex-wrap gap-3 p-3 justify-content-start">
{incomes.map((income) => (
<IngresoCard key={income.incomeId} income={income}
onUpdate={handleIncomeUpdate} className='from-members' />
onUpdate={handleIncomeUpdate} className='from-members' fieldErrors={incomeFieldErrors} />
))}
</div>
</CustomModal>
@@ -275,7 +277,7 @@ const SociosContent = ({ reqConfig }) => {
setSearchTerm("");
setDeleteTargetId(null);
} catch (err) {
setError(errorParser(err));
console.error(err);
}
}}
>

View File

@@ -12,11 +12,13 @@ import PaginatedCardGrid from '../components/PaginatedCardGrid';
import SolicitudCard from '../components/Solicitudes/SolicitudCard';
import { Button } from 'react-bootstrap';
import CustomModal from '../components/CustomModal';
import { useError } from '../context/ErrorContext';
const PAGE_SIZE = 10;
const Solicitudes = () => {
const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading || !config) return <p className="text-center my-5"><LoadingIcon /></p>;
@@ -29,14 +31,14 @@ const Solicitudes = () => {
};
return (
<DataProvider config={reqConfig}>
<DataProvider config={reqConfig} onError={showError}>
<SolicitudesContent reqConfig={reqConfig} />
</DataProvider>
);
};
const SolicitudesContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, putData, deleteData } = useDataContext();
const { data, dataLoading, putData, deleteData } = useDataContext();
const [deleteTargetId, setDeleteTargetId] = useState(null);
const {
@@ -84,7 +86,6 @@ const SolicitudesContent = ({ reqConfig }) => {
}
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return (
<CustomContainer>

View File

@@ -1,15 +0,0 @@
export const renderErrorAlert = (error, options = {}) => {
const { className = 'alert alert-danger py-1 px-2 small', role = 'alert' } = options;
if (!error) return null;
return (
<div className={className} role={role}>
{typeof error === 'string' ? error : 'An unexpected error occurred.'}
</div>
);
};
export const resetErrorIfEditEnds = (editMode, setError) => {
if (!editMode) setError(null);
};

View File

@@ -1,10 +0,0 @@
export const errorParser = (err) => {
const message = err.response?.data?.message;
try {
const parsed = JSON.parse(message);
return Object.values(parsed)[0];
// eslint-disable-next-line no-unused-vars
} catch (e) {
return message || err.message || "Unknown error";
}
};