Start integrating the onboarding flow with backend 6

This commit is contained in:
Urtzi Alfaro
2025-09-05 17:49:48 +02:00
parent 236c3a32ae
commit 069954981a
131 changed files with 5217 additions and 22838 deletions

View File

@@ -1,412 +0,0 @@
import React, { useState } from 'react';
import { Plus, Search, Filter, Star, Clock, Users, DollarSign } from 'lucide-react';
import { Button, Input, Card, Badge } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
const RecipesPage: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [selectedDifficulty, setSelectedDifficulty] = useState('all');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const mockRecipes = [
{
id: '1',
name: 'Pan de Molde Integral',
category: 'bread',
difficulty: 'medium',
prepTime: 120,
bakingTime: 35,
yield: 1,
rating: 4.8,
cost: 2.50,
price: 4.50,
profit: 2.00,
image: '/api/placeholder/300/200',
tags: ['integral', 'saludable', 'artesanal'],
description: 'Pan integral artesanal con semillas, perfecto para desayunos saludables.',
ingredients: [
{ name: 'Harina integral', quantity: 500, unit: 'g' },
{ name: 'Agua', quantity: 300, unit: 'ml' },
{ name: 'Levadura', quantity: 10, unit: 'g' },
{ name: 'Sal', quantity: 8, unit: 'g' },
],
},
{
id: '2',
name: 'Croissants de Mantequilla',
category: 'pastry',
difficulty: 'hard',
prepTime: 480,
bakingTime: 20,
yield: 12,
rating: 4.9,
cost: 8.50,
price: 18.00,
profit: 9.50,
image: '/api/placeholder/300/200',
tags: ['francés', 'mantequilla', 'hojaldrado'],
description: 'Croissants franceses tradicionales con laminado de mantequilla.',
ingredients: [
{ name: 'Harina de fuerza', quantity: 500, unit: 'g' },
{ name: 'Mantequilla', quantity: 250, unit: 'g' },
{ name: 'Leche', quantity: 150, unit: 'ml' },
{ name: 'Azúcar', quantity: 50, unit: 'g' },
],
},
{
id: '3',
name: 'Tarta de Manzana',
category: 'cake',
difficulty: 'easy',
prepTime: 45,
bakingTime: 40,
yield: 8,
rating: 4.6,
cost: 4.20,
price: 12.00,
profit: 7.80,
image: '/api/placeholder/300/200',
tags: ['frutal', 'casera', 'temporada'],
description: 'Tarta casera de manzana con canela y masa quebrada.',
ingredients: [
{ name: 'Manzanas', quantity: 1000, unit: 'g' },
{ name: 'Harina', quantity: 250, unit: 'g' },
{ name: 'Mantequilla', quantity: 125, unit: 'g' },
{ name: 'Azúcar', quantity: 100, unit: 'g' },
],
},
];
const categories = [
{ value: 'all', label: 'Todas las categorías' },
{ value: 'bread', label: 'Panes' },
{ value: 'pastry', label: 'Bollería' },
{ value: 'cake', label: 'Tartas' },
{ value: 'cookie', label: 'Galletas' },
{ value: 'other', label: 'Otros' },
];
const difficulties = [
{ value: 'all', label: 'Todas las dificultades' },
{ value: 'easy', label: 'Fácil' },
{ value: 'medium', label: 'Medio' },
{ value: 'hard', label: 'Difícil' },
];
const getCategoryBadge = (category: string) => {
const categoryConfig = {
bread: { color: 'brown', text: 'Pan' },
pastry: { color: 'yellow', text: 'Bollería' },
cake: { color: 'pink', text: 'Tarta' },
cookie: { color: 'orange', text: 'Galleta' },
other: { color: 'gray', text: 'Otro' },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = {
easy: { color: 'green', text: 'Fácil' },
medium: { color: 'yellow', text: 'Medio' },
hard: { color: 'red', text: 'Difícil' },
};
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
};
const formatTime = (minutes: number) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
};
const filteredRecipes = mockRecipes.filter(recipe => {
const matchesSearch = recipe.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
recipe.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = selectedCategory === 'all' || recipe.category === selectedCategory;
const matchesDifficulty = selectedDifficulty === 'all' || recipe.difficulty === selectedDifficulty;
return matchesSearch && matchesCategory && matchesDifficulty;
});
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Recetas"
description="Administra y organiza todas las recetas de tu panadería"
action={
<Button>
<Plus className="w-4 h-4 mr-2" />
Nueva Receta
</Button>
}
/>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Total Recetas</p>
<p className="text-3xl font-bold text-gray-900">{mockRecipes.length}</p>
</div>
<div className="h-12 w-12 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Más Populares</p>
<p className="text-3xl font-bold text-yellow-600">
{mockRecipes.filter(r => r.rating > 4.7).length}
</p>
</div>
<Star className="h-12 w-12 text-yellow-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Costo Promedio</p>
<p className="text-3xl font-bold text-green-600">
€{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<DollarSign className="h-12 w-12 text-green-600" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600">Margen Promedio</p>
<p className="text-3xl font-bold text-purple-600">
€{(mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
</Card>
</div>
{/* Filters and Search */}
<Card className="p-6">
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
placeholder="Buscar recetas por nombre, ingredientes o etiquetas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{categories.map(cat => (
<option key={cat.value} value={cat.value}>{cat.label}</option>
))}
</select>
<select
value={selectedDifficulty}
onChange={(e) => setSelectedDifficulty(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md"
>
{difficulties.map(diff => (
<option key={diff.value} value={diff.value}>{diff.label}</option>
))}
</select>
<Button
variant="outline"
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
>
{viewMode === 'grid' ? 'Vista Lista' : 'Vista Cuadrícula'}
</Button>
</div>
</div>
</Card>
{/* Recipes Grid/List */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredRecipes.map((recipe) => (
<Card key={recipe.id} className="overflow-hidden hover:shadow-lg transition-shadow">
<div className="aspect-w-16 aspect-h-9">
<img
src={recipe.image}
alt={recipe.name}
className="w-full h-48 object-cover"
/>
</div>
<div className="p-6">
<div className="flex items-start justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1">
{recipe.name}
</h3>
<div className="flex items-center ml-2">
<Star className="h-4 w-4 text-yellow-400 fill-current" />
<span className="text-sm text-gray-600 ml-1">{recipe.rating}</span>
</div>
</div>
<p className="text-gray-600 text-sm mb-3 line-clamp-2">
{recipe.description}
</p>
<div className="flex flex-wrap gap-2 mb-3">
{getCategoryBadge(recipe.category)}
{getDifficultyBadge(recipe.difficulty)}
</div>
<div className="grid grid-cols-2 gap-4 mb-4 text-sm text-gray-600">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-1" />
<span>{formatTime(recipe.prepTime + recipe.bakingTime)}</span>
</div>
<div className="flex items-center">
<Users className="h-4 w-4 mr-1" />
<span>{recipe.yield} porciones</span>
</div>
</div>
<div className="flex justify-between items-center mb-4">
<div className="text-sm">
<span className="text-gray-600">Costo: </span>
<span className="font-medium">€{recipe.cost.toFixed(2)}</span>
</div>
<div className="text-sm">
<span className="text-gray-600">Precio: </span>
<span className="font-medium text-green-600">€{recipe.price.toFixed(2)}</span>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="flex-1">
Ver Receta
</Button>
<Button size="sm" className="flex-1">
Producir
</Button>
</div>
</div>
</Card>
))}
</div>
) : (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Categoría
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Dificultad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tiempo Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rendimiento
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Costo
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Precio
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Margen
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredRecipes.map((recipe) => (
<tr key={recipe.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<img
src={recipe.image}
alt={recipe.name}
className="h-10 w-10 rounded-full mr-4"
/>
<div>
<div className="text-sm font-medium text-gray-900">{recipe.name}</div>
<div className="flex items-center">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-xs text-gray-500 ml-1">{recipe.rating}</span>
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getCategoryBadge(recipe.category)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getDifficultyBadge(recipe.difficulty)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{recipe.yield} porciones
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
€{recipe.cost.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-green-600 font-medium">
€{recipe.price.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-purple-600 font-medium">
€{recipe.profit.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex space-x-2">
<Button variant="outline" size="sm">Ver</Button>
<Button size="sm">Producir</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
);
};
export default RecipesPage;