Add new page designs

This commit is contained in:
Urtzi Alfaro
2025-08-30 19:11:15 +02:00
parent 221781731c
commit 62b1ab9cb1
12 changed files with 2129 additions and 1240 deletions

View File

@@ -1,13 +1,13 @@
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 { Plus, Download, Star, Clock, Users, DollarSign, Package, Eye, Edit, ChefHat, Timer } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
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 [showForm, setShowForm] = useState(false);
const [selectedRecipe, setSelectedRecipe] = useState<typeof mockRecipes[0] | null>(null);
const mockRecipes = [
{
@@ -76,46 +76,62 @@ const RecipesPage: React.FC = () => {
{ 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' },
{
id: '4',
name: 'Magdalenas de Limón',
category: 'pastry',
difficulty: 'easy',
prepTime: 20,
bakingTime: 25,
yield: 12,
rating: 4.4,
cost: 3.80,
price: 9.00,
profit: 5.20,
image: '/api/placeholder/300/200',
tags: ['cítrico', 'esponjoso', 'individual'],
description: 'Magdalenas suaves y esponjosas con ralladura de limón.',
ingredients: [
{ name: 'Harina', quantity: 200, unit: 'g' },
{ name: 'Huevos', quantity: 3, unit: 'uds' },
{ name: 'Azúcar', quantity: 150, unit: 'g' },
{ name: 'Limón', quantity: 2, unit: 'uds' },
],
},
];
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' },
bread: { color: 'default', text: 'Pan' },
pastry: { color: 'warning', text: 'Bollería' },
cake: { color: 'secondary', text: 'Tarta' },
cookie: { color: 'info', text: 'Galleta' },
other: { color: 'outline', text: 'Otro' },
};
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
return <Badge variant={config.color as any}>{config.text}</Badge>;
return (
<Badge
variant={config.color as any}
text={config.text}
/>
);
};
const getDifficultyBadge = (difficulty: string) => {
const difficultyConfig = {
easy: { color: 'green', text: 'Fácil' },
medium: { color: 'yellow', text: 'Medio' },
hard: { color: 'red', text: 'Difícil' },
easy: { color: 'success', text: 'Fácil' },
medium: { color: 'warning', text: 'Medio' },
hard: { color: 'error', text: 'Difícil' },
};
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
return <Badge variant={config.color as any}>{config.text}</Badge>;
return (
<Badge
variant={config?.color as any}
text={config?.text || difficulty}
/>
);
};
const formatTime = (minutes: number) => {
@@ -129,281 +145,300 @@ const RecipesPage: React.FC = () => {
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 matchesSearch;
});
const mockRecipeStats = {
totalRecipes: mockRecipes.length,
popularRecipes: mockRecipes.filter(r => r.rating > 4.7).length,
easyRecipes: mockRecipes.filter(r => r.difficulty === 'easy').length,
averageCost: mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length,
averageProfit: mockRecipes.reduce((sum, r) => sum + r.profit, 0) / mockRecipes.length,
categories: [...new Set(mockRecipes.map(r => r.category))].length,
};
const recipeStats = [
{
title: 'Total Recetas',
value: mockRecipeStats.totalRecipes,
variant: 'default' as const,
icon: ChefHat,
},
{
title: 'Populares',
value: mockRecipeStats.popularRecipes,
variant: 'warning' as const,
icon: Star,
},
{
title: 'Fáciles',
value: mockRecipeStats.easyRecipes,
variant: 'success' as const,
icon: Timer,
},
{
title: 'Costo Promedio',
value: formatters.currency(mockRecipeStats.averageCost),
variant: 'info' as const,
icon: DollarSign,
},
{
title: 'Margen Promedio',
value: formatters.currency(mockRecipeStats.averageProfit),
variant: 'success' as const,
icon: DollarSign,
},
{
title: 'Categorías',
value: mockRecipeStats.categories,
variant: 'info' as const,
icon: Package,
},
];
return (
<div className="p-6 space-y-6">
<div className="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>
}
actions={[
{
id: "export",
label: "Exportar",
variant: "outline" as const,
icon: Download,
onClick: () => console.log('Export recipes')
},
{
id: "new",
label: "Nueva Receta",
variant: "primary" as const,
icon: Plus,
onClick: () => setShowForm(true)
}
]}
/>
{/* 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-[var(--text-secondary)]">Total Recetas</p>
<p className="text-3xl font-bold text-[var(--text-primary)]">{mockRecipes.length}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-info)]/10 rounded-full flex items-center justify-center">
<svg className="h-6 w-6 text-[var(--color-info)]" 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>
{/* Stats Grid */}
<StatsGrid
stats={recipeStats}
columns={6}
/>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">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-[var(--text-secondary)]">Costo Promedio</p>
<p className="text-3xl font-bold text-[var(--color-success)]">
{(mockRecipes.reduce((sum, r) => sum + r.cost, 0) / mockRecipes.length).toFixed(2)}
</p>
</div>
<DollarSign className="h-12 w-12 text-[var(--color-success)]" />
</div>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">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">
{/* Simplified Controls */}
<Card className="p-4">
<div className="flex flex-col sm: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-[var(--text-tertiary)] 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-[var(--border-secondary)] 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-[var(--border-secondary)] 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>
<Input
placeholder="Buscar recetas por nombre, ingredientes o etiquetas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<Button variant="outline" onClick={() => console.log('Export filtered')}>
<Download className="w-4 h-4 mr-2" />
Exportar
</Button>
</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-[var(--text-primary)] line-clamp-1">
{/* Recipes Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredRecipes.map((recipe) => (
<Card key={recipe.id} className="p-4">
<div className="space-y-4">
{/* Header with Image */}
<div className="flex items-start gap-3">
<div className="flex-shrink-0">
<img
src={recipe.image}
alt={recipe.name}
className="w-16 h-16 rounded-lg object-cover bg-[var(--bg-secondary)]"
/>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-[var(--text-primary)] truncate">
{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-[var(--text-secondary)] ml-1">{recipe.rating}</span>
</div>
</div>
<p className="text-[var(--text-secondary)] 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-[var(--text-secondary)]">
<div className="flex items-center">
<Clock className="h-4 w-4 mr-1" />
<span>{formatTime(recipe.prepTime + recipe.bakingTime)}</span>
<div className="flex items-center gap-1 mt-1">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="text-xs text-[var(--text-secondary)]">{recipe.rating}</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-[var(--text-secondary)]">Costo: </span>
<span className="font-medium">{recipe.cost.toFixed(2)}</span>
</div>
<div className="text-sm">
<span className="text-[var(--text-secondary)]">Precio: </span>
<span className="font-medium text-[var(--color-success)]">{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>
<p className="text-xs text-[var(--text-secondary)] mt-1 line-clamp-2">
{recipe.description}
</p>
</div>
</div>
</Card>
))}
{/* Badges */}
<div className="flex gap-2 flex-wrap">
{getCategoryBadge(recipe.category)}
{getDifficultyBadge(recipe.difficulty)}
</div>
{/* Time and Yield */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4 text-[var(--text-tertiary)]" />
<span className="text-[var(--text-primary)]">
{recipe.yield} porciones
</span>
</div>
</div>
{/* Financial Info */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Costo:</span>
<span className="font-medium text-[var(--text-primary)]">
{formatters.currency(recipe.cost)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Precio:</span>
<span className="font-medium text-[var(--color-success)]">
{formatters.currency(recipe.price)}
</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-[var(--text-secondary)]">Margen:</span>
<span className="font-bold text-[var(--color-success)]">
{formatters.currency(recipe.profit)}
</span>
</div>
</div>
{/* Profit Margin Bar */}
<div className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-[var(--text-secondary)]">Margen de beneficio</span>
<span className="text-[var(--text-primary)]">
{Math.round((recipe.profit / recipe.price) * 100)}%
</span>
</div>
<div className="w-full bg-[var(--bg-tertiary)] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${
(recipe.profit / recipe.price) > 0.5
? 'bg-[var(--color-success)]'
: (recipe.profit / recipe.price) > 0.3
? 'bg-[var(--color-warning)]'
: 'bg-[var(--color-error)]'
}`}
style={{ width: `${Math.min((recipe.profit / recipe.price) * 100, 100)}%` }}
/>
</div>
</div>
{/* Ingredients Count */}
<div className="text-xs text-[var(--text-secondary)]">
{recipe.ingredients.length} ingredientes principales
</div>
{/* Actions */}
<div className="flex gap-2 pt-2 border-t border-[var(--border-primary)]">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => {
setSelectedRecipe(recipe);
setShowForm(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button
variant="primary"
size="sm"
className="flex-1"
onClick={() => console.log('Produce recipe', recipe.id)}
>
<ChefHat className="w-4 h-4 mr-1" />
Producir
</Button>
</div>
</div>
</Card>
))}
</div>
{/* Empty State */}
{filteredRecipes.length === 0 && (
<div className="text-center py-12">
<ChefHat className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
No se encontraron recetas
</h3>
<p className="text-[var(--text-secondary)] mb-4">
Intenta ajustar la búsqueda o crear una nueva receta
</p>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nueva Receta
</Button>
</div>
)}
{/* Recipe Form Modal - Placeholder */}
{showForm && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<Card className="w-full max-w-2xl max-h-[90vh] overflow-auto m-4 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
{selectedRecipe ? 'Ver Receta' : 'Nueva Receta'}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
setShowForm(false);
setSelectedRecipe(null);
}}
>
Cerrar
</Button>
</div>
{selectedRecipe && (
<div className="space-y-4">
<img
src={selectedRecipe.image}
alt={selectedRecipe.name}
className="w-full h-48 object-cover rounded-lg"
/>
<h3 className="text-lg font-medium">{selectedRecipe.name}</h3>
<p className="text-[var(--text-secondary)]">{selectedRecipe.description}</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Tiempo total:</span> {formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime)}
</div>
<div>
<span className="font-medium">Rendimiento:</span> {selectedRecipe.yield} porciones
</div>
</div>
<div>
<h4 className="font-medium mb-2">Ingredientes:</h4>
<ul className="space-y-1 text-sm">
{selectedRecipe.ingredients.map((ing, i) => (
<li key={i} className="flex justify-between">
<span>{ing.name}</span>
<span>{ing.quantity} {ing.unit}</span>
</li>
))}
</ul>
</div>
</div>
)}
</Card>
</div>
) : (
<Card>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-[var(--bg-secondary)]">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Receta
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Categoría
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Dificultad
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Tiempo Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Rendimiento
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Costo
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Precio
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] uppercase tracking-wider">
Margen
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-[var(--text-tertiary)] 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-[var(--bg-secondary)]">
<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-[var(--text-primary)]">{recipe.name}</div>
<div className="flex items-center">
<Star className="h-3 w-3 text-yellow-400 fill-current" />
<span className="text-xs text-[var(--text-tertiary)] 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-[var(--text-primary)]">
{formatTime(recipe.prepTime + recipe.bakingTime)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{recipe.yield} porciones
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--text-primary)]">
{recipe.cost.toFixed(2)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-[var(--color-success)] 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>
);