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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,8 @@ import { useData } from "../hooks/useData";
export const DataContext = createContext(); export const DataContext = createContext();
export const DataProvider = ({ config, children }) => { export const DataProvider = ({ config, onError, children }) => {
const data = useData(config); const data = useData(config, onError);
return ( return (
<DataContext.Provider value={data}> <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 { useState, useEffect, useCallback, useRef } from "react";
import axios from "axios"; import axios from "axios";
export const useData = (config) => { export const useData = (config, onError) => {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [dataLoading, setLoading] = useState(true); const [dataLoading, setLoading] = useState(true);
const [dataError, setError] = useState(null); const [dataError, setError] = useState(null);
@@ -13,10 +13,55 @@ export const useData = (config) => {
} }
}, [config]); }, [config]);
const getAuthHeaders = () => ({ const getAuthHeaders = () => {
const token = localStorage.getItem("token");
if (!token) return { "Content-Type": "application/json" };
return {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("token")}`, "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 fetchData = useCallback(async () => {
const current = configRef.current; const current = configRef.current;
@@ -32,69 +77,59 @@ export const useData = (config) => {
}); });
setData(response.data); setData(response.data);
} catch (err) { } catch (err) {
setError(err.response?.data); const error = handleAxiosError(err);
setError(error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (config?.baseUrl) { if (config?.baseUrl) fetchData();
fetchData();
}
}, [config, fetchData]); }, [config, fetchData]);
const getData = async (url, params = {}) => { const requestWrapper = async (method, endpoint, payload = null, refresh = false) => {
const response = await axios.get(url, { try {
headers: getAuthHeaders(), const headers = getAuthHeaders();
params, 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; 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 clearError = () => setError(null);
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;
};
return { return {
data, data,
dataLoading, dataLoading,
dataError, dataError,
getData, clearError,
postData, getData: (url, params, refresh = true) => requestWrapper("get", url, params, refresh),
putData, postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh),
deleteData, putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh),
deleteDataWithBody, 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.css";
import "slick-carousel/slick/slick-theme.css"; import "slick-carousel/slick/slick-theme.css";
import './css/index.css' import './css/index.css'
import { ErrorProvider } from './context/ErrorContext.jsx'
createRoot(document.getElementById('root')).render( createRoot(document.getElementById('root')).render(
<StrictMode> <StrictMode>
<ConfigProvider> <ConfigProvider>
<ThemeProvider> <ThemeProvider>
<ErrorProvider>
<AuthProvider> <AuthProvider>
<BrowserRouter> <BrowserRouter>
<App /> <App />
</BrowserRouter> </BrowserRouter>
</AuthProvider> </AuthProvider>
</ErrorProvider>
</ThemeProvider> </ThemeProvider>
</ConfigProvider> </ConfigProvider>
</StrictMode> </StrictMode>

View File

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

View File

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

View File

@@ -16,14 +16,15 @@ import { GastosPDF } from '../components/Gastos/GastosPDF';
import '../css/Ingresos.css'; import '../css/Ingresos.css';
import { CONSTANTS } from '../util/constants'; import { CONSTANTS } from '../util/constants';
import { errorParser } from '../util/parsers/errorParser';
import CustomModal from '../components/CustomModal'; import CustomModal from '../components/CustomModal';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { useError } from '../context/ErrorContext';
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
const Gastos = () => { const Gastos = () => {
const { config, configLoading } = useConfig(); const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>; if (configLoading) return <p><LoadingIcon /></p>;
@@ -33,19 +34,19 @@ const Gastos = () => {
}; };
return ( return (
<DataProvider config={reqConfig}> <DataProvider config={reqConfig} onError={showError}>
<GastosContent reqConfig={reqConfig}/> <GastosContent reqConfig={reqConfig}/>
</DataProvider> </DataProvider>
); );
}; };
const GastosContent = ({ reqConfig }) => { const GastosContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, postData, putData, deleteData } = useDataContext(); const { data, dataLoading, postData, putData, deleteData } = useDataContext();
const [showPDFModal, setShowPDFModal] = useState(false); const [showPDFModal, setShowPDFModal] = useState(false);
const [creatingGasto, setCreatingGasto] = useState(false); const [creatingGasto, setCreatingGasto] = useState(false);
const [tempGasto, setTempGasto] = useState(null); const [tempGasto, setTempGasto] = useState(null);
const [error, setError] = useState(null);
const [deleteTargetId, setDeleteTargetId] = useState(null); const [deleteTargetId, setDeleteTargetId] = useState(null);
const [fieldErrors, setFieldErrors] = useState(null);
const { const {
filtered, filtered,
@@ -94,21 +95,24 @@ const GastosContent = ({ reqConfig }) => {
const handleCreateSubmit = async (nuevo) => { const handleCreateSubmit = async (nuevo) => {
try { try {
await postData(reqConfig.baseUrl, nuevo); await postData(reqConfig.baseUrl, nuevo);
setError(null);
setCreatingGasto(false); setCreatingGasto(false);
setTempGasto(null); setTempGasto(null);
setFieldErrors(null);
} catch (err) { } catch (err) {
setTempGasto({ ...nuevo }); setTempGasto({ ...nuevo });
setError(errorParser(err)); if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
} }
}; };
const handleEditSubmit = async (editado, id) => { const handleEditSubmit = async (editado, id) => {
try { try {
await putData(`${reqConfig.baseUrl}/${id}`, editado); await putData(`${reqConfig.baseUrl}/${id}`, editado);
setError(null);
} catch (err) { } catch (err) {
setError(errorParser(err)); if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
} }
}; };
@@ -119,11 +123,10 @@ const GastosContent = ({ reqConfig }) => {
const handleCancelCreate = () => { const handleCancelCreate = () => {
setCreatingGasto(false); setCreatingGasto(false);
setTempGasto(null); setTempGasto(null);
setError(null); setFieldErrors(null);
}; };
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>; 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 ( return (
<CustomContainer> <CustomContainer>
@@ -151,8 +154,7 @@ const GastosContent = ({ reqConfig }) => {
isNew isNew
onCreate={handleCreateSubmit} onCreate={handleCreateSubmit}
onCancel={handleCancelCreate} onCancel={handleCancelCreate}
error={error} fieldErrors={fieldErrors}
onClearError={() => setError(null)}
/> />
)} )}
renderCard={(gasto) => ( renderCard={(gasto) => (
@@ -161,8 +163,7 @@ const GastosContent = ({ reqConfig }) => {
gasto={gasto} gasto={gasto}
onUpdate={handleEditSubmit} onUpdate={handleEditSubmit}
onDelete={handleDelete} onDelete={handleDelete}
error={error} fieldErrors={fieldErrors}
onClearError={() => setError(null)}
/> />
)} )}
/> />
@@ -187,7 +188,7 @@ const GastosContent = ({ reqConfig }) => {
setSearchTerm(""); setSearchTerm("");
setDeleteTargetId(null); setDeleteTargetId(null);
} catch (err) { } 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 IngresosFilter from '../components/Ingresos/IngresosFilter';
import { IngresosPDF } from '../components/Ingresos/IngresosPDF'; import { IngresosPDF } from '../components/Ingresos/IngresosPDF';
import { CONSTANTS } from '../util/constants'; import { CONSTANTS } from '../util/constants';
import { errorParser } from '../util/parsers/errorParser';
import '../css/Ingresos.css'; import '../css/Ingresos.css';
import CustomModal from '../components/CustomModal'; import CustomModal from '../components/CustomModal';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { useError } from '../context/ErrorContext';
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
const Ingresos = () => { const Ingresos = () => {
const { config, configLoading } = useConfig(); const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>; if (configLoading) return <p><LoadingIcon /></p>;
@@ -36,19 +37,19 @@ const Ingresos = () => {
}; };
return ( return (
<DataProvider config={reqConfig}> <DataProvider config={reqConfig} onError={showError}>
<IngresosContent reqConfig={reqConfig} /> <IngresosContent reqConfig={reqConfig} />
</DataProvider> </DataProvider>
); );
}; };
const IngresosContent = ({ reqConfig }) => { const IngresosContent = ({ reqConfig }) => {
const { data, dataLoading, dataError, postData, putData, deleteData } = useDataContext(); const { data, dataLoading, postData, putData, deleteData } = useDataContext();
const [showPDFModal, setShowPDFModal] = useState(false); const [showPDFModal, setShowPDFModal] = useState(false);
const [creatingIngreso, setCreatingIngreso] = useState(false); const [creatingIngreso, setCreatingIngreso] = useState(false);
const [tempIngreso, setTempIngreso] = useState(null); const [tempIngreso, setTempIngreso] = useState(null);
const [error, setError] = useState(null);
const [deleteTargetId, setDeleteTargetId] = useState(null); const [deleteTargetId, setDeleteTargetId] = useState(null);
const [fieldErrors, setFieldErrors] = useState(null);
const members = data const members = data
? Array.from( ? Array.from(
@@ -116,37 +117,38 @@ const IngresosContent = ({ reqConfig }) => {
const handleCancelCreate = () => { const handleCancelCreate = () => {
setCreatingIngreso(false); setCreatingIngreso(false);
setTempIngreso(null); setTempIngreso(null);
setError(null); setFieldErrors(null);
}; };
const handleCreateSubmit = async (nuevo) => { const handleCreateSubmit = async (nuevo) => {
try { try {
await postData(reqConfig.rawUrl, nuevo); await postData(reqConfig.rawUrl, nuevo);
setError(null);
setCreatingIngreso(false); setCreatingIngreso(false);
setTempIngreso(null); setTempIngreso(null);
setFieldErrors(null);
} catch (err) { } catch (err) {
setTempIngreso({ ...nuevo }); setTempIngreso({ ...nuevo });
setError(errorParser(err)); if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
} }
}; };
const handleEditSubmit = async (editado, id) => { const handleEditSubmit = async (editado, id) => {
try { try {
await putData(`${reqConfig.rawUrl}/${id}`, editado); await putData(`${reqConfig.rawUrl}/${id}`, editado);
setError(null);
} catch (err) { } catch (err) {
setError(errorParser(err)); if (err?.status === 422 && err?.errors) {
setFieldErrors(err.errors);
}
} }
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
setDeleteTargetId(id); setDeleteTargetId(id);
}; };
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>; 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 ( return (
<CustomContainer> <CustomContainer>
@@ -173,9 +175,8 @@ const IngresosContent = ({ reqConfig }) => {
isNew isNew
onCreate={handleCreateSubmit} onCreate={handleCreateSubmit}
onCancel={handleCancelCreate} onCancel={handleCancelCreate}
error={error}
onClearError={() => setError(null)}
members={members} members={members}
fieldErrors={fieldErrors}
/> />
)} )}
renderCard={(income) => ( renderCard={(income) => (
@@ -184,8 +185,7 @@ const IngresosContent = ({ reqConfig }) => {
income={income} income={income}
onUpdate={(data, id) => handleEditSubmit(data, id)} onUpdate={(data, id) => handleEditSubmit(data, id)}
onDelete={() => handleDelete(income.incomeId)} onDelete={() => handleDelete(income.incomeId)}
error={error} fieldErrors={fieldErrors}
onClearError={() => setError(null)}
/> />
)} )}
/> />
@@ -210,7 +210,7 @@ const IngresosContent = ({ reqConfig }) => {
setSearchTerm(""); setSearchTerm("");
setDeleteTargetId(null); setDeleteTargetId(null);
} catch (err) { } 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 { faPencil } from '@fortawesome/free-solid-svg-icons';
import IfNotAuthenticated from '../components/Auth/IfNotAuthenticated'; import IfNotAuthenticated from '../components/Auth/IfNotAuthenticated';
import NotificationModal from '../components/NotificationModal'; import NotificationModal from '../components/NotificationModal';
import { useError } from '../context/ErrorContext';
const ListaEspera = () => { const ListaEspera = () => {
const { config, configLoading } = useConfig(); const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading) return <p><LoadingIcon /></p>; if (configLoading) return <p><LoadingIcon /></p>;
@@ -29,7 +31,7 @@ const ListaEspera = () => {
}; };
return ( return (
<DataProvider config={reqConfig}> <DataProvider config={reqConfig} onError={showError}>
<ListaEsperaContent reqConfig={reqConfig} /> <ListaEsperaContent reqConfig={reqConfig} />
</DataProvider> </DataProvider>
); );
@@ -37,7 +39,7 @@ const ListaEspera = () => {
const ListaEsperaContent = ({ reqConfig }) => { const ListaEsperaContent = ({ reqConfig }) => {
const { authStatus } = useAuth(); const { authStatus } = useAuth();
const { data, dataLoading, dataError, postData } = useDataContext(); const { data, dataLoading, postData } = useDataContext();
const [showWelcomeModal, setShowWelcomeModal] = useState(false); const [showWelcomeModal, setShowWelcomeModal] = useState(false);
const [showNewUserFormModal, setShowNewUserFormModal] = 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 (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>;
if (dataError) return <p className="text-danger text-center my-5">{dataError}</p>;
return ( return (
<CustomContainer> <CustomContainer>

View File

@@ -31,6 +31,7 @@ import { Button, Col, Row } from 'react-bootstrap';
import AnimatedDropdown from '../components/AnimatedDropdown'; import AnimatedDropdown from '../components/AnimatedDropdown';
import { useAuth } from '../hooks/useAuth'; import { useAuth } from '../hooks/useAuth';
import { CONSTANTS } from '../util/constants'; import { CONSTANTS } from '../util/constants';
import { useError } from '../context/ErrorContext';
const parseDate = (date) => { const parseDate = (date) => {
if (!date) return 'NO'; if (!date) return 'NO';
@@ -53,6 +54,7 @@ const getPFP = (tipo) => {
const Perfil = () => { const Perfil = () => {
const { config, configLoading } = useConfig(); const { config, configLoading } = useConfig();
const { showError } = useError();
if (configLoading || !config) return <p className="text-center my-5"><LoadingIcon /></p>; if (configLoading || !config) return <p className="text-center my-5"><LoadingIcon /></p>;
@@ -75,14 +77,14 @@ const Perfil = () => {
}; };
return ( return (
<DataProvider config={reqConfig}> <DataProvider config={reqConfig} onError={showError}>
<PerfilContent config={reqConfig} /> <PerfilContent config={reqConfig} />
</DataProvider> </DataProvider>
); );
}; };
const PerfilContent = ({ config }) => { const PerfilContent = ({ config }) => {
const { data, dataLoading, dataError, postData } = useDataContext(); const { data, dataLoading, postData } = useDataContext();
const { logout } = useAuth(); const { logout } = useAuth();
const identity = JSON.parse(localStorage.getItem("identity")); const identity = JSON.parse(localStorage.getItem("identity"));
@@ -99,6 +101,7 @@ const PerfilContent = ({ config }) => {
const [showRemoveCollaboratorModal, setShowRemoveCollaboratorModal] = useState(false); const [showRemoveCollaboratorModal, setShowRemoveCollaboratorModal] = useState(false);
const [feedbackModal, setFeedbackModal] = useState(null); const [feedbackModal, setFeedbackModal] = useState(null);
const closeFeedback = () => setFeedbackModal(null); const closeFeedback = () => setFeedbackModal(null);
const [fieldErrors, setFieldErrors] = useState(null);
const baseMetadata = { const baseMetadata = {
displayName: identity.user.displayName, displayName: identity.user.displayName,
@@ -113,6 +116,7 @@ const PerfilContent = ({ config }) => {
}; };
const sendSimpleRequest = async (type) => { const sendSimpleRequest = async (type) => {
setFieldErrors(null);
const requestOf = type == 1 ? "baja" : type == 2 ? "adición de colaborador" : const requestOf = type == 1 ? "baja" : type == 2 ? "adición de colaborador" :
type == 3 ? "eliminación de colaborador" : type == 4 ? "adición de invernadero" : type == 3 ? "eliminación de colaborador" : type == 4 ? "adición de invernadero" :
type == 5 ? "eliminación de invernadero" : "desconocido"; type == 5 ? "eliminación de invernadero" : "desconocido";
@@ -131,21 +135,16 @@ const PerfilContent = ({ config }) => {
onClick: closeFeedback onClick: closeFeedback
}); });
} catch (err) { } catch (err) {
setFeedbackModal({ if (err?.status === 422 && err?.errors) {
title: 'Error', setFieldErrors(err.errors);
message: err.message, }
variant: 'danger',
onClick: closeFeedback
});
} }
}; };
const [validationErrors, setValidationErrors] = useState({});
const [newPasswordData, setNewPasswordData] = useState({ const [newPasswordData, setNewPasswordData] = useState({
currentPassword: "", oldPassword: "",
newPassword: "", newPassword: "",
confirmNewPassword: "" serviceId: identity.account.serviceId
}); });
const [showOld, setShowOld] = useState(false); const [showOld, setShowOld] = useState(false);
@@ -157,28 +156,21 @@ const PerfilContent = ({ config }) => {
...newPasswordData, ...newPasswordData,
[e.target.name]: e.target.value [e.target.name]: e.target.value
}); });
setFieldErrors(null);
} }
const handleChangePassword = async () => { const handleChangePassword = async () => {
try { try {
const validOldPassword = await postData(config.loginValidateUrl, { await postData(config.changePasswordUrl, {
userId: identity.user.userId, oldPassword: newPasswordData.oldPassword,
password: newPasswordData.currentPassword newPassword: newPasswordData.newPassword,
}); serviceId: identity.user.account.serviceId
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
}); });
if (!response) throw new Error("Error al cambiar la contraseña.");
setNewPasswordData({ setNewPasswordData({
currentPassword: "", oldPassword: "",
newPassword: "", newPassword: "",
confirmNewPassword: "" serviceId: identity.user.account.serviceId
}); });
setFeedbackModal({ setFeedbackModal({
@@ -191,14 +183,13 @@ const PerfilContent = ({ config }) => {
} }
}); });
} catch (err) { } catch (err) {
setFeedbackModal({ if (err?.status === 422 && err?.errors) {
title: 'Error', setFieldErrors(err.errors);
message: err.message,
variant: 'danger',
onClick: closeFeedback
});
} }
} }
};
const getFieldError = (field) => fieldErrors?.[field] ?? null;
const mappedRequests = myRequests.map(r => ({ const mappedRequests = myRequests.map(r => ({
...r, ...r,
@@ -208,7 +199,6 @@ const PerfilContent = ({ config }) => {
})); }));
if (dataLoading) return <p className="text-center my-5"><LoadingIcon /></p>; 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 ( return (
<CustomContainer> <CustomContainer>
@@ -243,6 +233,7 @@ const PerfilContent = ({ config }) => {
{!hasCollaborator && !hasCollaboratorRequest && ( {!hasCollaborator && !hasCollaboratorRequest && (
<div className="dropdown-item d-flex align-items-center" onClick={() => { <div className="dropdown-item d-flex align-items-center" onClick={() => {
setShowAddCollaboratorModal(true); setShowAddCollaboratorModal(true);
setFieldErrors(null);
closeDropdown(); closeDropdown();
}}> }}>
<FontAwesomeIcon icon={faUserPlus} className="me-2" />Añadir un colaborador <FontAwesomeIcon icon={faUserPlus} className="me-2" />Añadir un colaborador
@@ -334,7 +325,11 @@ const PerfilContent = ({ config }) => {
placeholder="" placeholder=""
name="currentPassword" name="currentPassword"
className="rounded-4" className="rounded-4"
isInvalid={!!fieldErrors?.oldPassword}
/> />
<Form.Control.Feedback type="invalid">
{getFieldError("oldPassword")}
</Form.Control.Feedback>
<Button <Button
variant="link" variant="link"
className="show-button position-absolute end-0 top-50 translate-middle-y me-2" className="show-button position-absolute end-0 top-50 translate-middle-y me-2"
@@ -356,7 +351,11 @@ const PerfilContent = ({ config }) => {
placeholder="" placeholder=""
name="newPassword" name="newPassword"
className="rounded-4" className="rounded-4"
isInvalid={!!fieldErrors?.newPassword}
/> />
<Form.Control.Feedback type="invalid" as="span">
{getFieldError("newPassword")}
</Form.Control.Feedback>
<Button <Button
variant="link" variant="link"
className="show-button position-absolute end-0 top-50 translate-middle-y me-2" 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.newPassword === '' || newPasswordData.confirmNewPassword === '' ||
newPasswordData.currentPassword === '' newPasswordData.currentPassword === ''
} }
onClick={(e) => { e.preventDefault(); handleChangePassword(); }}
type='submit' type='submit'
variant="warning" variant="warning"
style={{ width: 'fit-content' }} style={{ width: 'fit-content' }}
@@ -413,17 +411,16 @@ const PerfilContent = ({ config }) => {
show={showAddCollaboratorModal} show={showAddCollaboratorModal}
onClose={() => { onClose={() => {
setShowAddCollaboratorModal(false); setShowAddCollaboratorModal(false);
setValidationErrors({}); setFieldErrors(null);
}} }}
> >
<NewUserForm <NewUserForm
userType={3} userType={3}
plotNumber={identity.metadata.plotNumber} plotNumber={identity.metadata.plotNumber}
errors={validationErrors} fieldErrors={fieldErrors}
onSubmit={async (formData) => { onSubmit={async (formData) => {
console.log("🚀 Enviando al backend...", formData); // Debug
try { try {
setValidationErrors({}); setFieldErrors(null);
await postData(config.requestUrl, { await postData(config.requestUrl, {
type: CONSTANTS.REQUEST_TYPE_ADD_COLLABORATOR, type: CONSTANTS.REQUEST_TYPE_ADD_COLLABORATOR,
@@ -449,9 +446,10 @@ const PerfilContent = ({ config }) => {
onClick: closeFeedback onClick: closeFeedback
}); });
} catch (error) { } catch (err) {
console.error("💥 Error al añadir:", error); if (err?.status === 422 && err?.errors) {
setValidationErrors({ general: error.message || "Ha ocurrido un error al procesar la solicitud." }); setFieldErrors(err.errors);
}
} }
}} }}
/> />
@@ -483,13 +481,8 @@ const PerfilContent = ({ config }) => {
onClick: closeFeedback onClick: closeFeedback
}); });
setShowRemoveCollaboratorModal(false); setShowRemoveCollaboratorModal(false);
} catch (err) { } catch (error) {
setFeedbackModal({ console.error(error);
title: "Error",
message: err.message,
variant: "danger",
onClick: closeFeedback
});
} }
}} }}
> >

View File

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