Improve frontend 5

This commit is contained in:
Urtzi Alfaro
2025-11-20 19:14:49 +01:00
parent 29e6ddcea9
commit 4433b66f25
30 changed files with 3649 additions and 600 deletions

View File

@@ -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}
/>
)}

View File

@@ -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>