Add a ne model and card design across pages
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
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 { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
|
||||
const RecipesPage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<'view' | 'edit'>('view');
|
||||
const [selectedRecipe, setSelectedRecipe] = useState<typeof mockRecipes[0] | null>(null);
|
||||
|
||||
const mockRecipes = [
|
||||
@@ -100,38 +101,34 @@ const RecipesPage: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const getCategoryBadge = (category: string) => {
|
||||
const getRecipeStatusConfig = (category: string, difficulty: string, rating: number) => {
|
||||
const categoryConfig = {
|
||||
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' },
|
||||
bread: { text: 'Pan', icon: ChefHat },
|
||||
pastry: { text: 'Bollería', icon: ChefHat },
|
||||
cake: { text: 'Tarta', icon: ChefHat },
|
||||
cookie: { text: 'Galleta', icon: ChefHat },
|
||||
other: { text: 'Otro', icon: ChefHat },
|
||||
};
|
||||
|
||||
const config = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
|
||||
return (
|
||||
<Badge
|
||||
variant={config.color as any}
|
||||
text={config.text}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getDifficultyBadge = (difficulty: string) => {
|
||||
const difficultyConfig = {
|
||||
easy: { color: 'success', text: 'Fácil' },
|
||||
medium: { color: 'warning', text: 'Medio' },
|
||||
hard: { color: 'error', text: 'Difícil' },
|
||||
easy: { icon: '●', label: 'Fácil' },
|
||||
medium: { icon: '●●', label: 'Medio' },
|
||||
hard: { icon: '●●●', label: 'Difícil' },
|
||||
};
|
||||
|
||||
const config = difficultyConfig[difficulty as keyof typeof difficultyConfig];
|
||||
return (
|
||||
<Badge
|
||||
variant={config?.color as any}
|
||||
text={config?.text || difficulty}
|
||||
/>
|
||||
);
|
||||
const categoryInfo = categoryConfig[category as keyof typeof categoryConfig] || categoryConfig.other;
|
||||
const difficultyInfo = difficultyConfig[difficulty as keyof typeof difficultyConfig];
|
||||
const isPopular = rating >= 4.7;
|
||||
|
||||
return {
|
||||
color: getStatusColor(category),
|
||||
text: categoryInfo.text,
|
||||
icon: categoryInfo.icon,
|
||||
difficultyIcon: difficultyInfo?.icon || '●',
|
||||
difficultyLabel: difficultyInfo?.label || difficulty,
|
||||
isCritical: false,
|
||||
isHighlight: isPopular
|
||||
};
|
||||
};
|
||||
|
||||
const formatTime = (minutes: number) => {
|
||||
@@ -245,130 +242,65 @@ const RecipesPage: React.FC = () => {
|
||||
|
||||
{/* 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}
|
||||
</div>
|
||||
<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>
|
||||
<p className="text-xs text-[var(--text-secondary)] mt-1 line-clamp-2">
|
||||
{recipe.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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={() => {
|
||||
{filteredRecipes.map((recipe) => {
|
||||
const statusConfig = getRecipeStatusConfig(recipe.category, recipe.difficulty, recipe.rating);
|
||||
const profitMargin = Math.round((recipe.profit / recipe.price) * 100);
|
||||
const totalTime = formatTime(recipe.prepTime + recipe.bakingTime);
|
||||
|
||||
return (
|
||||
<StatusCard
|
||||
key={recipe.id}
|
||||
id={recipe.id}
|
||||
statusIndicator={statusConfig}
|
||||
title={recipe.name}
|
||||
subtitle={`${statusConfig.text} • ${statusConfig.difficultyLabel}${statusConfig.isHighlight ? ' ★' + recipe.rating : ''}`}
|
||||
primaryValue={formatters.currency(recipe.profit)}
|
||||
primaryValueLabel="margen"
|
||||
secondaryInfo={{
|
||||
label: 'Precio de venta',
|
||||
value: `${formatters.currency(recipe.price)} (costo: ${formatters.currency(recipe.cost)})`
|
||||
}}
|
||||
progress={{
|
||||
label: 'Margen de beneficio',
|
||||
percentage: profitMargin,
|
||||
color: profitMargin > 50 ? '#10b981' : profitMargin > 30 ? '#f59e0b' : '#ef4444'
|
||||
}}
|
||||
metadata={[
|
||||
`Tiempo: ${totalTime}`,
|
||||
`Porciones: ${recipe.yield}`,
|
||||
`${recipe.ingredients.length} ingredientes principales`
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Ver',
|
||||
icon: Eye,
|
||||
variant: 'outline',
|
||||
onClick: () => {
|
||||
setSelectedRecipe(recipe);
|
||||
setModalMode('view');
|
||||
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>
|
||||
))}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Editar',
|
||||
icon: Edit,
|
||||
variant: 'outline',
|
||||
onClick: () => {
|
||||
setSelectedRecipe(recipe);
|
||||
setModalMode('edit');
|
||||
setShowForm(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Producir',
|
||||
icon: ChefHat,
|
||||
variant: 'primary',
|
||||
onClick: () => console.log('Produce recipe', recipe.id)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
@@ -388,57 +320,133 @@ const RecipesPage: React.FC = () => {
|
||||
</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>
|
||||
{/* Recipe Details Modal */}
|
||||
{showForm && selectedRecipe && (
|
||||
<StatusModal
|
||||
isOpen={showForm}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedRecipe(null);
|
||||
setModalMode('view');
|
||||
}}
|
||||
mode={modalMode}
|
||||
onModeChange={setModalMode}
|
||||
title={selectedRecipe.name}
|
||||
subtitle={selectedRecipe.description}
|
||||
statusIndicator={getRecipeStatusConfig(selectedRecipe.category, selectedRecipe.difficulty, selectedRecipe.rating)}
|
||||
image={selectedRecipe.image}
|
||||
size="xl"
|
||||
sections={[
|
||||
{
|
||||
title: 'Información Básica',
|
||||
icon: ChefHat,
|
||||
fields: [
|
||||
{
|
||||
label: 'Categoría',
|
||||
value: selectedRecipe.category,
|
||||
type: 'status'
|
||||
},
|
||||
{
|
||||
label: 'Dificultad',
|
||||
value: selectedRecipe.difficulty
|
||||
},
|
||||
{
|
||||
label: 'Valoración',
|
||||
value: `${selectedRecipe.rating} ★`,
|
||||
highlight: selectedRecipe.rating >= 4.7
|
||||
},
|
||||
{
|
||||
label: 'Rendimiento',
|
||||
value: `${selectedRecipe.yield} porciones`
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Tiempos',
|
||||
icon: Clock,
|
||||
fields: [
|
||||
{
|
||||
label: 'Tiempo de preparación',
|
||||
value: formatTime(selectedRecipe.prepTime)
|
||||
},
|
||||
{
|
||||
label: 'Tiempo de horneado',
|
||||
value: formatTime(selectedRecipe.bakingTime)
|
||||
},
|
||||
{
|
||||
label: 'Tiempo total',
|
||||
value: formatTime(selectedRecipe.prepTime + selectedRecipe.bakingTime),
|
||||
highlight: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Análisis Financiero',
|
||||
icon: DollarSign,
|
||||
fields: [
|
||||
{
|
||||
label: 'Costo de producción',
|
||||
value: selectedRecipe.cost,
|
||||
type: 'currency'
|
||||
},
|
||||
{
|
||||
label: 'Precio de venta',
|
||||
value: selectedRecipe.price,
|
||||
type: 'currency'
|
||||
},
|
||||
{
|
||||
label: 'Margen de beneficio',
|
||||
value: selectedRecipe.profit,
|
||||
type: 'currency',
|
||||
highlight: true
|
||||
},
|
||||
{
|
||||
label: 'Porcentaje de margen',
|
||||
value: Math.round((selectedRecipe.profit / selectedRecipe.price) * 100),
|
||||
type: 'percentage',
|
||||
highlight: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Ingredientes',
|
||||
icon: Package,
|
||||
fields: [
|
||||
{
|
||||
label: 'Lista de ingredientes',
|
||||
value: selectedRecipe.ingredients.map(ing => `${ing.name}: ${ing.quantity} ${ing.unit}`),
|
||||
type: 'list',
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Etiquetas',
|
||||
fields: [
|
||||
{
|
||||
label: 'Tags',
|
||||
value: selectedRecipe.tags.join(', '),
|
||||
span: 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
actions={[
|
||||
{
|
||||
label: 'Producir',
|
||||
icon: ChefHat,
|
||||
variant: 'primary',
|
||||
onClick: () => {
|
||||
console.log('Producing recipe:', selectedRecipe.id);
|
||||
setShowForm(false);
|
||||
setSelectedRecipe(null);
|
||||
}
|
||||
}
|
||||
]}
|
||||
onEdit={() => {
|
||||
console.log('Editing recipe:', selectedRecipe.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user