Ongoing adaptation to new backend structure

This commit is contained in:
Jose
2026-01-25 23:43:25 +01:00
parent 5f5d54d642
commit 59efb7d81f
31 changed files with 505 additions and 514 deletions

View File

@@ -45,7 +45,7 @@ const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onC
const [formData, setFormData] = useState({
body: anuncio.body || '',
priority: anuncio.priority ?? 1,
published_by: JSON.parse(localStorage.getItem('user'))?.user_id,
publishedBy: JSON.parse(localStorage.getItem('identity'))?.user?.userId,
});
useEffect(() => {
@@ -53,7 +53,7 @@ const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onC
setFormData({
body: anuncio.body || '',
priority: anuncio.priority ?? 1,
published_by: JSON.parse(localStorage.getItem('user'))?.user_id,
publishedBy: JSON.parse(localStorage.getItem('identity'))?.user?.userId,
});
}
}, [anuncio, editMode]);
@@ -63,7 +63,7 @@ const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onC
setEditMode(true);
};
const handleDelete = () => typeof onDelete === 'function' && onDelete(anuncio.announce_id);
const handleDelete = () => typeof onDelete === 'function' && onDelete(anuncio.announceId);
const handleCancel = () => {
if (onClearError) onClearError();
@@ -77,12 +77,12 @@ const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onC
formData.body = sanitizedBody;
const updated = { ...anuncio, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(updated);
if (typeof onUpdate === 'function') return onUpdate(updated, anuncio.announce_id);
if (typeof onUpdate === 'function') return onUpdate(updated, anuncio.announceId);
};
const handleChange = (field, value) => setFormData((prev) => ({ ...prev, [field]: value }));
const { date, time } = formatDateTime(anuncio.created_at);
const { date, time } = formatDateTime(anuncio.createdAt);
const priorityInfo = PRIORITY_CONFIG[formData.priority] || PRIORITY_CONFIG[1];
const isLongBody = formData.body.length > 300;
const displayBody = isLongBody && !showFullBody
@@ -101,11 +101,15 @@ const AnuncioCard = ({ anuncio, isNew = false, onCreate, onUpdate, onDelete, onC
<Card className="anuncio-card rounded-4 border-0 shadow-sm mb-4">
<Card.Header className="d-flex justify-content-between align-items-center rounded-top-4 px-3 py-2">
<div className="d-flex flex-column">
<span className="fw-bold">📢&emsp;Anuncio #{anuncio.announce_id}</span>
<small className="muted">
Publicado el {date} a las {time} por{' '}
<span className="fw-semibold">#{anuncio.published_by}</span>
</small>
<span className="fw-bold">📢&emsp;Anuncio {!createMode ? ("#"+anuncio.idx) : ("")}</span>
{!createMode ? (
<small className="muted">
Publicado el {date} a las {time} por{' '}
<span className="fw-semibold">{anuncio.publishedByName}</span>
</small>
) : (
<></>
)}
</div>
{!createMode && !editMode && (
<AnimatedDropdown

View File

@@ -32,7 +32,6 @@ function App() {
<NavBar />
<Routes>
<Route path="/" element={<Home />} />
{/*
<Route path="/lista-espera" element={<ListaEspera />} />
<Route path="/login" element={<Login />} />
<Route path="/gestion/socios" element={
@@ -80,7 +79,6 @@ function App() {
<Correo />
</ProtectedRoute>
} />
*/}
<Route path="/*" element={<Maintenance />} />
</Routes>
{routesWithFooter.includes(useLocation().pathname) ? <Footer /> : null}

View File

@@ -1,11 +1,11 @@
import { useAuth } from "../../hooks/useAuth.js";
const IfRole = ({ roles, children }) => {
const { user, authStatus } = useAuth();
const { identity, authStatus } = useAuth();
if (authStatus !== "authenticated") return null;
const userRole = user?.role;
const userRole = identity?.metadata?.role;
return roles.includes(userRole) ? children : null;
};

View File

@@ -11,15 +11,15 @@ import CustomContainer from '../CustomContainer.jsx';
import ContentWrapper from '../ContentWrapper.jsx';
import '../../css/LoginForm.css';
import { CONSTANTS } from '../../util/constants.js';
const LoginForm = () => {
const { login, error } = useContext(AuthContext);
const navigate = useNavigate();
const [formState, setFormState] = useState({
emailOrUserName: "",
password: "",
keepLoggedIn: false
username: "",
password: ""
});
const handleChange = (e) => {
@@ -30,19 +30,12 @@ const LoginForm = () => {
const handleSubmit = async (e) => {
e.preventDefault();
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formState.emailOrUserName);
const loginBody = {
username: formState.username,
password: formState.password,
keepLoggedIn: Boolean(formState.keepLoggedIn),
serviceId: CONSTANTS.SERVICE_ID
};
if (isEmail) {
loginBody.email = formState.emailOrUserName;
} else {
loginBody.userName = formState.emailOrUserName;
}
try {
await login(loginBody);
navigate("/");
@@ -63,15 +56,15 @@ const LoginForm = () => {
label={
<>
<FontAwesomeIcon icon={faUser} className="me-2" />
Usuario o Email
Usuario
</>
}
>
<Form.Control
type="text"
placeholder=""
name="emailOrUserName"
value={formState.emailOrUserName}
name="username"
value={formState.username}
onChange={handleChange}
className="rounded-4"
/>
@@ -83,7 +76,7 @@ const LoginForm = () => {
name="password"
/>
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2">
{/*<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2">
<Form.Check
type="checkbox"
name="keepLoggedIn"
@@ -92,10 +85,10 @@ const LoginForm = () => {
value={formState.keepLoggedIn}
onChange={(e) => { formState.keepLoggedIn = e.target.checked; setFormState({ ...formState }) }}
/>
{/*<Link disabled to="#" className="muted">
<Link disabled to="#" className="muted">
Olvidé mi contraseña
</Link>*/}
</div>
</Link>
</div>*/}
</div>
{error && (

View File

@@ -6,10 +6,10 @@ import { faSpinner } from "@fortawesome/free-solid-svg-icons";
const ProtectedRoute = ({ minimumRoles, children }) => {
const { authStatus } = useAuth();
if (authStatus === "checking") return <FontAwesomeIcon icon={faSpinner} />; // o un loader si quieres
if (authStatus === "checking") return <FontAwesomeIcon icon={faSpinner} />;
if (authStatus === "unauthenticated") return <Navigate to="/login" replace />;
if (authStatus === "authenticated" && minimumRoles) {
const userRole = JSON.parse(localStorage.getItem("user"))?.role;
const userRole = JSON.parse(localStorage.getItem("identity"))?.metadata?.role;
if (!minimumRoles.includes(userRole)) return <Navigate to="/" replace />;
}
return children;

View File

@@ -67,17 +67,17 @@ const formatCurrency = (value) =>
export const BalancePDF = ({ balance }) => {
const {
initial_bank,
initial_cash,
total_bank_expenses,
total_cash_expenses,
total_bank_incomes,
total_cash_incomes,
created_at
initialBank,
initialCash,
totalBankExpenses,
totalCashExpenses,
totalBankIncomes,
totalCashIncomes,
createdAt
} = balance;
const final_bank = initial_bank + total_bank_incomes - total_bank_expenses;
const final_cash = initial_cash + total_cash_incomes - total_cash_expenses;
const finalBank = initialBank + totalBankIncomes - totalBankExpenses;
const finalCash = initialCash + totalCashIncomes - totalCashExpenses;
return (
<Document>
@@ -91,22 +91,22 @@ export const BalancePDF = ({ balance }) => {
</View>
<Text style={styles.sectionTitle}>Banco</Text>
<View style={styles.row}><Text style={styles.label}>Saldo inicial</Text><Text style={styles.value}>{formatCurrency(initial_bank)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Ingresos</Text><Text style={styles.value}>{formatCurrency(total_bank_incomes)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Gastos</Text><Text style={styles.value}>{formatCurrency(total_bank_expenses)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Saldo inicial</Text><Text style={styles.value}>{formatCurrency(initialBank)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Ingresos</Text><Text style={styles.value}>{formatCurrency(totalBankIncomes)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Gastos</Text><Text style={styles.value}>{formatCurrency(totalBankExpenses)}</Text></View>
<Text style={styles.sectionTitle}>Caja</Text>
<View style={styles.row}><Text style={styles.label}>Saldo inicial</Text><Text style={styles.value}>{formatCurrency(initial_cash)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Ingresos</Text><Text style={styles.value}>{formatCurrency(total_cash_incomes)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Gastos</Text><Text style={styles.value}>{formatCurrency(total_cash_expenses)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Saldo inicial</Text><Text style={styles.value}>{formatCurrency(initialCash)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Ingresos</Text><Text style={styles.value}>{formatCurrency(totalCashIncomes)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Gastos</Text><Text style={styles.value}>{formatCurrency(totalCashExpenses)}</Text></View>
<Text style={styles.sectionTitle}>Total</Text>
<View style={styles.row}><Text style={styles.label}>Banco</Text><Text style={styles.value}>{formatCurrency(final_bank)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Caja</Text><Text style={styles.value}>{formatCurrency(final_cash)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Total</Text><Text style={styles.value}>{formatCurrency(final_bank + final_cash)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Banco</Text><Text style={styles.value}>{formatCurrency(finalBank)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Caja</Text><Text style={styles.value}>{formatCurrency(finalCash)}</Text></View>
<View style={styles.row}><Text style={styles.label}>Total</Text><Text style={styles.value}>{formatCurrency(finalBank + finalCash)}</Text></View>
<Text style={[styles.label, { marginTop: 20 }]}>
Última actualización: {format(new Date(created_at), 'dd/MM/yyyy HH:mm')}
Última actualización: {format(new Date(createdAt), 'dd/MM/yyyy HH:mm')}
</Text>
</Page>
</Document>

View File

@@ -24,17 +24,17 @@ const BalanceReport = ({ balance }) => {
const closePDFModal = () => setShowPDF(false);
const {
initial_bank,
initial_cash,
total_bank_expenses,
total_cash_expenses,
total_bank_incomes,
total_cash_incomes,
created_at
initialBank,
initialCash,
totalBankExpenses,
totalCashExpenses,
totalBankIncomes,
totalCashIncomes,
createdAt
} = balance;
const final_bank = initial_bank + total_bank_incomes - total_bank_expenses;
const final_cash = initial_cash + total_cash_incomes - total_cash_expenses;
const finalBank = initialBank + totalBankIncomes - totalBankExpenses;
const finalCash = initialCash + totalCashIncomes - totalCashExpenses;
return (
<>
@@ -56,20 +56,20 @@ const BalanceReport = ({ balance }) => {
<Col md={6}>
<div className="balance-box">
<h4><FontAwesomeIcon icon={faPiggyBank} className="me-2" />Banco</h4>
<p>Saldo inicial: <span className="balance-value">{formatCurrency(initial_bank)}</span></p>
<p><FontAwesomeIcon icon={faArrowUp} className="me-1 text-success" />Ingresos: <span className="balance-value">{formatCurrency(total_bank_incomes)}</span></p>
<p><FontAwesomeIcon icon={faArrowDown} className="me-1 text-danger" />Gastos: <span className="balance-value">{formatCurrency(total_bank_expenses)}</span></p>
<p className="fw-bold mt-3">💰 Saldo final: {formatCurrency(final_bank)}</p>
<p>Saldo inicial: <span className="balance-value">{formatCurrency(initialBank)}</span></p>
<p><FontAwesomeIcon icon={faArrowUp} className="me-1 text-success" />Ingresos: <span className="balance-value">{formatCurrency(totalBankIncomes)}</span></p>
<p><FontAwesomeIcon icon={faArrowDown} className="me-1 text-danger" />Gastos: <span className="balance-value">{formatCurrency(totalBankExpenses)}</span></p>
<p className="fw-bold mt-3">💰 Saldo final: {formatCurrency(finalBank)}</p>
</div>
</Col>
<Col md={6}>
<div className="balance-box">
<h4><FontAwesomeIcon icon={faCoins} className="me-2" />Caja</h4>
<p>Saldo inicial: <span className="balance-value">{formatCurrency(initial_cash)}</span></p>
<p><FontAwesomeIcon icon={faArrowUp} className="me-1 text-success" />Ingresos: <span className="balance-value">{formatCurrency(total_cash_incomes)}</span></p>
<p><FontAwesomeIcon icon={faArrowDown} className="me-1 text-danger" />Gastos: <span className="balance-value">{formatCurrency(total_cash_expenses)}</span></p>
<p className="fw-bold mt-3">💵 Saldo final: {formatCurrency(final_cash)}</p>
<p>Saldo inicial: <span className="balance-value">{formatCurrency(initialCash)}</span></p>
<p><FontAwesomeIcon icon={faArrowUp} className="me-1 text-success" />Ingresos: <span className="balance-value">{formatCurrency(totalCashIncomes)}</span></p>
<p><FontAwesomeIcon icon={faArrowDown} className="me-1 text-danger" />Gastos: <span className="balance-value">{formatCurrency(totalCashExpenses)}</span></p>
<p className="fw-bold mt-3">💵 Saldo final: {formatCurrency(finalCash)}</p>
</div>
</Col>
</Row>
@@ -77,7 +77,7 @@ const BalanceReport = ({ balance }) => {
<Row className="mt-4">
<Col className="text-end balance-timestamp">
<FontAwesomeIcon icon={faClock} className="me-2" />
Última actualización: {format(new Date(created_at), 'dd/MM/yyyy HH:mm')}
Última actualización: {format(new Date(createdAt), 'dd/MM/yyyy HH:mm')}
</Col>
</Row>
</Card>

View File

@@ -25,19 +25,19 @@ const File = ({ file, onDelete }) => {
return (
<Card
className="file-card col-sm-3 col-lg-2 col-xxl-1 m-0 p-0 position-relative text-decoration-none bg-transparent"
onClick={() => window.open(`https://miarma.net/files/huertos/${file.file_name}`, "_blank")}
onClick={() => window.open(`https://miarma.net/files/huertos/${file.fileName}`, "_blank")}
>
<Card.Body className="text-center">
<img
src={getIcon(file.mime_type)}
alt={file.file_name}
src={getIcon(file.mimeType)}
alt={file.fileName}
className="img-fluid mb-2"
/>
<OverlayTrigger
placement="bottom"
overlay={<Tooltip>{file.file_name}</Tooltip>}
overlay={<Tooltip>{file.fileName}</Tooltip>}
>
<p className="m-0 p-0 text-truncate">{file.file_name}</p>
<p className="m-0 p-0 text-truncate">{file.fileName}</p>
</OverlayTrigger>
</Card.Body>

View File

@@ -49,7 +49,7 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
supplier: gasto.supplier || '',
invoice: gasto.invoice || '',
type: gasto.type ?? 0,
created_at: gasto.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
createdAt: gasto.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
useEffect(() => {
@@ -60,7 +60,7 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
supplier: gasto.supplier || '',
invoice: gasto.invoice || '',
type: gasto.type ?? 0,
created_at: gasto.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
createdAt: gasto.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -68,7 +68,7 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
const handleChange = (field, value) => setFormData(prev => ({ ...prev, [field]: value }));
const handleDelete = () => typeof onDelete === 'function' && onDelete(gasto.expense_id);
const handleDelete = () => typeof onDelete === 'function' && onDelete(gasto.expenseId);
const handleCancel = () => {
if (onClearError) onClearError();
@@ -80,7 +80,7 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
if (onClearError) onClearError();
const newExpense = { ...gasto, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(newExpense);
if (typeof onUpdate === 'function') return onUpdate(newExpense, gasto.expense_id);
if (typeof onUpdate === 'function') return onUpdate(newExpense, gasto.expenseId);
};
return (
@@ -102,13 +102,13 @@ const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCance
<small>
{editMode ? (
<SpanishDateTimePicker
selected={new Date(formData.created_at)}
selected={new Date(formData.createdAt)}
onChange={(date) =>
handleChange('created_at', date.toISOString().slice(0, 16))
handleChange('createdAt', date.toISOString().slice(0, 16))
}
/>
) : (
DateParser.isoToStringWithTime(formData.created_at)
DateParser.isoToStringWithTime(formData.createdAt)
)}
</small>
</div>

View File

@@ -104,7 +104,7 @@ export const GastosPDF = ({ gastos }) => (
{ borderBottomRightRadius: idx === gastos.length - 1 ? 10 : 0 },
]}
>
<Text style={[styles.cell, { flex: 2 }]}>{parseDate(gasto.created_at)}</Text>
<Text style={[styles.cell, { flex: 2 }]}>{parseDate(gasto.createdAt)}</Text>
<Text style={[styles.cell, { flex: 4 }]}>{gasto.concept}</Text>
<Text style={[styles.cell, { flex: 2 }]}>{gasto.amount.toFixed(2)} </Text>
<Text style={[styles.cell, { flex: 3 }]}>{gasto.supplier}</Text>

View File

@@ -63,8 +63,8 @@ const IngresoCard = ({
amount: income.amount || 0,
type: income.type ?? CONSTANTS.PAYMENT_TYPE_CASH,
frequency: income.frequency ?? CONSTANTS.PAYMENT_FREQUENCY_YEARLY,
member_number: income.member_number,
created_at: income.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
memberNumber: income.memberNumber,
createdAt: income.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
useEffect(() => {
@@ -74,9 +74,9 @@ const IngresoCard = ({
amount: income.amount || 0,
type: income.type ?? CONSTANTS.PAYMENT_TYPE_CASH,
frequency: income.frequency ?? CONSTANTS.PAYMENT_FREQUENCY_YEARLY,
display_name: income.display_name,
member_number: income.member_number,
created_at: income.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
displayName: income.displayName,
memberNumber: income.memberNumber,
createdAt: income.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -95,14 +95,14 @@ const IngresoCard = ({
if (onClearError) onClearError();
const newIncome = { ...income, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(newIncome);
if (typeof onUpdate === 'function') return onUpdate(newIncome, income.income_id);
if (typeof onUpdate === 'function') return onUpdate(newIncome, income.incomeId);
};
const handleDelete = () => typeof onDelete === 'function' && onDelete(income.income_id);
const handleDelete = () => typeof onDelete === 'function' && onDelete(income.incomeId);
const uniqueMembers = Array.from(
new Map(members.map(item => [item.member_number, item])).values()
).sort((a, b) => a.member_number - b.member_number);
new Map(members.map(item => [item.memberNumber, item])).values()
).sort((a, b) => a.memberNumber - b.memberNumber);
return (
<MotionCard className={`ingreso-card shadow-sm rounded-4 border-0 h-100 ${className}`}>
@@ -125,13 +125,13 @@ const IngresoCard = ({
<small>
{editMode ? (
<SpanishDateTimePicker
selected={new Date(formData.created_at)}
selected={new Date(formData.createdAt)}
onChange={(date) =>
handleChange('created_at', date.toISOString().slice(0, 16))
handleChange('createdAt', date.toISOString().slice(0, 16))
}
/>
) : (
DateParser.isoToStringWithTime(formData.created_at)
DateParser.isoToStringWithTime(formData.createdAt)
)}
</small>
</div>
@@ -167,13 +167,13 @@ const IngresoCard = ({
<Form.Select
className="themed-input"
size="sm"
value={formData.member_number}
onChange={(e) => handleChange('member_number', parseInt(e.target.value))}
value={formData.memberNumber}
onChange={(e) => handleChange('memberNumber', parseInt(e.target.value))}
style={{ maxWidth: '300px', display: 'inline-block' }}
>
{uniqueMembers.map((m) => (
<option key={m.member_number} value={m.member_number}>
{`${m.display_name} (${m.member_number})`}
<option key={m.memberNumber} value={m.memberNumber}>
{`${m.displayName} (${m.memberNumber})`}
</option>
))}
</Form.Select>
@@ -187,24 +187,24 @@ const IngresoCard = ({
disabled
size="sm"
type="text"
value={`${formData.display_name || 'Socio'} (${formData.member_number})`}
value={`${formData.displayName || 'Socio'} (${formData.memberNumber})`}
style={{ maxWidth: '300px', display: 'inline-block' }}
/>
</OverlayTrigger>
) : (
formData.display_name ? (
formData.displayName ? (
<>
<OverlayTrigger
placement="top"
overlay={<Tooltip>{formData.display_name}</Tooltip>}
overlay={<Tooltip>{formData.displayName}</Tooltip>}
>
<span className="text-truncate d-inline-block" style={{ maxWidth: '200px', verticalAlign: 'middle' }}>
{formData.display_name}
{formData.displayName}
</span>
</OverlayTrigger>
&nbsp;({formData.member_number})
&nbsp;({formData.memberNumber})
</>
) : formData.member_number
) : formData.memberNumber
)}
</Card.Text>

View File

@@ -105,12 +105,12 @@ export const IngresosPDF = ({ ingresos }) => (
{ borderBottomRightRadius: idx === ingresos.length - 1 ? 10 : 0 },
]}
>
<Text style={[styles.cell, { flex: 1 }]}>{ing.member_number}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{ing.memberNumber}</Text>
<Text style={[styles.cell, { flex: 3 }]}>{ing.concept}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{ing.amount.toFixed(2)} </Text>
<Text style={[styles.cell, { flex: 1 }]}>{getTypeLabel(ing.type)}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{getFreqLabel(ing.frequency)}</Text>
<Text style={[styles.cell, { flex: 2 }]}>{parseDate(ing.created_at)}</Text>
<Text style={[styles.cell, { flex: 2 }]}>{parseDate(ing.createdAt)}</Text>
</View>
))}
</Page>

View File

@@ -27,7 +27,7 @@ import AnimatedDropdown from '../AnimatedDropdown.jsx';
import { CONSTANTS } from '../../util/constants.js';
const NavBar = () => {
const { user, logout } = useAuth();
const { identity, logout } = useAuth();
const [showingUserDropdown, setShowingUserDropdown] = useState(false);
const [expanded, setExpanded] = useState(false);
const [isLg, setIsLg] = useState(window.innerWidth >= 992);
@@ -138,7 +138,7 @@ const NavBar = () => {
onToggle={(isOpen) => setShowingUserDropdown(isOpen)}
trigger={
<Link className="nav-link dropdown-toggle fw-bold">
@{user?.user_name}
@{identity?.account?.username}
</Link>
}
>

View File

@@ -45,16 +45,16 @@ const renderDateField = (label, icon, dateValue, editMode, fieldKey, handleChang
};
const getFechas = (formData, editMode, handleChange) => {
const { created_at, assigned_at, deactivated_at } = formData;
const { createdAt, assignedAt, deactivatedAt } = formData;
// Si no hay fechas y no está en modo edición, no muestres nada
if (!editMode && !created_at && !assigned_at && !deactivated_at) return null;
if (!editMode && !createdAt && !assignedAt && !deactivatedAt) return null;
return (
<ListGroup className="mt-2 border-1 rounded-3 shadow-sm">
{renderDateField("ALTA", faCalendar, created_at, editMode, "created_at", handleChange)}
{renderDateField("ENTREGA", faCalendar, assigned_at, editMode, "assigned_at", handleChange)}
{renderDateField("BAJA", faCalendar, deactivated_at, editMode, "deactivated_at", handleChange)}
{renderDateField("ALTA", faCalendar, createdAt, editMode, "createdAt", handleChange)}
{renderDateField("ENTREGA", faCalendar, assignedAt, editMode, "assignedAt", handleChange)}
{renderDateField("BAJA", faCalendar, deactivatedAt, editMode, "deactivatedAt", handleChange)}
</ListGroup>
);
};
@@ -91,7 +91,7 @@ const getPFP = (tipo) => {
const MotionCard = _motion.create(Card);
const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCancel, onViewIncomes, error, onClearError, positionIfWaitlist }) => {
const SocioCard = ({ identity, isNew = false, onCreate, onUpdate, onDelete, onCancel, onViewIncomes, error, onClearError, positionIfWaitlist }) => {
const createMode = isNew;
const [editMode, setEditMode] = useState(isNew);
const [showPassword, setShowPassword] = useState(false);
@@ -99,60 +99,59 @@ const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCance
const { getData } = useDataContext();
const [formData, setFormData] = useState({
display_name: socio.display_name,
user_name: socio.user_name,
email: socio.email || '',
dni: socio.dni,
phone: socio.phone,
member_number: socio.member_number || latestNumber,
plot_number: socio.plot_number,
notes: socio.notes || '',
status: socio.status,
type: socio.type,
created_at: socio.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
assigned_at: socio.assigned_at?.slice(0, 16) || undefined,
deactivated_at: socio.deactivated_at?.slice(0, 16) || undefined,
global_role: 0,
displayName: identity?.user.displayName,
userName: identity?.account.username,
email: identity?.account.email || '',
dni: identity?.metadata.dni,
phone: identity?.metadata.phone,
memberNumber: identity?.metadata.memberNumber || latestNumber,
plotNumber: identity?.metadata.plotNumber,
notes: identity?.metadata.notes || '',
status: identity?.account.status,
type: identity?.metadata.type,
createdAt: identity?.metadata.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
assignedAt: identity?.metadata.assignedAt?.slice(0, 16) || undefined,
deactivatedAt: identity?.metadata.deactivatedAt?.slice(0, 16) || undefined,
globalRole: 0,
password: createMode && !editMode ? generateSecurePassword() : null,
});
useEffect(() => {
if (!editMode) {
setFormData({
display_name: socio.display_name,
user_name: socio.user_name,
email: socio.email || '',
dni: socio.dni,
phone: socio.phone,
member_number: socio.member_number,
plot_number: socio.plot_number,
notes: socio.notes || '',
status: socio.status,
type: socio.type,
created_at: socio.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
assigned_at: socio.assigned_at?.slice(0, 16) || undefined,
deactivated_at: socio.deactivated_at?.slice(0, 16) || undefined,
global_role: 0,
displayName: identity.user.displayName,
userName: identity.account.username,
email: identity.account.email || '',
dni: identity.metadata.dni,
phone: identity.metadata.phone,
memberNumber: identity.metadata.memberNumber,
plotNumber: identity.metadata.plotNumber,
notes: identity.metadata.notes || '',
status: identity.account.status,
type: identity.metadat.type,
createdAt: identity.metadata.createdAt?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
assignedAt: identity.metadata.assignedAt?.slice(0, 16) || undefined,
deactivatedAt: identity.metadata.deactivatedAt?.slice(0, 16) || undefined,
globalRole: 0,
password: createMode ? generateSecurePassword() : ''
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [socio, editMode]);
}, [identity, editMode]);
useEffect(() => {
const fetchLastNumber = async () => {
try {
if (!(createMode || editMode)) return;
const { data, error } = await getData("https://api.huertosbellavista.es/v1/members/latest-number");
if (error) throw new Error(error);
const latestNumber = await getData("http://localhost:8081/v2/huertos/users/latest-number");
const nuevoNumero = data.lastMemberNumber + 1;
const nuevoNumero = latestNumber + 1;
setLatestNumber(nuevoNumero);
setFormData(prev => ({
...prev,
member_number: prev.member_number || nuevoNumero
memberNumber: prev.memberNumber || nuevoNumero
}));
} catch (err) {
console.error("Error al obtener el número de socio:", err);
@@ -167,7 +166,7 @@ const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCance
setEditMode(true);
};
const handleDelete = () => typeof onDelete === "function" && onDelete(socio.user_id);
const handleDelete = () => typeof onDelete === "function" && onDelete(identity.user.userId);
const handleCancel = () => {
if (onClearError) onClearError();
@@ -177,16 +176,16 @@ const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCance
const handleSave = () => {
if (onClearError) onClearError();
const newSocio = { ...socio, ...formData };
const newSocio = { ...identity, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(newSocio);
if (typeof onUpdate === 'function') return onUpdate(newSocio, socio.user_id);
if (typeof onUpdate === 'function') return onUpdate(newSocio, identity.user.userId);
};
const handleChange = (field, value) => {
if (["member_number"].includes(field)) {
if (["memberNumber"].includes(field)) {
value = value === "" ? latestNumber : parseInt(value);
}
if (field === "display_name") {
if (field === "displayName") {
value = value.toUpperCase();
}
if (field === "dni") {
@@ -196,7 +195,7 @@ const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCance
};
const handleViewIncomes = () => {
onViewIncomes(socio.user_id);
onViewIncomes(identity.user.userId);
}
return (
@@ -206,7 +205,7 @@ const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCance
{editMode ? (
<TipoSocioDropdown value={formData.type} onChange={(val) => handleChange('type', val)} />
) : (
positionIfWaitlist && socio.type === 0 ? (
positionIfWaitlist && identity.metadata.type === 0 ? (
<OverlayTrigger
placement="top"
overlay={
@@ -225,8 +224,8 @@ const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCance
<div className='d-flex flex-column gap-1'>
<Card.Title className="m-0">
{editMode ? (
<Form.Control className="themed-input" size="sm" value={formData.display_name} onChange={(e) => handleChange('display_name', e.target.value)} style={{ maxWidth: '220px' }} />
) : formData.display_name}
<Form.Control className="themed-input" size="sm" value={formData.displayName} onChange={(e) => handleChange('displayName', e.target.value)} style={{ maxWidth: '220px' }} />
) : formData.displayName}
</Card.Title>
{editMode ? (
<Form.Select className="themed-input" size="sm" value={formData.status} onChange={(e) => handleChange('status', parseInt(e.target.value))} style={{ maxWidth: '8rem' }}>
@@ -269,9 +268,9 @@ const SocioCard = ({ socio, isNew = false, onCreate, onUpdate, onDelete, onCance
{[{
label: 'DNI', clazz: '', icon: faIdCard, value: formData.dni, field: 'dni', type: 'text', maxWidth: '180px'
}, {
label: 'SOCIO Nº', clazz: '', icon: faUser, value: formData.member_number || latestNumber, field: 'member_number', type: 'number', maxWidth: '100px'
label: 'SOCIO Nº', clazz: '', icon: faUser, value: formData.memberNumber || latestNumber, field: 'memberNumber', type: 'number', maxWidth: '100px'
}, {
label: 'HUERTO Nº', clazz: '', icon: faSunPlantWilt, value: formData.plot_number, field: 'plot_number', type: 'number', maxWidth: '100px'
label: 'HUERTO Nº', clazz: '', icon: faSunPlantWilt, value: formData.plotNumber, field: 'plotNumber', type: 'number', maxWidth: '100px'
}, {
label: 'TLF.', clazz: '', icon: faPhone, value: formData.phone, field: 'phone', type: 'number', maxWidth: '200px'
}, {

View File

@@ -107,13 +107,13 @@ export const SociosPDF = ({ socios }) => (
{ borderBottomRightRadius: idx === socios.length - 1 ? 10 : 0 },
]}
>
<Text style={[styles.cell, { flex: 0.2 }]}>{socio?.member_number}</Text>
<Text style={[styles.cell, { flex: 0.2 }]}>{socio?.plot_number}</Text>
<Text style={[styles.cell, { flex: 3 }]}>{socio?.display_name}</Text>
<Text style={[styles.cell, { flex: 0.2 }]}>{socio?.memberNumber}</Text>
<Text style={[styles.cell, { flex: 0.2 }]}>{socio?.plotNumber}</Text>
<Text style={[styles.cell, { flex: 3 }]}>{socio?.displayName}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{socio?.dni}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{socio?.phone}</Text>
<Text style={[styles.cell, { flex: 3 }]}>{socio?.email || ''}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{parseDate(socio?.created_at?.split('T')[0] || '')}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{parseDate(socio?.createdAt?.split('T')[0] || '')}</Text>
<Text style={[styles.cell, { flex: 1 }]}>
{(() => {
switch (socio?.type) {

View File

@@ -3,22 +3,25 @@ import { Form, Row, Col, Button } from 'react-bootstrap';
import { useDataContext } from '../../hooks/useDataContext';
import { Alert } from 'react-bootstrap';
import PropTypes from 'prop-types';
import { generateSecurePassword } from '../../util/passwordGenerator';
const PreUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
const { getData } = useDataContext();
const fetchedOnce = useRef(false);
const [form, setForm] = useState({
user_name: '',
display_name: '',
userName: '',
password: generateSecurePassword(8),
displayName: '',
dni: '',
phone: '',
email: '',
address: '',
zip_code: '',
zipCode: '',
city: '',
member_number: '',
plot_number: plotNumber,
memberNumber: '',
plotNumber: plotNumber,
type: userType,
status: 1,
role: 0
@@ -30,12 +33,10 @@ const PreUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
fetchedOnce.current = true;
try {
const { data, error } = await getData("https://api.huertosbellavista.es/v1/members/latest-number");
if (error) throw new Error(error);
const latestNumber = await getData("http://localhost:8081/v2/huertos/users/latest-number");
setForm((prev) => ({
...prev,
member_number: data.lastMemberNumber + 1
memberNumber: latestNumber + 1
}));
} catch (err) {
console.error("Error al obtener el número de socio:", err);
@@ -47,21 +48,21 @@ const PreUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
}, []);
useEffect(() => {
const trimmedName = form.display_name?.trim() ?? "";
const trimmedName = form.displayName?.trim() ?? "";
const nuevoUsername = trimmedName
? trimmedName.split(' ')[0].toLowerCase() : "";
if (form.user_name !== nuevoUsername) {
setForm(prev => ({ ...prev, user_name: nuevoUsername }));
if (form.userName !== nuevoUsername) {
setForm(prev => ({ ...prev, userName: nuevoUsername }));
}
}, [form.member_number, form.display_name, form.user_name]);
}, [form.memberNumber, form.displayName, form.userName]);
const handleChange = (e) => {
const { name, value, type } = e.target;
let updatedValue = value;
if (name === 'display_name' || name === 'dni') {
if (name === 'displayName' || name === 'dni') {
updatedValue = value.toUpperCase();
}
@@ -84,13 +85,13 @@ const PreUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
<Row className="gy-3">
{[
{ label: 'Nombre completo', name: 'display_name', type: 'text', required: true },
{ label: 'Nombre de usuario', name: 'user_name', type: 'text', required: true },
{ label: 'Nombre completo', name: 'displayName', type: 'text', required: true },
{ label: 'Nombre de usuario', name: 'userName', type: 'text', required: true },
{ label: 'DNI', name: 'dni', type: 'text', required: true, maxLength: 9 },
{ label: 'Teléfono', name: 'phone', type: 'tel', required: true },
{ label: 'Correo electrónico', name: 'email', type: 'email', required: true },
{ label: 'Domicilio', name: 'address', type: 'text' },
{ label: 'Código Postal', name: 'zip_code', type: 'text' },
{ label: 'Código Postal', name: 'zipCode', type: 'text' },
{ label: 'Ciudad', name: 'city', type: 'text' }
].map(({ label, name, type, required, maxLength }) => (
<Col md={4} key={name}>
@@ -120,8 +121,8 @@ const PreUserForm = ({ onSubmit, userType, plotNumber, errors = {} }) => {
className="shadow-sm"
disabled
type="number"
name="member_number"
value={form.member_number}
name="memberNumber"
value={form.memberNumber}
onChange={handleChange}
/>
</Form.Group>

View File

@@ -35,15 +35,15 @@ const getPFP = (tipo) => {
};
const renderDescripcionSolicitud = (data, onProfile) => {
const { request_type, request_status, requested_by_name, pre_display_name } = data;
const { type, status, requestedByName, preDisplayName } = data;
switch (request_type) {
switch (type) {
case 0:
if (requested_by_name) {
return `${requested_by_name} quiere darse de alta.`;
} else if (request_status !== 1 && pre_display_name) {
return `${pre_display_name} quiere darse de alta.`;
} else if (request_status !== 1) {
if (requestedByName) {
return `${requestedByName} quiere darse de alta.`;
} else if (status !== 1 && preDisplayName) {
return `${preDisplayName} quiere darse de alta.`;
} else if (status !== 1) {
return `Alguien quiere darse de alta.`;
} else {
return `Se ha aceptado esta solicitud de alta.`;
@@ -52,30 +52,30 @@ const renderDescripcionSolicitud = (data, onProfile) => {
case 1:
return onProfile
? "Has solicitado darte de baja."
: requested_by_name
? `${requested_by_name} quiere darse de baja.`
: request_status !== 1
: requestedByName
? `${requestedByName} quiere darse de baja.`
: status !== 1
? `Alguien quiere darse de baja.`
: `Se ha aceptado esta solicitud de baja.`;
case 2:
if (onProfile) {
switch (request_status) {
switch (status) {
case 0: return "Has solicitado añadir un colaborador.";
case 1: return "Tu solicitud de colaborador ha sido aceptada.";
case 2: return "Tu solicitud de colaborador ha sido rechazada.";
default: return "Solicitud de colaborador desconocida.";
}
} else {
switch (request_status) {
switch (status) {
case 0:
return requested_by_name
? `${requested_by_name} quiere añadir a ${pre_display_name || "un colaborador"} como colaborador.`
: `Alguien quiere añadir a ${pre_display_name || "un colaborador"} como colaborador.`;
return requestedByName
? `${requestedByName} quiere añadir a ${preDisplayName || "un colaborador"} como colaborador.`
: `Alguien quiere añadir a ${preDisplayName || "un colaborador"} como colaborador.`;
case 1:
return `La solicitud de colaborador de ${requested_by_name || "alguien"} ha sido aceptada.`;
return `La solicitud de colaborador de ${requestedByName || "alguien"} ha sido aceptada.`;
case 2:
return `La solicitud de colaborador de ${requested_by_name || "alguien"} ha sido rechazada.`;
return `La solicitud de colaborador de ${requestedByName || "alguien"} ha sido rechazada.`;
default:
return "Solicitud de colaborador desconocida.";
}
@@ -84,27 +84,27 @@ const renderDescripcionSolicitud = (data, onProfile) => {
case 3:
return onProfile
? "Has solicitado quitar tu colaborador."
: requested_by_name
? `${requested_by_name} quiere quitar su colaborador.`
: request_status !== 1
: requestedByName
? `${requestedByName} quiere quitar su colaborador.`
: status !== 1
? `Alguien quiere quitar su colaborador.`
: `Se ha aceptado esta solicitud de baja de colaborador.`;
case 4:
return onProfile
? "Has solicitado una parcela en el invernadero."
: requested_by_name
? `${requested_by_name} quiere una parcela en el invernadero.`
: request_status !== 1
: requestedByName
? `${requestedByName} quiere una parcela en el invernadero.`
: status !== 1
? `Alguien quiere una parcela en el invernadero.`
: `Se ha aceptado esta solicitud de parcela en el invernadero.`;
case 5:
return onProfile
? "Has solicitado dejar tu parcela del invernadero."
: requested_by_name
? `${requested_by_name} quiere dejar su parcela del invernadero.`
: request_status !== 1
: requestedByName
? `${requestedByName} quiere dejar su parcela del invernadero.`
: status !== 1
? `Alguien quiere dejar su parcela del invernadero.`
: `Se ha aceptado esta solicitud de salida del invernadero.`;
@@ -114,18 +114,18 @@ const renderDescripcionSolicitud = (data, onProfile) => {
};
const SolicitudCard = ({ data, onAccept, onReject, onDelete, editable = true, onProfile = false }) => {
const handleDelete = () => typeof onDelete === "function" && onDelete(data.request_id);
const handleDelete = () => typeof onDelete === "function" && onDelete(data.requestId);
return (
<MotionCard className="solicitud-card shadow-sm rounded-4 h-100">
<Card.Header className="rounded-top-4 d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center">
<img src={getPFP(data.pre_type)} width="36" className="rounded me-3" alt="PFP" />
<img src={getPFP(data.preType)} width="36" className="rounded me-3" alt="PFP" />
<div>
<Card.Title className="mb-0">
Solicitud #{data.request_id} - {getTipoSolicitud(data.request_type)}
Solicitud #{data.requestId} - {getTipoSolicitud(data.type)}
</Card.Title>
<small className='state-small'>Estado: <strong>{getEstadoSolicitud(data.request_status)}</strong></small>
<small className='state-small'>Estado: <strong>{getEstadoSolicitud(data.status)}</strong></small>
</div>
</div>
@@ -147,7 +147,7 @@ const SolicitudCard = ({ data, onAccept, onReject, onDelete, editable = true, on
<ListGroup variant="flush" className="border rounded-3 mb-3">
<ListGroup.Item>
<FontAwesomeIcon icon={faCalendar} className="me-2" />
Fecha de solicitud: <strong>{parseDate(data.request_created_at)}</strong>
Fecha de solicitud: <strong>{parseDate(data.request_createdAt)}</strong>
</ListGroup.Item>
</ListGroup>
@@ -157,24 +157,24 @@ const SolicitudCard = ({ data, onAccept, onReject, onDelete, editable = true, on
</ListGroup.Item>
</ListGroup>
{data.pre_display_name && (
{data.preDisplayName && (
<>
<Card.Subtitle className="card-subtitle mt-3 mb-2">Datos del futuro socio</Card.Subtitle>
<ListGroup variant="flush" className="border rounded-3">
<ListGroup.Item><FontAwesomeIcon icon={faUser} className="me-2" />Nombre: <strong>{data.pre_display_name}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faIdCard} className="me-2" />DNI: <strong>{data.pre_dni}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faPhone} className="me-2" />Teléfono: <strong>{data.pre_phone}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faEnvelope} className="me-2" />Email: <strong>{data.pre_email}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faHome} className="me-2" />Dirección: <strong>{data.pre_address ?? 'NO'}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faMapMarkerAlt} className="me-2" />Ciudad: <strong>{data.pre_city ?? 'NO'} ({data.pre_zip_code ?? 'NO'})</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faHashtag} className="me-2" /> socio: <strong>{data.pre_member_number ?? 'NO'}</strong> | huerto: <strong>{data.pre_plot_number ?? 'NO'}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faSeedling} className="me-2" />Tipo: <strong>{['Lista de Espera', 'Hortelano', 'Hortelano + Invernadero', 'Colaborador'][data.pre_type]}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faUserShield} className="me-2" />Rol: <strong>{['Usuario', 'Admin', 'Desarrollador'][data.pre_role]}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faUser} className="me-2" />Nombre: <strong>{data.preDisplayName}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faIdCard} className="me-2" />DNI: <strong>{data.preDni}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faPhone} className="me-2" />Teléfono: <strong>{data.prePhone}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faEnvelope} className="me-2" />Email: <strong>{data.preEmail}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faHome} className="me-2" />Dirección: <strong>{data.preAddress ?? 'NO'}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faMapMarkerAlt} className="me-2" />Ciudad: <strong>{data.preCity ?? 'NO'} ({data.preZipCode ?? 'NO'})</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faHashtag} className="me-2" /> socio: <strong>{data.preMemberNumber ?? 'NO'}</strong> | huerto: <strong>{data.prePlotNumber ?? 'NO'}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faSeedling} className="me-2" />Tipo: <strong>{['Lista de Espera', 'Hortelano', 'Hortelano + Invernadero', 'Colaborador'][data.preType]}</strong></ListGroup.Item>
<ListGroup.Item><FontAwesomeIcon icon={faUserShield} className="me-2" />Rol: <strong>{['Usuario', 'Admin', 'Desarrollador'][data.preRole]}</strong></ListGroup.Item>
</ListGroup>
</>
)}
{editable && data.request_status === 0 && (
{editable && data.status === 0 && (
<div className="d-flex justify-content-end gap-2 mt-3">
<Button variant="danger" size="sm" onClick={() => onReject?.(data)}>Rechazar</Button>
<Button variant="success" size="sm" onClick={() => onAccept?.(data)}>Aceptar</Button>