Improve frontend 5
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Plus, Star, Clock, Euro, Package, Eye, ChefHat, Timer, CheckCircle, Trash2, Settings, FileText } from 'lucide-react';
|
||||
import { StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, type FilterConfig, EmptyState } from '../../../../components/ui';
|
||||
import { StatsGrid, StatusCard, getStatusColor, EditViewModal, SearchAndFilter, type FilterConfig, EmptyState, type EditViewModalSection } from '../../../../components/ui';
|
||||
import { QualityPromptDialog } from '../../../../components/ui/QualityPromptDialog';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
@@ -11,7 +11,7 @@ import type { RecipeResponse, RecipeCreate } from '../../../../api/types/recipes
|
||||
import { MeasurementUnit } from '../../../../api/types/recipes';
|
||||
import { useIngredients } from '../../../../api/hooks/inventory';
|
||||
import { ProcessStage, type RecipeQualityConfiguration } from '../../../../api/types/qualityTemplates';
|
||||
import { CreateRecipeModal, DeleteRecipeModal } from '../../../../components/domain/recipes';
|
||||
import { CreateRecipeModal, DeleteRecipeModal, RecipeViewEditModal } from '../../../../components/domain/recipes';
|
||||
import { QualityCheckConfigurationModal } from '../../../../components/domain/recipes/QualityCheckConfigurationModal';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { RecipeIngredientResponse } from '../../../../api/types/recipes';
|
||||
@@ -408,7 +408,7 @@ const RecipesPage: React.FC = () => {
|
||||
);
|
||||
|
||||
const totalTemplates = Object.values(stages).reduce(
|
||||
(sum, stage) => sum + (stage.template_ids?.length || 0),
|
||||
(sum, stage) => sum + (stage?.template_ids?.length || 0),
|
||||
0
|
||||
);
|
||||
|
||||
@@ -785,7 +785,7 @@ const RecipesPage: React.FC = () => {
|
||||
};
|
||||
|
||||
// Get modal sections with editable fields
|
||||
const getModalSections = () => {
|
||||
const getModalSections = (): EditViewModalSection[] => {
|
||||
if (!selectedRecipe) return [];
|
||||
|
||||
return [
|
||||
@@ -965,8 +965,8 @@ const RecipesPage: React.FC = () => {
|
||||
: (selectedRecipe.is_seasonal ? 'Sí' : 'No'),
|
||||
type: modalMode === 'edit' ? 'select' : 'text',
|
||||
options: modalMode === 'edit' ? [
|
||||
{ value: false, label: 'No' },
|
||||
{ value: true, label: 'Sí' }
|
||||
{ value: 'false', label: 'No' },
|
||||
{ value: 'true', label: 'Sí' }
|
||||
] : undefined,
|
||||
editable: modalMode === 'edit'
|
||||
},
|
||||
@@ -989,8 +989,8 @@ const RecipesPage: React.FC = () => {
|
||||
: (selectedRecipe.is_signature_item ? 'Sí' : 'No'),
|
||||
type: modalMode === 'edit' ? 'select' : 'text',
|
||||
options: modalMode === 'edit' ? [
|
||||
{ value: false, label: 'No' },
|
||||
{ value: true, label: 'Sí' }
|
||||
{ value: 'false', label: 'No' },
|
||||
{ value: 'true', label: 'Sí' }
|
||||
] : undefined,
|
||||
editable: modalMode === 'edit',
|
||||
highlight: selectedRecipe.is_signature_item
|
||||
@@ -1059,10 +1059,96 @@ const RecipesPage: React.FC = () => {
|
||||
label: 'Instrucciones de preparación',
|
||||
value: modalMode === 'edit'
|
||||
? formatJsonField(getFieldValue(selectedRecipe.instructions, 'instructions'))
|
||||
: formatJsonField(selectedRecipe.instructions),
|
||||
type: modalMode === 'edit' ? 'textarea' : 'text',
|
||||
: '', // Empty string when using customRenderer in view mode
|
||||
type: modalMode === 'edit' ? 'textarea' : 'custom',
|
||||
editable: modalMode === 'edit',
|
||||
span: 2
|
||||
span: 2,
|
||||
customRenderer: modalMode === 'view' ? () => {
|
||||
const instructions = selectedRecipe.instructions;
|
||||
|
||||
if (!instructions) {
|
||||
return (
|
||||
<div className="text-sm text-[var(--text-secondary)] italic">
|
||||
No especificado
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle array of instruction objects
|
||||
if (Array.isArray(instructions)) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{instructions.map((item: any, index: number) => {
|
||||
// Handle different item types
|
||||
let stepTitle = '';
|
||||
let stepDescription = '';
|
||||
|
||||
if (typeof item === 'string') {
|
||||
stepDescription = item;
|
||||
} else if (typeof item === 'object' && item !== null) {
|
||||
stepTitle = item.step || item.title || '';
|
||||
stepDescription = item.description || '';
|
||||
|
||||
// If no description but has other fields, stringify them
|
||||
if (!stepDescription && !stepTitle) {
|
||||
stepDescription = JSON.stringify(item);
|
||||
}
|
||||
} else {
|
||||
stepDescription = String(item);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="flex gap-3">
|
||||
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-[var(--color-primary)]/10 flex items-center justify-center text-xs font-semibold text-[var(--color-primary)]">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{stepTitle && stepDescription ? (
|
||||
<>
|
||||
<div className="font-medium text-[var(--text-primary)]">{stepTitle}</div>
|
||||
<div className="text-sm text-[var(--text-secondary)] mt-1">{stepDescription}</div>
|
||||
</>
|
||||
) : stepDescription ? (
|
||||
<div className="text-sm text-[var(--text-primary)]">{stepDescription}</div>
|
||||
) : stepTitle ? (
|
||||
<div className="text-sm text-[var(--text-primary)]">{stepTitle}</div>
|
||||
) : (
|
||||
<div className="text-sm text-[var(--text-secondary)] italic">
|
||||
Sin información
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle string
|
||||
if (typeof instructions === 'string') {
|
||||
return (
|
||||
<div className="text-sm text-[var(--text-primary)] whitespace-pre-wrap">
|
||||
{instructions}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle object
|
||||
if (typeof instructions === 'object') {
|
||||
return (
|
||||
<div className="text-sm text-[var(--text-primary)] whitespace-pre-wrap">
|
||||
{formatJsonField(instructions)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-sm text-[var(--text-secondary)] italic">
|
||||
No especificado
|
||||
</div>
|
||||
);
|
||||
} : undefined
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1105,43 +1191,11 @@ const RecipesPage: React.FC = () => {
|
||||
fields: [
|
||||
{
|
||||
label: '',
|
||||
value: modalMode === 'edit'
|
||||
? (editedIngredients.length > 0 ? editedIngredients : selectedRecipe.ingredients || [])
|
||||
: (selectedRecipe.ingredients
|
||||
?.sort((a, b) => a.ingredient_order - b.ingredient_order)
|
||||
?.map(ing => {
|
||||
const ingredientName = ingredientLookup[ing.ingredient_id] || 'Ingrediente desconocido';
|
||||
const parts = [];
|
||||
|
||||
// Main ingredient line with quantity
|
||||
parts.push(`${ing.quantity} ${ing.unit}`);
|
||||
parts.push(ingredientName);
|
||||
|
||||
// Add optional indicator
|
||||
if (ing.is_optional) {
|
||||
parts.push('(opcional)');
|
||||
}
|
||||
|
||||
// Add preparation method on new line if exists
|
||||
if (ing.preparation_method) {
|
||||
parts.push(`\n → ${ing.preparation_method}`);
|
||||
}
|
||||
|
||||
// Add notes on new line if exists
|
||||
if (ing.ingredient_notes) {
|
||||
parts.push(`\n 💡 ${ing.ingredient_notes}`);
|
||||
}
|
||||
|
||||
// Add substitution info if exists
|
||||
if (ing.substitution_options) {
|
||||
parts.push(`\n 🔄 Sustituto: ${ing.substitution_options}`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}) || ['No especificados']),
|
||||
type: modalMode === 'edit' ? 'component' as const : 'custom' as const,
|
||||
value: '',
|
||||
type: modalMode === 'edit' ? ('component' as const) : ('custom' as const),
|
||||
component: modalMode === 'edit' ? IngredientsEditComponent : undefined,
|
||||
componentProps: modalMode === 'edit' ? {
|
||||
value: editedIngredients.length > 0 ? editedIngredients : (selectedRecipe.ingredients || []),
|
||||
availableIngredients,
|
||||
unitOptions,
|
||||
onChange: (newIngredients: RecipeIngredientResponse[]) => {
|
||||
@@ -1208,7 +1262,7 @@ const RecipesPage: React.FC = () => {
|
||||
<div className="flex items-start gap-2 text-[var(--text-secondary)]">
|
||||
<span className="flex-shrink-0">🔄</span>
|
||||
<span>
|
||||
<strong>Sustituto:</strong> {ing.substitution_options}
|
||||
<strong>Sustituto:</strong> {typeof ing.substitution_options === 'string' ? ing.substitution_options : JSON.stringify(ing.substitution_options)}
|
||||
{ing.substitution_ratio && ` (ratio: ${ing.substitution_ratio})`}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1519,37 +1573,20 @@ const RecipesPage: React.FC = () => {
|
||||
|
||||
{/* Recipe Details Modal */}
|
||||
{showForm && selectedRecipe && (
|
||||
<EditViewModal
|
||||
<RecipeViewEditModal
|
||||
isOpen={showForm}
|
||||
onClose={() => {
|
||||
setShowForm(false);
|
||||
setSelectedRecipe(null);
|
||||
setModalMode('view');
|
||||
setEditedRecipe({});
|
||||
setEditedIngredients([]);
|
||||
}}
|
||||
recipe={selectedRecipe}
|
||||
mode={modalMode}
|
||||
onModeChange={(newMode) => {
|
||||
setModalMode(newMode);
|
||||
if (newMode === 'view') {
|
||||
setEditedRecipe({});
|
||||
setEditedIngredients([]);
|
||||
} else if (newMode === 'edit' && selectedRecipe) {
|
||||
// Initialize edited ingredients when entering edit mode
|
||||
setEditedIngredients(selectedRecipe.ingredients || []);
|
||||
}
|
||||
}}
|
||||
title={selectedRecipe.name}
|
||||
subtitle={selectedRecipe.description || ''}
|
||||
statusIndicator={getRecipeStatusConfig(selectedRecipe)}
|
||||
size="xl"
|
||||
sections={getModalSections()}
|
||||
onFieldChange={handleFieldChange}
|
||||
showDefaultActions={true}
|
||||
onSave={handleSaveRecipe}
|
||||
waitForRefetch={true}
|
||||
isRefetching={isRefetchingRecipes}
|
||||
onSaveComplete={handleRecipeSaveComplete}
|
||||
isSaving={updateRecipeMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -61,9 +61,9 @@ const LandingPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Scarcity Badge */}
|
||||
<div className="mb-8 inline-block">
|
||||
<div className="bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50 dark:from-amber-900/20 dark:via-orange-900/20 dark:to-amber-900/20 border-2 border-amber-400 dark:border-amber-500 rounded-full px-6 py-3 shadow-lg hover:shadow-xl transition-shadow">
|
||||
<p className="text-sm font-bold text-amber-700 dark:text-amber-300">
|
||||
<div className="mb-8 inline-flex">
|
||||
<div className="bg-gradient-to-r from-amber-50 via-orange-50 to-amber-50 dark:from-amber-900/20 dark:via-orange-900/20 dark:to-amber-900/20 border-2 border-amber-400 dark:border-amber-500 rounded-full px-5 py-2 shadow-lg hover:shadow-xl transition-all hover:scale-105">
|
||||
<p className="text-sm font-bold text-amber-700 dark:text-amber-300 leading-tight">
|
||||
🔥 {t('landing:hero.scarcity', 'Solo 12 plazas restantes de 20 • 3 meses GRATIS')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user