BioalereNutricionAnimal / formulador.html
goodemagod's picture
Update formulador.html
3b8f567 verified
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Formulador de Piensos para Cerdos</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.custom-shadow {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.ingredient-input:hover {
transform: translateY(-2px);
transition: all 0.2s ease;
}
.nutrition-bar {
height: 8px;
border-radius: 4px;
}
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="mb-10 text-center">
<h1 class="text-4xl font-bold text-indigo-800 mb-2">Formulador de Piensos para Cerdos</h1>
<p class="text-lg text-gray-600">Crea f贸rmulas nutricionales 贸ptimas para cada etapa productiva</p>
</header>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Panel - Form -->
<div class="lg:col-span-2">
<div class="bg-white rounded-xl p-6 custom-shadow">
<div class="flex flex-wrap gap-4 mb-6">
<button @click="selectPreset('lechones')" class="flex-1 bg-pink-100 hover:bg-pink-200 text-pink-800 py-3 px-4 rounded-lg transition-colors">
<i class="fas fa-baby mr-2"></i> Lechones
</button>
<button @click="selectPreset('crecimiento')" class="flex-1 bg-blue-100 hover:bg-blue-200 text-blue-800 py-3 px-4 rounded-lg transition-colors">
<i class="fas fa-chart-line mr-2"></i> Crecimiento
</button>
<button @click="selectPreset('engorde')" class="flex-1 bg-green-100 hover:bg-green-200 text-green-800 py-3 px-4 rounded-lg transition-colors">
<i class="fas fa-weight mr-2"></i> Engorde
</button>
<button @click="selectPreset('reproductoras')" class="flex-1 bg-purple-100 hover:bg-purple-200 text-purple-800 py-3 px-4 rounded-lg transition-colors">
<i class="fas fa-venus mr-2"></i> Reproductoras
</button>
</div>
<div class="mb-6">
<label class="block text-gray-700 font-medium mb-2">Nombre de la f贸rmula</label>
<input v-model="formulaName" type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="mb-6">
<label class="block text-gray-700 font-medium mb-2">Descripci贸n</label>
<textarea v-model="formulaDescription" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"></textarea>
</div>
<h3 class="text-xl font-semibold text-gray-800 mb-4">Ingredientes</h3>
<div class="space-y-4 mb-6">
<div v-for="(ingredient, index) in ingredients" :key="index" class="ingredient-input bg-gray-50 p-4 rounded-lg custom-shadow">
<div class="flex flex-wrap gap-4 mb-3">
<div class="flex-1 min-w-[150px]">
<label class="block text-sm text-gray-600 mb-1">Ingrediente</label>
<select v-model="ingredient.name" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
<option v-for="item in ingredientOptions" :value="item.name">{{ item.name }}</option>
</select>
</div>
<div class="flex-1 min-w-[100px]">
<label class="block text-sm text-gray-600 mb-1">Porcentaje (%)</label>
<input v-model.number="ingredient.percentage" type="number" min="0" max="100" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
</div>
<div class="flex items-end">
<button @click="removeIngredient(index)" class="bg-red-100 hover:bg-red-200 text-red-600 px-3 py-2 rounded-lg">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div v-if="getIngredientDetails(ingredient.name)" class="text-xs text-gray-500">
Prote铆na: {{ getIngredientDetails(ingredient.name).protein }}% |
Energ铆a: {{ getIngredientDetails(ingredient.name).energy }} kcal/kg |
Fibra: {{ getIngredientDetails(ingredient.name).fiber }}%
</div>
</div>
</div>
<button @click="addIngredient" class="bg-indigo-100 hover:bg-indigo-200 text-indigo-700 py-2 px-4 rounded-lg mb-6">
<i class="fas fa-plus mr-2"></i> A帽adir Ingrediente
</button>
<div class="flex flex-wrap gap-4">
<button @click="calculateNutrition" class="bg-green-600 hover:bg-green-700 text-white py-3 px-6 rounded-lg font-medium">
<i class="fas fa-calculator mr-2"></i> Calcular Nutrici贸n
</button>
<button @click="saveFormula" class="bg-indigo-600 hover:bg-indigo-700 text-white py-3 px-6 rounded-lg font-medium">
<i class="fas fa-save mr-2"></i> Guardar F贸rmula
</button>
<button @click="resetForm" class="bg-gray-200 hover:bg-gray-300 text-gray-800 py-3 px-6 rounded-lg font-medium">
<i class="fas fa-redo mr-2"></i> Reiniciar
</button>
</div>
</div>
</div>
<!-- Right Panel - Results -->
<div class="lg:col-span-1">
<div class="bg-white rounded-xl p-6 custom-shadow sticky top-6">
<h3 class="text-xl font-semibold text-gray-800 mb-4">Resultados Nutricionales</h3>
<div v-if="totalPercentage > 0" class="mb-4">
<div class="flex justify-between mb-1">
<span class="text-sm font-medium">Total ingredientes</span>
<span class="text-sm font-medium">{{ totalPercentage }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div :class="totalPercentage < 100 ? 'bg-yellow-500' : 'bg-green-500'"
class="h-2.5 rounded-full"
:style="'width: ' + Math.min(totalPercentage, 100) + '%'"></div>
</div>
<p v-if="totalPercentage < 100" class="text-xs text-yellow-600 mt-1">La f贸rmula est谩 incompleta (faltan {{ 100 - totalPercentage }}%)</p>
<p v-if="totalPercentage > 100" class="text-xs text-red-600 mt-1">La f贸rmula excede el 100% ({{ totalPercentage - 100 }}% de m谩s)</p>
</div>
<div v-if="nutritionResults" class="space-y-4">
<div class="p-4 bg-blue-50 rounded-lg">
<h4 class="font-medium text-blue-800 mb-2">Prote铆na</h4>
<div class="flex justify-between mb-1">
<span class="text-sm">{{ nutritionResults.protein }}%</span>
<span class="text-sm">{{ getTargetRange('protein').min }}-{{ getTargetRange('protein').max }}% (objetivo)</span>
</div>
<div class="nutrition-bar bg-gray-200">
<div class="nutrition-bar bg-blue-500" :style="'width: ' + getNutritionPercentage('protein') + '%'"></div>
</div>
<p v-if="nutritionResults.protein < getTargetRange('protein').min" class="text-xs text-red-600 mt-1">Bajo en prote铆na</p>
<p v-if="nutritionResults.protein > getTargetRange('protein').max" class="text-xs text-red-600 mt-1">Alto en prote铆na</p>
</div>
<div class="p-4 bg-green-50 rounded-lg">
<h4 class="font-medium text-green-800 mb-2">Energ铆a</h4>
<div class="flex justify-between mb-1">
<span class="text-sm">{{ nutritionResults.energy }} kcal/kg</span>
<span class="text-sm">{{ getTargetRange('energy').min }}-{{ getTargetRange('energy').max }} kcal/kg (objetivo)</span>
</div>
<div class="nutrition-bar bg-gray-200">
<div class="nutrition-bar bg-green-500" :style="'width: ' + getNutritionPercentage('energy') + '%'"></div>
</div>
</div>
<div class="p-4 bg-yellow-50 rounded-lg">
<h4 class="font-medium text-yellow-800 mb-2">Fibra</h4>
<div class="flex justify-between mb-1">
<span class="text-sm">{{ nutritionResults.fiber }}%</span>
<span class="text-sm">{{ getTargetRange('fiber').min }}-{{ getTargetRange('fiber').max }}% (objetivo)</span>
</div>
<div class="nutrition-bar bg-gray-200">
<div class="nutrition-bar bg-yellow-500" :style="'width: ' + getNutritionPercentage('fiber') + '%'"></div>
</div>
</div>
<div class="p-4 bg-purple-50 rounded-lg">
<h4 class="font-medium text-purple-800 mb-2">Costo estimado</h4>
<p class="text-lg font-semibold">${{ nutritionResults.cost.toFixed(2) }} por tonelada</p>
</div>
</div>
<div v-else class="text-center py-8 text-gray-500">
<i class="fas fa-calculator text-4xl mb-3 text-gray-300"></i>
<p>Ingresa los ingredientes y haz clic en "Calcular Nutrici贸n" para ver los resultados</p>
</div>
</div>
</div>
</div>
<!-- Saved Formulas -->
<div v-if="savedFormulas.length > 0" class="mt-12">
<h2 class="text-2xl font-bold text-gray-800 mb-6">F贸rmulas Guardadas</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div v-for="(formula, index) in savedFormulas" :key="index" class="bg-white rounded-xl p-6 custom-shadow hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-3">
<h3 class="text-lg font-semibold text-indigo-700">{{ formula.name }}</h3>
<button @click="deleteFormula(index)" class="text-red-400 hover:text-red-600">
<i class="fas fa-trash"></i>
</button>
</div>
<p class="text-sm text-gray-600 mb-4">{{ formula.description }}</p>
<div class="mb-4">
<div class="flex justify-between text-sm mb-1">
<span class="font-medium">Composici贸n:</span>
<span class="text-gray-500">{{ getFormulaTotal(formula.ingredients) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-indigo-500 h-2 rounded-full" :style="'width: ' + getFormulaTotal(formula.ingredients) + '%'"></div>
</div>
</div>
<ul class="space-y-1 mb-4">
<li v-for="(ing, i) in formula.ingredients" :key="i" class="flex justify-between text-sm">
<span>{{ ing.name }}</span>
<span class="font-medium">{{ ing.percentage }}%</span>
</li>
</ul>
<button @click="loadFormula(index)" class="w-full bg-indigo-50 hover:bg-indigo-100 text-indigo-700 py-2 px-4 rounded-lg text-sm">
<i class="fas fa-edit mr-2"></i> Cargar esta f贸rmula
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const app = {
data() {
return {
formulaName: '',
formulaDescription: '',
ingredients: [
{ name: 'Ma铆z', percentage: 0 },
{ name: 'Soja', percentage: 0 }
],
nutritionResults: null,
savedFormulas: [],
currentPreset: null,
ingredientOptions: [
{ name: 'Ma铆z', protein: 8.5, energy: 3400, fiber: 2.3, cost: 0.15 },
{ name: 'Soja', protein: 44.0, energy: 3300, fiber: 6.0, cost: 0.35 },
{ name: 'Harina de pescado', protein: 65.0, energy: 3000, fiber: 0.5, cost: 0.80 },
{ name: 'Leche en polvo', protein: 26.0, energy: 4900, fiber: 0.0, cost: 1.20 },
{ name: 'Harina de trigo', protein: 13.0, energy: 3300, fiber: 2.5, cost: 0.25 },
{ name: 'Grasa animal', protein: 0.0, energy: 7700, fiber: 0.0, cost: 0.50 },
{ name: 'Sorgo', protein: 10.0, energy: 3300, fiber: 2.5, cost: 0.18 },
{ name: 'Salvado de trigo', protein: 15.5, energy: 2500, fiber: 10.0, cost: 0.12 },
{ name: 'Minerales', protein: 0.0, energy: 0.0, fiber: 0.0, cost: 1.50 },
{ name: 'Vitaminas', protein: 0.0, energy: 0.0, fiber: 0.0, cost: 2.00 }
],
presetTargets: {
lechones: {
protein: { min: 22, max: 24 },
energy: { min: 3400, max: 3600 },
fiber: { min: 3, max: 5 }
},
crecimiento: {
protein: { min: 18, max: 20 },
energy: { min: 3300, max: 3500 },
fiber: { min: 4, max: 6 }
},
engorde: {
protein: { min: 14, max: 16 },
energy: { min: 3400, max: 3600 },
fiber: { min: 5, max: 7 }
},
reproductoras: {
protein: { min: 16, max: 18 },
energy: { min: 3200, max: 3400 },
fiber: { min: 6, max: 8 }
}
}
}
},
computed: {
totalPercentage() {
return this.ingredients.reduce((sum, ing) => sum + (Number(ing.percentage) || 0), 0);
}
},
methods: {
addIngredient() {
this.ingredients.push({ name: 'Ma铆z', percentage: 0 });
},
removeIngredient(index) {
if (this.ingredients.length > 1) {
this.ingredients.splice(index, 1);
}
},
getIngredientDetails(name) {
return this.ingredientOptions.find(item => item.name === name);
},
calculateNutrition() {
if (this.totalPercentage === 0) return;
let totalProtein = 0;
let totalEnergy = 0;
let totalFiber = 0;
let totalCost = 0;
this.ingredients.forEach(ing => {
const details = this.getIngredientDetails(ing.name);
if (details) {
const weight = ing.percentage / this.totalPercentage;
totalProtein += details.protein * weight;
totalEnergy += details.energy * weight;
totalFiber += details.fiber * weight;
totalCost += details.cost * ing.percentage;
}
});
// Adjust for 100% if needed
const adjustment = this.totalPercentage !== 100 ? (100 / this.totalPercentage) : 1;
this.nutritionResults = {
protein: (totalProtein * adjustment).toFixed(1),
energy: Math.round(totalEnergy * adjustment),
fiber: (totalFiber * adjustment).toFixed(1),
cost: totalCost * 10 // Convert to cost per ton (assuming costs are per kg)
};
},
saveFormula() {
if (!this.formulaName.trim()) {
alert('Por favor ingresa un nombre para la f贸rmula');
return;
}
if (this.totalPercentage === 0) {
alert('La f贸rmula no contiene ingredientes');
return;
}
this.savedFormulas.push({
name: this.formulaName,
description: this.formulaDescription,
ingredients: JSON.parse(JSON.stringify(this.ingredients)),
preset: this.currentPreset
});
// Save to localStorage
localStorage.setItem('savedFormulas', JSON.stringify(this.savedFormulas));
this.formulaName = '';
this.formulaDescription = '';
alert('F贸rmula guardada con 茅xito!');
},
loadFormula(index) {
const formula = this.savedFormulas[index];
this.formulaName = formula.name;
this.formulaDescription = formula.description;
this.ingredients = JSON.parse(JSON.stringify(formula.ingredients));
this.currentPreset = formula.preset;
this.calculateNutrition();
},
deleteFormula(index) {
if (confirm('驴Est谩s seguro de eliminar esta f贸rmula?')) {
this.savedFormulas.splice(index, 1);
localStorage.setItem('savedFormulas', JSON.stringify(this.savedFormulas));
}
},
resetForm() {
this.formulaName = '';
this.formulaDescription = '';
this.ingredients = [{ name: 'Ma铆z', percentage: 0 }, { name: 'Soja', percentage: 0 }];
this.nutritionResults = null;
this.currentPreset = null;
},
selectPreset(preset) {
this.currentPreset = preset;
// Set default ingredients based on preset
if (preset === 'lechones') {
this.ingredients = [
{ name: 'Ma铆z', percentage: 50 },
{ name: 'Soja', percentage: 25 },
{ name: 'Leche en polvo', percentage: 15 },
{ name: 'Harina de pescado', percentage: 10 }
];
this.formulaName = 'F贸rmula para Lechones';
this.formulaDescription = 'Alta en prote铆na y energ铆a para el desarrollo inicial';
}
else if (preset === 'crecimiento') {
this.ingredients = [
{ name: 'Ma铆z', percentage: 60 },
{ name: 'Soja', percentage: 30 },
{ name: 'Harina de trigo', percentage: 10 }
];
this.formulaName = 'F贸rmula para Crecimiento';
this.formulaDescription = 'Balance de prote铆na y energ铆a para desarrollo 贸ptimo';
}
else if (preset === 'engorde') {
this.ingredients = [
{ name: 'Ma铆z', percentage: 70 },
{ name: 'Soja', percentage: 20 },
{ name: 'Grasa animal', percentage: 10 }
];
this.formulaName = 'F贸rmula para Engorde';
this.formulaDescription = 'Alta en energ铆a para aumento de peso';
}
else if (preset === 'reproductoras') {
this.ingredients = [
{ name: 'Ma铆z', percentage: 60 },
{ name: 'Soja', percentage: 30 },
{ name: 'Minerales', percentage: 5 },
{ name: 'Vitaminas', percentage: 5 }
];
this.formulaName = 'F贸rmula para Reproductoras';
this.formulaDescription = 'Balance nutricional para salud y reproducci贸n';
}
this.calculateNutrition();
},
getTargetRange(nutrient) {
if (this.currentPreset && this.presetTargets[this.currentPreset]) {
return this.presetTargets[this.currentPreset][nutrient];
}
// Default ranges if no preset selected
return {
protein: { min: 14, max: 24 },
energy: { min: 3200, max: 3600 },
fiber: { min: 3, max: 8 }
};
},
getNutritionPercentage(nutrient) {
const value = parseFloat(this.nutritionResults[nutrient]);
const range = this.getTargetRange(nutrient);
const target = (range.min + range.max) / 2;
const span = range.max - range.min;
// Calculate position within range (0-100%)
let percentage = ((value - (target - span/2)) / span) * 100;
// Clamp between 0 and 100
return Math.min(100, Math.max(0, percentage));
},
getFormulaTotal(ingredients) {
return ingredients.reduce((sum, ing) => sum + (Number(ing.percentage) || 0), 0);
}
},
mounted() {
// Load saved formulas from localStorage
const saved = localStorage.getItem('savedFormulas');
if (saved) {
this.savedFormulas = JSON.parse(saved);
}
}
};
Vue.createApp(app).mount('body');
});
</script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</body>
</html>