- Backend Node.js/Express con PostgreSQL - Frontend React 19 con Vite - Docker Compose para orquestacion - Documentacion completa en README.md - Scripts SQL para base de datos - Configuracion de ejemplo (.env.example)
799 lines
25 KiB
JavaScript
799 lines
25 KiB
JavaScript
import React, { useEffect, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import "./NewExpense.css";
|
|
import { useContext } from "react";
|
|
import { langContext } from "../../context/LenguageContext";
|
|
import { AuthContext } from "../../context/AuthContext";
|
|
|
|
export default function NewExpense() {
|
|
const { lang } = useContext(langContext);
|
|
const { userData } = useContext(AuthContext);
|
|
const navigate = useNavigate();
|
|
|
|
const isEditMode = false;
|
|
|
|
const categoryRequiresProducts = (categoryId) => {
|
|
return categoryId === 1 || categoryId === 10;
|
|
};
|
|
const [form, setForm] = useState({
|
|
new_description: "",
|
|
suppliers_id: "",
|
|
new_request_date: "",
|
|
new_payment_deadline: "",
|
|
area: "",
|
|
expense_cat: "",
|
|
currency_id: "",
|
|
new_tax: 1,
|
|
subtotal: 0,
|
|
iva: 0,
|
|
ieps: 0,
|
|
total: 0,
|
|
needtoapprove: false,
|
|
products: [],
|
|
});
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [success, setSuccess] = useState(false);
|
|
const [error, setError] = useState("");
|
|
|
|
// estados para datos de selects
|
|
const [suppliers, setSuppliers] = useState([]);
|
|
const [areas, setAreas] = useState([]);
|
|
const [categories, setCategories] = useState([]);
|
|
const [currencies, setCurrencies] = useState([]);
|
|
const [taxes, setTaxes] = useState([]);
|
|
const [listproducts, setListProducts] = useState([]);
|
|
|
|
useEffect(() => {
|
|
async function fetchSelectData() {
|
|
try {
|
|
const res = await fetch(
|
|
import.meta.env.VITE_API_BASE_URL + "/expenses/getinfo",
|
|
{
|
|
method: "GET",
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
if (!res.ok) throw new Error(`Error fetching info: ${res.status}`);
|
|
const data = await res.json();
|
|
// Supongo que data tiene algo como:
|
|
// { suppliers: [...], areas: [...], categories: [...], currencies: [...] }
|
|
setSuppliers(data.suppliers || []);
|
|
setAreas(data.areas || []);
|
|
setCategories(data.categories || []);
|
|
setCurrencies(data.currencies || []);
|
|
setTaxes(data.tax || []);
|
|
} catch (err) {
|
|
console.error("Error cargando metadata (getinfo):", err);
|
|
}
|
|
}
|
|
// Helper para cargar opciones de selects
|
|
const fetchOptions = (url, setter, mapFn) => {
|
|
fetch(import.meta.env.VITE_API_BASE_URL + url)
|
|
.then((res) => res.json())
|
|
.then((data) => {
|
|
const items =
|
|
data.data ||
|
|
data.currency ||
|
|
data.categoryex ||
|
|
data.approve ||
|
|
data.request;
|
|
if (items) {
|
|
setter(items.map(mapFn));
|
|
}
|
|
})
|
|
.catch((err) => console.error(`Error fetching ${url}`, err));
|
|
};
|
|
fetchOptions("/products", (data) => {
|
|
const sortedProducts = data.map(c => ({
|
|
id: c.id_product,
|
|
name: c.name_product,
|
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
setListProducts(sortedProducts);
|
|
}, (c) => c);
|
|
fetchSelectData();
|
|
}, []);
|
|
|
|
const handleChange = (e) => {
|
|
const { name, type, checked, value } = e.target;
|
|
|
|
// Crear una copia del formulario
|
|
const updatedForm = {
|
|
...form,
|
|
[name]: type === "checkbox" ? checked : value,
|
|
};
|
|
// Inicializar valores por si están vacíos
|
|
const subtotal = parseFloat(updatedForm.subtotal) || 0;
|
|
const ieps = parseFloat(updatedForm.ieps) || 0;
|
|
|
|
let ivaAmount = 0;
|
|
|
|
if (name === "total" && !categoryRequiresProducts(Number(updatedForm.expense_cat))) {
|
|
const totalValue = parseFloat(value) || 0;
|
|
const iepsValue = parseFloat(updatedForm.ieps) || 0;
|
|
const taxRate = parseFloat(
|
|
taxes.find((t) => t.id === parseInt(updatedForm.new_tax))?.number || 0
|
|
);
|
|
|
|
if (taxRate > 0) {
|
|
const subtotalBeforeIeps = totalValue - iepsValue;
|
|
const calculatedSubtotal = subtotalBeforeIeps / (1 + taxRate);
|
|
const calculatedIva = calculatedSubtotal * taxRate;
|
|
|
|
updatedForm.subtotal = calculatedSubtotal;
|
|
updatedForm.iva = calculatedIva;
|
|
updatedForm.total = totalValue;
|
|
} else {
|
|
updatedForm.subtotal = totalValue - iepsValue;
|
|
updatedForm.iva = 0;
|
|
updatedForm.total = totalValue;
|
|
}
|
|
|
|
setForm({
|
|
...updatedForm,
|
|
subtotal: updatedForm.subtotal.toFixed(2),
|
|
iva: updatedForm.iva.toFixed(2),
|
|
total: updatedForm.total,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Si hay productos, recalcular desde ellos
|
|
if (updatedForm.products.length > 0) {
|
|
updatedForm.subtotal = updatedForm.products.reduce(
|
|
(acc, p) => acc + (parseFloat(p.subtotal) || 0),
|
|
0
|
|
);
|
|
|
|
ivaAmount = updatedForm.products.reduce(
|
|
(acc, p) => acc + (parseFloat(p.iva) || 0),
|
|
0
|
|
);
|
|
console.log(ivaAmount);
|
|
} else {
|
|
const taxRate = parseFloat(
|
|
taxes.find((t) => t.id === parseInt(updatedForm.new_tax))?.number || 0
|
|
);
|
|
console.log(taxRate);
|
|
ivaAmount = subtotal * taxRate;
|
|
}
|
|
|
|
updatedForm.iva = ivaAmount;
|
|
|
|
// Calcular total final (subtotal + IVA + IEPS)
|
|
updatedForm.total = subtotal + ivaAmount + ieps;
|
|
|
|
// Actualizar estado
|
|
setForm({
|
|
...updatedForm,
|
|
iva: updatedForm.iva.toFixed(2),
|
|
total: name === "total" ? updatedForm.total : updatedForm.total.toFixed(2),
|
|
});
|
|
};
|
|
|
|
const handleCategoryChange = (e) => {
|
|
const { name, value } = e.target;
|
|
const categoryValue = Number(value) || 0;
|
|
setForm((prev) => {
|
|
const updatedForm = { ...prev, [name]: categoryValue };
|
|
if (categoryRequiresProducts(categoryValue)) {
|
|
updatedForm.products = [
|
|
{
|
|
id_product: null,
|
|
quantity: 1,
|
|
unit_cost: 0,
|
|
id_tax: 1,
|
|
subtotal: 0,
|
|
iva: 0,
|
|
},
|
|
];
|
|
updatedForm.new_tax = "";
|
|
} else {
|
|
// Si cambia a otra categoría, vaciamos los productos
|
|
updatedForm.products = [];
|
|
}
|
|
return updatedForm;
|
|
});
|
|
};
|
|
|
|
const handleProductChange = (index, field, value) => {
|
|
const updatedProducts = [...form.products]; // Convertir a entero si es un campo numérico esperado
|
|
const ieps = parseFloat(form.ieps) || 0;
|
|
const processedValue = ["unit_cost"].includes(field)
|
|
? parseFloat(value)
|
|
: ["quantity", "id_tax", "id_product"].includes(field)
|
|
? parseInt(value)
|
|
: value;
|
|
updatedProducts[index][field] = processedValue;
|
|
|
|
if (field === "quantity" || field === "unit_cost" || field === "id_tax") {
|
|
const qty = parseFloat(updatedProducts[index].quantity) || 0;
|
|
const cost = parseFloat(updatedProducts[index].unit_cost) || 0;
|
|
const taxValue = parseFloat(
|
|
taxes.find((t) => t.id === parseInt(updatedProducts[index].id_tax))
|
|
?.number || 0
|
|
);
|
|
updatedProducts[index].subtotal = qty * cost;
|
|
updatedProducts[index].iva = qty * cost * taxValue;
|
|
}
|
|
|
|
// Recalcular el amount (suma de todos los productos)
|
|
const subtotalAmount = updatedProducts.reduce(
|
|
(acc, p) => acc + (parseFloat(p.subtotal) || 0),
|
|
0
|
|
);
|
|
|
|
// Recalcular el amount (suma de todos los productos)
|
|
const ivaAmount = updatedProducts.reduce(
|
|
(acc, p) => acc + (parseFloat(p.iva) || 0),
|
|
0
|
|
);
|
|
|
|
setError(null);
|
|
setSuccess(false);
|
|
|
|
setForm((prev) => ({
|
|
...prev,
|
|
products: updatedProducts,
|
|
subtotal: subtotalAmount.toFixed(2), // redondeado a 2 decimales
|
|
iva: ivaAmount.toFixed(2), // redondeado a 2 decimales
|
|
total: (subtotalAmount + ivaAmount + ieps).toFixed(2), // Calcular total final (subtotal + IVA + IEPS)
|
|
}));
|
|
};
|
|
|
|
const handleAddProduct = () => {
|
|
const ieps = parseFloat(form.ieps) || 0;
|
|
|
|
// Primero, agregamos el nuevo producto
|
|
const updatedProducts = [
|
|
...form.products,
|
|
{
|
|
id_product: null,
|
|
quantity: 1,
|
|
unit_cost: 0,
|
|
id_tax: "",
|
|
subtotal: 0,
|
|
iva: 0,
|
|
},
|
|
];
|
|
|
|
// Ahora sí, recalculamos después de agregarlo
|
|
const subtotalAmount = updatedProducts.reduce(
|
|
(acc, p) => acc + (parseFloat(p.subtotal) || 0),
|
|
0
|
|
);
|
|
|
|
const ivaAmount = updatedProducts.reduce(
|
|
(acc, p) => acc + (parseFloat(p.iva) || 0),
|
|
0
|
|
);
|
|
|
|
setForm((prev) => ({
|
|
...prev,
|
|
products: updatedProducts,
|
|
subtotal: subtotalAmount.toFixed(2),
|
|
iva: ivaAmount.toFixed(2),
|
|
total: (subtotalAmount + ivaAmount + ieps).toFixed(2),
|
|
}));
|
|
};
|
|
|
|
const handleRemoveProduct = (index) => {
|
|
setForm((prev) => {
|
|
const updated = [...prev.products];
|
|
updated.splice(index, 1);
|
|
|
|
// Recalcular el amount (suma de todos los productos)
|
|
const subtotalAmount = updated.reduce(
|
|
(acc, p) => acc + (parseFloat(p.subtotal) || 0),
|
|
0
|
|
);
|
|
|
|
// Recalcular el amount (suma de todos los productos)
|
|
const ivaAmount = updated.reduce(
|
|
(acc, p) => acc + (parseFloat(p.iva) || 0),
|
|
0
|
|
);
|
|
|
|
const ieps = parseFloat(prev.ieps) || 0;
|
|
|
|
return {
|
|
...prev,
|
|
products: updated,
|
|
subtotal: subtotalAmount.toFixed(2), // redondeado a 2 decimales
|
|
iva: ivaAmount.toFixed(2), // redondeado a 2 decimales
|
|
total: (subtotalAmount + ivaAmount + ieps).toFixed(2), // Calcular total final (subtotal + IVA + IEPS)
|
|
};
|
|
});
|
|
};
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setSuccess(false);
|
|
setError("");
|
|
|
|
// ✅ Validar campos obligatorios antes de enviar
|
|
if (
|
|
!form.suppliers_id ||
|
|
!form.area ||
|
|
!form.expense_cat ||
|
|
!form.currency_id ||
|
|
!form.total
|
|
) {
|
|
setError(
|
|
lang === "en"
|
|
? "Please fill in all required fields (Supplier, Area, Category, Currency, total)."
|
|
: "Por favor llena todos los campos obligatorios (Proveedor, Área, Categoría, Moneda, Monto)."
|
|
);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const payload = {
|
|
new_description: form.new_description,
|
|
suppliers_id: Number(form.suppliers_id),
|
|
new_request_date: form.new_request_date,
|
|
new_payment_deadline: form.new_payment_deadline,
|
|
request_by: Number(userData.user_id),
|
|
area: Number(form.area),
|
|
expense_cat: Number(form.expense_cat),
|
|
currency_id: Number(form.currency_id),
|
|
needtoapprove: form.needtoapprove,
|
|
products: form.products,
|
|
new_subtotal: Number(form.subtotal),
|
|
new_iva: Number(form.iva),
|
|
new_ieps: Number(form.ieps),
|
|
new_total: Number(form.total),
|
|
};
|
|
|
|
// 🔍 LOGS para depurar el error 500
|
|
console.group("🧾 Envío de nuevo gasto");
|
|
console.log("Payload completo que se enviará al backend:", payload);
|
|
console.log("Tipo de cada campo:");
|
|
Object.entries(payload).forEach(([k, v]) =>
|
|
console.log(`${k}:`, v, typeof v)
|
|
);
|
|
console.groupEnd();
|
|
|
|
// Enviar petición
|
|
console.log('payload', payload);
|
|
const res = await fetch(
|
|
import.meta.env.VITE_API_BASE_URL + "/expenses/newexpense",
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
}
|
|
);
|
|
console.log('res', res);
|
|
|
|
const text = await res.text();
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
data = text;
|
|
}
|
|
|
|
// 🧩 Nuevo: Log para inspeccionar respuesta cruda
|
|
console.group("📨 Respuesta del servidor");
|
|
console.log("Status:", res.status);
|
|
console.log("Texto recibido:", text);
|
|
console.groupEnd();
|
|
|
|
if (!res.ok) {
|
|
console.error("Error response:", res.status, data);
|
|
throw new Error(`Server error ${res.status}: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
setSuccess(true);
|
|
|
|
// limpiar formulario
|
|
setForm({
|
|
new_description: "",
|
|
suppliers_id: "",
|
|
new_request_date: "",
|
|
new_payment_deadline: "",
|
|
area: "",
|
|
expense_cat: "",
|
|
currency_id: "",
|
|
subtotal: 0,
|
|
needtoapprove: false,
|
|
iva: 0,
|
|
ieps: 0,
|
|
total: 0,
|
|
products: [],
|
|
});
|
|
} catch (err) {
|
|
console.error("❌ handleSubmit error:", err);
|
|
setError(err.message || "Unexpected error");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="new-expense-container">
|
|
<div className="form-card">
|
|
<div className="form-header">
|
|
<div className="form-id">{lang === "en" ? "New expense" : "Nuevo gasto"}</div>
|
|
<button
|
|
type="button"
|
|
className="back-button"
|
|
onClick={() => navigate("/app/report-expense")}
|
|
>
|
|
<span>←</span>
|
|
{lang === "en" ? "Back" : "Volver"}
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<div className="form-grid">
|
|
<div>
|
|
<label>{lang === "en" ? "Description" : "Descripción"}</label>
|
|
<input
|
|
name="new_description"
|
|
value={form.new_description}
|
|
onChange={handleChange}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label>{lang === "en" ? "Supplier" : "Proveedor"}</label>
|
|
<select
|
|
name="suppliers_id"
|
|
value={form.suppliers_id}
|
|
onChange={handleChange}
|
|
required
|
|
>
|
|
<option value="">
|
|
{lang === "en" ? "Select supplier" : "Seleccionar proveedor"}
|
|
</option>
|
|
{suppliers
|
|
.sort((a, b) => {
|
|
const nameA = (a.name || '').toLowerCase();
|
|
const nameB = (b.name || '').toLowerCase();
|
|
return nameA.localeCompare(nameB);
|
|
})
|
|
.map((s) => (
|
|
<option key={s.id} value={s.id}>
|
|
{s.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label>
|
|
{lang === "en" ? "Request date" : "Fecha de solicitud"}
|
|
</label>
|
|
<input
|
|
type="date"
|
|
name="new_request_date"
|
|
value={form.new_request_date}
|
|
onChange={handleChange}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label>{lang === "en" ? "Area" : "Área"}</label>
|
|
<select
|
|
name="area"
|
|
value={form.area}
|
|
onChange={handleChange}
|
|
required
|
|
>
|
|
<option value="">
|
|
{lang === "en" ? "Select area" : "Seleccionar área"}
|
|
</option>
|
|
{areas
|
|
.sort((a, b) => {
|
|
const nameA = (a.name || '').toLowerCase();
|
|
const nameB = (b.name || '').toLowerCase();
|
|
return nameA.localeCompare(nameB);
|
|
})
|
|
.map((a) => (
|
|
<option key={a.id} value={a.id}>
|
|
{a.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label>{lang === "en" ? "Category" : "Categoría"}</label>
|
|
<select
|
|
name="expense_cat"
|
|
value={form.expense_cat}
|
|
onChange={handleCategoryChange}
|
|
required
|
|
>
|
|
<option value="">
|
|
{lang === "en" ? "Select category" : "Seleccionar categoría"}
|
|
</option>
|
|
{categories
|
|
.sort((a, b) => {
|
|
const nameA = (lang === "en" ? (a.name || '') : (a.spanish_name || a.name || '')).toLowerCase();
|
|
const nameB = (lang === "en" ? (b.name || '') : (b.spanish_name || b.name || '')).toLowerCase();
|
|
return nameA.localeCompare(nameB);
|
|
})
|
|
.map((c) => {
|
|
const displayName = lang === "en" ? (c.name || '') : (c.spanish_name || c.name || '');
|
|
return (
|
|
<option key={c.id} value={c.id}>
|
|
{displayName}
|
|
</option>
|
|
);
|
|
})}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label>
|
|
{lang === "en" ? "Payment deadline" : "Fecha límite de pago"}
|
|
</label>
|
|
<input
|
|
type="date"
|
|
name="new_payment_deadline"
|
|
value={form.new_payment_deadline}
|
|
onChange={handleChange}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label>{lang === "en" ? "Currency" : "Moneda"}</label>
|
|
<select
|
|
name="currency_id"
|
|
value={form.currency_id}
|
|
onChange={handleChange}
|
|
required
|
|
>
|
|
<option value="">
|
|
{lang === "en" ? "Select currency" : "Seleccionar moneda"}
|
|
</option>
|
|
{currencies.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label>{"Subtotal"}</label>
|
|
<input
|
|
name="subtotal"
|
|
value={form.subtotal}
|
|
onChange={handleChange}
|
|
type="number"
|
|
required
|
|
disabled={categoryRequiresProducts(Number(form.expense_cat))}
|
|
/>
|
|
</div>
|
|
|
|
{!categoryRequiresProducts(Number(form.expense_cat)) && (
|
|
<div>
|
|
<label>{lang === "en" ? "VAT rate:" : "Tasa de IVA:"}</label>
|
|
<select
|
|
name="new_tax"
|
|
value={form.new_tax}
|
|
onChange={handleChange}
|
|
disabled={categoryRequiresProducts(Number(form.expense_cat))}
|
|
>
|
|
<option value="" disabled>
|
|
{lang === "en" ? "Select Tax" : "Seleccione Impuesto"}
|
|
</option>
|
|
{taxes.map((tax) => (
|
|
<option key={tax.id} value={tax.id}>
|
|
{tax.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<label>{lang === "en" ? "VAT" : "IVA"}</label>
|
|
<input
|
|
name="iva"
|
|
value={form.iva}
|
|
onChange={handleChange}
|
|
type="number"
|
|
required
|
|
disabled={categoryRequiresProducts(Number(form.expense_cat))}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label>{"IEPS"}</label>
|
|
<input
|
|
name="ieps"
|
|
value={form.ieps}
|
|
onChange={handleChange}
|
|
type="number"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label>{"Total"}</label>
|
|
<input
|
|
name="total"
|
|
value={form.total}
|
|
onChange={handleChange}
|
|
type="number"
|
|
required
|
|
disabled={isEditMode}
|
|
/>
|
|
</div>
|
|
|
|
{/* Need to Approve (Campo solicitado en la estructura) */}
|
|
<div className="checkbox-field">
|
|
<label>{lang === "en" ? "Needs Approval:" : "Requiere aprobación:"}</label>
|
|
<input
|
|
type="checkbox"
|
|
name="needtoapprove"
|
|
checked={form.needtoapprove}
|
|
onChange={handleChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sección de Productos */}
|
|
{categoryRequiresProducts(Number(form.expense_cat)) && (
|
|
<div className="add-products">
|
|
<h3 style={{ marginTop: "20px" }}>
|
|
{lang === "en" ? "Products" : "Productos"}
|
|
</h3>
|
|
|
|
<div
|
|
style={{
|
|
border: "1px solid #ccc",
|
|
padding: "10px",
|
|
marginTop: "10px",
|
|
}}
|
|
>
|
|
{form.products.map((product, index) => (
|
|
<div
|
|
key={index}
|
|
style={{
|
|
marginBottom: "10px",
|
|
borderBottom: "1px dotted #eee",
|
|
paddingBottom: "5px",
|
|
display: "grid",
|
|
gridTemplateColumns: "repeat(3, 1fr)",
|
|
gap: "10px",
|
|
}}
|
|
>
|
|
<div>
|
|
<label>
|
|
{lang === "en" ? "Product ID:" : "ID Producto:"}
|
|
</label>
|
|
<select
|
|
name="product_id"
|
|
value={product.id_product || ""}
|
|
onChange={(e) =>
|
|
handleProductChange(
|
|
index,
|
|
"id_product",
|
|
e.target.value
|
|
)
|
|
}
|
|
>
|
|
<option value="" disabled>
|
|
{lang === "en"
|
|
? "Select Product ID"
|
|
: "Seleccione ID Producto"}
|
|
</option>
|
|
{listproducts.map((lp) => (
|
|
<option key={lp.id} value={lp.id}>
|
|
{lp.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>{lang === "en" ? "Quantity:" : "Cantidad:"}</label>
|
|
<input
|
|
type="number"
|
|
value={product.quantity}
|
|
onChange={(e) =>
|
|
handleProductChange(index, "quantity", e.target.value)
|
|
}
|
|
placeholder={lang === "en" ? "Quantity" : "Cantidad"}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>
|
|
{lang === "en" ? "Unit Cost:" : "Precio Unitario:"}
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={product.unit_cost}
|
|
onChange={(e) =>
|
|
handleProductChange(
|
|
index,
|
|
"unit_cost",
|
|
e.target.value
|
|
)
|
|
}
|
|
placeholder={
|
|
lang === "en" ? "Unit Cost" : "Precio Unitario"
|
|
}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label>{lang === "en" ? "VAT:" : "IVA:"}</label>
|
|
<select
|
|
name="product_id_tax"
|
|
value={product.id_tax}
|
|
onChange={(e) =>
|
|
handleProductChange(index, "id_tax", e.target.value)
|
|
}
|
|
>
|
|
<option value="" disabled>
|
|
{lang === "en" ? "Select Tax" : "Seleccione Impuesto"}
|
|
</option>
|
|
{taxes.map((tax) => (
|
|
<option key={tax.id} value={tax.id}>
|
|
{tax.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div style={{ display: "flex", alignItems: "end", gap: "10px" }}>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveProduct(index)}
|
|
className="save-button"
|
|
disabled={form.products.length <= 1}
|
|
>
|
|
{lang === "en" ? "Remove" : "Eliminar"}
|
|
</button>
|
|
{index === form.products.length - 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={handleAddProduct}
|
|
className="save-button"
|
|
>
|
|
{lang === "en" ? "Add Product" : "Agregar producto"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="save-button-wrapper">
|
|
<button type="submit" className="save-button" disabled={loading}>
|
|
{loading
|
|
? lang === "en"
|
|
? "Saving..."
|
|
: "Guardando..."
|
|
: lang === "en"
|
|
? "Save"
|
|
: "Guardar"}
|
|
</button>
|
|
</div>
|
|
|
|
{success && (
|
|
<span style={{ color: "green" }}>
|
|
{lang === "en"
|
|
? "Expense saved successfully!"
|
|
: "¡Gasto guardado con éxito!"}
|
|
</span>
|
|
)}
|
|
{error && <span style={{ color: "red" }}>{error}</span>}
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|