[REPO REFACTOR]: changed to a better git repository structure with branches

This commit is contained in:
2025-11-01 03:57:48 +01:00
parent 85f98f66b4
commit 3dd17352aa
156 changed files with 15648 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
import { useEffect, useState } from 'react';
import {
Card, Badge, Button, Form
} from 'react-bootstrap';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faMoneyBillWave,
faTruck,
faReceipt,
faTrash,
faEdit,
faTimes,
faEllipsisVertical
} from '@fortawesome/free-solid-svg-icons';
import { motion as _motion } from 'framer-motion';
import AnimatedDropdown from '../../components/AnimatedDropdown';
import { useTheme } from '../../hooks/useTheme';
import '../../css/IngresoCard.css';
import { CONSTANTS } from '../../util/constants';
import { DateParser } from '../../util/parsers/dateParser';
import { renderErrorAlert } from '../../util/alertHelpers';
import { getNowAsLocalDatetime } from '../../util/date';
import SpanishDateTimePicker from '../SpanishDateTimePicker';
const MotionCard = _motion.create(Card);
const getTypeLabel = (type) => type === CONSTANTS.PAYMENT_TYPE_BANK ? "Banco" : "Caja";
const getTypeColor = (type, theme) => type === 0 ? "primary" : theme === "light" ? "dark" : "light";
const getTypeTextColor = (type, theme) => type === 0 ? "light" : theme === "light" ? "light" : "dark";
const getPFP = (tipo) => {
const base = '/images/icons/';
const map = {
1: 'cash.svg',
0: 'bank.svg'
};
return base + (map[tipo] || 'farmer.svg');
};
const GastoCard = ({ gasto, isNew = false, onCreate, onUpdate, onDelete, onCancel, error, onClearError }) => {
const createMode = isNew;
const [editMode, setEditMode] = useState(createMode);
const { theme } = useTheme();
const [formData, setFormData] = useState({
concept: gasto.concept || '',
amount: gasto.amount || 0,
supplier: gasto.supplier || '',
invoice: gasto.invoice || '',
type: gasto.type ?? 0,
created_at: gasto.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
useEffect(() => {
if (!editMode) {
setFormData({
concept: gasto.concept || '',
amount: gasto.amount || 0,
supplier: gasto.supplier || '',
invoice: gasto.invoice || '',
type: gasto.type ?? 0,
created_at: gasto.created_at?.slice(0, 16) || (isNew ? getNowAsLocalDatetime() : ''),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gasto, editMode]);
const handleChange = (field, value) => setFormData(prev => ({ ...prev, [field]: value }));
const handleDelete = () => typeof onDelete === 'function' && onDelete(gasto.expense_id);
const handleCancel = () => {
if (onClearError) onClearError();
if (isNew && typeof onCancel === 'function') return onCancel();
setEditMode(false);
};
const handleSave = () => {
if (onClearError) onClearError();
const newExpense = { ...gasto, ...formData };
if (createMode && typeof onCreate === 'function') return onCreate(newExpense);
if (typeof onUpdate === 'function') return onUpdate(newExpense, gasto.expense_id);
};
return (
<MotionCard className="ingreso-card shadow-sm rounded-4 border-0 h-100">
<Card.Header className="d-flex justify-content-between align-items-center rounded-top-4 bg-light-green">
<div className="d-flex align-items-center">
<img src={getPFP(formData.type)} width={36} alt="Tipo de gasto" className='me-3' />
<div className="d-flex flex-column">
<span className="fw-bold">
{editMode ? (
<Form.Control
className="themed-input"
size="sm"
value={formData.concept}
onChange={(e) => handleChange('concept', e.target.value.toUpperCase())}
/>
) : formData.concept}
</span>
<small>
{editMode ? (
<SpanishDateTimePicker
selected={new Date(formData.created_at)}
onChange={(date) =>
handleChange('created_at', date.toISOString().slice(0, 16))
}
/>
) : (
DateParser.isoToStringWithTime(formData.created_at)
)}
</small>
</div>
</div>
{!createMode && !editMode && (
<AnimatedDropdown className='end-0' icon={<FontAwesomeIcon icon={faEllipsisVertical} className="fa-xl text-dark" />}>
{({ closeDropdown }) => (
<>
<div className="dropdown-item d-flex align-items-center" onClick={() => { setEditMode(true); closeDropdown(); }}>
<FontAwesomeIcon icon={faEdit} className="me-2" />Editar
</div>
<div className="dropdown-item d-flex align-items-center text-danger" onClick={() => { handleDelete(); closeDropdown(); }}>
<FontAwesomeIcon icon={faTrash} className="me-2" />Eliminar
</div>
</>
)}
</AnimatedDropdown>
)}
</Card.Header>
<Card.Body>
{(editMode || createMode) && renderErrorAlert(error)}
<Card.Text className="mb-2">
<FontAwesomeIcon icon={faMoneyBillWave} className="me-2" />
<strong>Importe:</strong>{' '}
{editMode ? (
<Form.Control className="themed-input" size="sm" type="number" step="0.01" value={formData.amount} onChange={(e) => handleChange('amount', parseFloat(e.target.value))} style={{ maxWidth: '150px', display: 'inline-block' }} />
) : `${formData.amount.toFixed(2)}`}
</Card.Text>
<Card.Text className="mb-2">
<FontAwesomeIcon icon={faTruck} className="me-2" />
<strong>Proveedor:</strong>{' '}
{editMode ? (
<Form.Control className="themed-input" size="sm" type="text" value={formData.supplier} onChange={(e) => handleChange('supplier', e.target.value)} />
) : formData.supplier}
</Card.Text>
<Card.Text className="mb-2">
<FontAwesomeIcon icon={faReceipt} className="me-2" />
<strong>Factura:</strong>{' '}
{editMode ? (
<Form.Control className="themed-input" size="sm" type="text" value={formData.invoice} onChange={(e) => handleChange('invoice', e.target.value)} />
) : formData.invoice}
</Card.Text>
{editMode ? (
<>
<Form.Group className="mb-3">
<Form.Label>Tipo de gasto</Form.Label>
<Form.Select className='themed-input' size="sm" value={formData.type} onChange={(e) => handleChange('type', parseInt(e.target.value))}>
<option value={0}>Banco</option>
<option value={1}>Caja</option>
</Form.Select>
</Form.Group>
<div className="d-flex justify-content-end gap-2">
<Button variant="secondary" size="sm" onClick={handleCancel}><FontAwesomeIcon icon={faTimes} /> Cancelar</Button>
<Button variant="primary" size="sm" onClick={handleSave}>Guardar</Button>
</div>
</>
) : (
<div className="text-end">
<Badge bg={getTypeColor(formData.type, theme)} text={getTypeTextColor(formData.type, theme)}>
{getTypeLabel(formData.type)}
</Badge>
</div>
)}
</Card.Body>
</MotionCard>
);
};
GastoCard.propTypes = {
gasto: PropTypes.object.isRequired,
isNew: PropTypes.bool,
onCreate: PropTypes.func,
onUpdate: PropTypes.func,
onDelete: PropTypes.func,
onCancel: PropTypes.func
};
export default GastoCard;

View File

@@ -0,0 +1,68 @@
import PropTypes from 'prop-types';
const GastosFilter = ({ filters, onChange }) => {
const handleCheckboxChange = (key) => {
if (key === 'todos') {
const newValue = !filters.todos;
onChange({
todos: newValue,
banco: newValue,
caja: newValue
});
} else {
const updated = { ...filters, [key]: !filters[key] };
const allTrue = Object.entries(updated)
.filter(([k]) => k !== 'todos')
.every(([, v]) => v === true);
updated.todos = allTrue;
onChange(updated);
}
};
return (
<>
<div className="dropdown-item d-flex align-items-center">
<input
type="checkbox"
id="todosCheck"
className="me-2"
checked={filters.todos}
onChange={() => handleCheckboxChange('todos')}
/>
<label htmlFor="todosCheck" className="m-0">Mostrar Todos</label>
</div>
<hr className="dropdown-divider" />
<div className="dropdown-item d-flex align-items-center">
<input
type="checkbox"
id="bancoCheck"
className="me-2"
checked={filters.banco}
onChange={() => handleCheckboxChange('banco')}
/>
<label htmlFor="bancoCheck" className="m-0">Banco</label>
</div>
<div className="dropdown-item d-flex align-items-center">
<input
type="checkbox"
id="cajaCheck"
className="me-2"
checked={filters.caja}
onChange={() => handleCheckboxChange('caja')}
/>
<label htmlFor="cajaCheck" className="m-0">Caja</label>
</div>
</>
);
};
GastosFilter.propTypes = {
filters: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired
};
export default GastosFilter;

View File

@@ -0,0 +1,117 @@
import { Document, Page, Text, View, StyleSheet, Font, Image } from '@react-pdf/renderer';
import { CONSTANTS } from '../../util/constants';
Font.register({
family: 'Open Sans',
fonts: [{ src: '/fonts/OpenSans.ttf', fontWeight: 'normal' }]
});
const styles = StyleSheet.create({
page: {
padding: 25,
fontSize: 12,
fontFamily: 'Open Sans',
backgroundColor: '#F8F9FA',
},
headerContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 25,
justifyContent: 'left',
},
headerText: {
flexDirection: 'column',
marginLeft: 25,
},
logo: {
width: 60,
height: 60,
},
header: {
fontSize: 26,
fontWeight: 'bold',
color: '#2C3E50',
},
subHeader: {
fontSize: 12,
marginTop: 5,
color: '#34495E'
},
tableHeader: {
flexDirection: 'row',
backgroundColor: '#3E8F5A',
fontWeight: 'bold',
paddingVertical: 6,
paddingHorizontal: 5,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
},
headerCell: {
paddingHorizontal: 5,
color: '#ffffff',
fontWeight: 'bold',
fontSize: 10,
},
row: {
flexDirection: 'row',
paddingVertical: 5,
paddingHorizontal: 5,
borderBottomWidth: 1,
borderBottomColor: '#D5D8DC'
},
cell: {
paddingHorizontal: 5,
fontSize: 9,
color: '#2C3E50'
}
});
const parseDate = (iso) => {
if (!iso) return '';
const [y, m, d] = iso.split('T')[0].split('-');
return `${d}/${m}/${y}`;
};
const getTypeLabel = (type) => type === CONSTANTS.PAYMENT_TYPE_BANK ? 'Banco' : 'Caja';
export const GastosPDF = ({ gastos }) => (
<Document>
<Page size="A4" orientation="landscape" style={styles.page}>
<View style={styles.headerContainer}>
<Image src="/images/logo.png" style={styles.logo} />
<View style={styles.headerText}>
<Text style={styles.header}>Listado de Gastos</Text>
<Text style={styles.subHeader}>Asociación Huertos La Salud - Bellavista Generado el {new Date().toLocaleDateString()} a las {new Date().toLocaleTimeString()}</Text>
</View>
</View>
<View style={styles.tableHeader}>
<Text style={[styles.headerCell, { flex: 2 }]}>Fecha</Text>
<Text style={[styles.headerCell, { flex: 4 }]}>Concepto</Text>
<Text style={[styles.headerCell, { flex: 2 }]}>Importe</Text>
<Text style={[styles.headerCell, { flex: 3 }]}>Proveedor</Text>
<Text style={[styles.headerCell, { flex: 2 }]}>Factura</Text>
<Text style={[styles.headerCell, { flex: 1 }]}>Tipo</Text>
</View>
{gastos.map((gasto, idx) => (
<View
key={idx}
style={[
styles.row,
{ backgroundColor: idx % 2 === 0 ? '#ECF0F1' : '#FDFEFE' },
{ borderBottomLeftRadius: idx === gastos.length - 1 ? 10 : 0 },
{ borderBottomRightRadius: idx === gastos.length - 1 ? 10 : 0 },
]}
>
<Text style={[styles.cell, { flex: 2 }]}>{parseDate(gasto.created_at)}</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>
<Text style={[styles.cell, { flex: 2 }]}>{gasto.invoice}</Text>
<Text style={[styles.cell, { flex: 1 }]}>{getTypeLabel(gasto.type)}</Text>
</View>
))}
</Page>
</Document>
);