Create the frontend receipes page to use real API 2

This commit is contained in:
Urtzi Alfaro
2025-09-20 08:24:03 +02:00
parent d18c64ce6e
commit 66ef2121a1
6 changed files with 177 additions and 82 deletions

View File

@@ -57,7 +57,7 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [mode, setMode] = useState<'overview' | 'edit'>('edit'); const [mode, setMode] = useState<'view' | 'edit'>('edit');
// Get tenant and fetch inventory data // Get tenant and fetch inventory data
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
@@ -280,7 +280,8 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
} }
}; };
const getModalSections = () => [ const getModalSections = () => {
const sections = [
{ {
title: 'Información Básica', title: 'Información Básica',
icon: ChefHat, icon: ChefHat,
@@ -588,12 +589,21 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
} }
]; ];
// Add editable: true to all fields since this is a creation modal
return sections.map(section => ({
...section,
fields: section.fields.map(field => ({
...field,
editable: field.readonly !== true // Make editable unless explicitly readonly
}))
}));
};
return ( return (
<StatusModal <StatusModal
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
mode={mode} mode="edit"
onModeChange={setMode}
title="Nueva Receta" title="Nueva Receta"
subtitle="Crear una nueva receta para la panadería" subtitle="Crear una nueva receta para la panadería"
statusIndicator={{ statusIndicator={{
@@ -606,15 +616,9 @@ export const CreateRecipeModal: React.FC<CreateRecipeModalProps> = ({
size="xl" size="xl"
sections={getModalSections()} sections={getModalSections()}
onFieldChange={handleFieldChange} onFieldChange={handleFieldChange}
actions={[ showDefaultActions={true}
{ onSave={handleSubmit}
label: loading ? 'Creando...' : 'Crear Receta', onCancel={onClose}
icon: ChefHat,
variant: 'primary',
onClick: handleSubmit,
disabled: loading || !formData.name.trim() || !formData.finished_product_id.trim()
}
]}
/> />
); );
}; };

View File

@@ -0,0 +1,122 @@
import React from 'react';
import { Card } from '../Card';
export interface TabItem {
id: string;
label: string;
disabled?: boolean;
}
export interface TabsProps {
items: TabItem[];
activeTab: string;
onTabChange: (tabId: string) => void;
variant?: 'default' | 'pills' | 'underline';
size?: 'sm' | 'md' | 'lg';
fullWidth?: boolean;
className?: string;
}
const Tabs: React.FC<TabsProps> = ({
items,
activeTab,
onTabChange,
variant = 'pills',
size = 'md',
fullWidth = false,
className = ''
}) => {
// Size classes
const sizeClasses = {
sm: 'px-2 py-1.5 text-xs',
md: 'px-3 sm:px-4 py-3 sm:py-2 text-sm',
lg: 'px-4 sm:px-6 py-4 sm:py-3 text-base'
};
// Variant styles
const getVariantClasses = (isActive: boolean, isDisabled: boolean) => {
if (isDisabled) {
return 'text-[var(--text-tertiary)] cursor-not-allowed opacity-50';
}
switch (variant) {
case 'pills':
return isActive
? 'bg-[var(--color-primary)] text-white'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]';
case 'underline':
return isActive
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)]'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] border-b-2 border-transparent';
default:
return isActive
? 'bg-[var(--color-primary)] text-white'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]';
}
};
const baseButtonClasses = `
font-medium transition-colors
${fullWidth ? 'flex-1' : 'sm:flex-none'}
${variant === 'pills' ? 'rounded-md' : ''}
${sizeClasses[size]}
`.trim();
if (variant === 'underline') {
return (
<div className={`border-b border-[var(--border-primary)] ${className}`}>
<nav className="flex">
{items.map((item) => {
const isActive = activeTab === item.id;
const isDisabled = item.disabled || false;
return (
<button
key={item.id}
onClick={() => !isDisabled && onTabChange(item.id)}
disabled={isDisabled}
className={`
${baseButtonClasses}
${getVariantClasses(isActive, isDisabled)}
`}
type="button"
>
{item.label}
</button>
);
})}
</nav>
</div>
);
}
return (
<Card className={`p-1 ${className}`}>
<div className="flex space-x-1">
{items.map((item) => {
const isActive = activeTab === item.id;
const isDisabled = item.disabled || false;
return (
<button
key={item.id}
onClick={() => !isDisabled && onTabChange(item.id)}
disabled={isDisabled}
className={`
${baseButtonClasses}
${getVariantClasses(isActive, isDisabled)}
`}
type="button"
>
{item.label}
</button>
);
})}
</div>
</Card>
);
};
export default Tabs;

View File

@@ -0,0 +1,2 @@
export { default as Tabs } from './Tabs';
export type { TabsProps, TabItem } from './Tabs';

View File

@@ -9,6 +9,7 @@ export { default as Avatar } from './Avatar';
export { default as Tooltip } from './Tooltip'; export { default as Tooltip } from './Tooltip';
export { default as Select } from './Select'; export { default as Select } from './Select';
export { default as DatePicker } from './DatePicker'; export { default as DatePicker } from './DatePicker';
export { Tabs } from './Tabs';
export { ThemeToggle } from './ThemeToggle'; export { ThemeToggle } from './ThemeToggle';
export { ProgressBar } from './ProgressBar'; export { ProgressBar } from './ProgressBar';
export { StatusIndicator } from './StatusIndicator'; export { StatusIndicator } from './StatusIndicator';
@@ -29,6 +30,7 @@ export type { AvatarProps } from './Avatar';
export type { TooltipProps } from './Tooltip'; export type { TooltipProps } from './Tooltip';
export type { SelectProps, SelectOption } from './Select'; export type { SelectProps, SelectOption } from './Select';
export type { DatePickerProps } from './DatePicker'; export type { DatePickerProps } from './DatePicker';
export type { TabsProps, TabItem } from './Tabs';
export type { ThemeToggleProps } from './ThemeToggle'; export type { ThemeToggleProps } from './ThemeToggle';
export type { ProgressBarProps } from './ProgressBar'; export type { ProgressBarProps } from './ProgressBar';
export type { StatusIndicatorProps } from './StatusIndicator'; export type { StatusIndicatorProps } from './StatusIndicator';

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Plus, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Loader, Euro } from 'lucide-react'; import { Plus, Clock, Package, Eye, Edit, CheckCircle, AlertCircle, Timer, Users, Loader, Euro } from 'lucide-react';
import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui'; import { Button, Input, Card, Badge, StatsGrid, StatusCard, getStatusColor, StatusModal, Tabs } from '../../../../components/ui';
import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
import { import {
@@ -302,31 +302,18 @@ const OrdersPage: React.FC = () => {
]} ]}
/> />
{/* Tabs - Mobile-friendly */} {/* Tabs */}
<Card className="p-1"> <Tabs
<div className="flex space-x-1"> items={[
<button { id: 'orders', label: 'Pedidos' },
onClick={() => setActiveTab('orders')} { id: 'customers', label: 'Clientes' }
className={`flex-1 sm:flex-none px-3 sm:px-4 py-3 sm:py-2 rounded-md text-sm font-medium transition-colors ${ ]}
activeTab === 'orders' activeTab={activeTab}
? 'bg-[var(--color-primary)] text-white' onTabChange={(tabId) => setActiveTab(tabId as 'orders' | 'customers')}
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]' fullWidth={true}
}`} variant="pills"
> size="md"
Pedidos />
</button>
<button
onClick={() => setActiveTab('customers')}
className={`flex-1 sm:flex-none px-3 sm:px-4 py-3 sm:py-2 rounded-md text-sm font-medium transition-colors ${
activeTab === 'customers'
? 'bg-[var(--color-primary)] text-white'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'
}`}
>
Clientes
</button>
</div>
</Card>
{/* Stats Grid */} {/* Stats Grid */}
<StatsGrid <StatsGrid

View File

@@ -6,7 +6,7 @@ import { formatters } from '../../../../components/ui/Stats/StatsPresets';
import { PageHeader } from '../../../../components/layout'; import { PageHeader } from '../../../../components/layout';
import { useRecipes, useRecipeStatistics, useCreateRecipe, useUpdateRecipe, useDeleteRecipe } from '../../../../api/hooks/recipes'; import { useRecipes, useRecipeStatistics, useCreateRecipe, useUpdateRecipe, useDeleteRecipe } from '../../../../api/hooks/recipes';
import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useCurrentTenant } from '../../../../stores/tenant.store';
import type { RecipeResponse, RecipeCreate } from '../../../../api/types/recipes'; import type { RecipeResponse, RecipeCreate, MeasurementUnit } from '../../../../api/types/recipes';
import { CreateRecipeModal } from '../../../../components/domain/recipes'; import { CreateRecipeModal } from '../../../../components/domain/recipes';
const RecipesPage: React.FC = () => { const RecipesPage: React.FC = () => {
@@ -201,6 +201,12 @@ const RecipesPage: React.FC = () => {
cook_time_minutes: typeof editedRecipe.cook_time_minutes === 'string' cook_time_minutes: typeof editedRecipe.cook_time_minutes === 'string'
? parseInt(editedRecipe.cook_time_minutes.toString()) ? parseInt(editedRecipe.cook_time_minutes.toString())
: editedRecipe.cook_time_minutes, : editedRecipe.cook_time_minutes,
// Ensure yield_unit is properly typed
yield_unit: editedRecipe.yield_unit ? editedRecipe.yield_unit as MeasurementUnit : undefined,
// Convert difficulty level to number if needed
difficulty_level: typeof editedRecipe.difficulty_level === 'string'
? parseInt(editedRecipe.difficulty_level.toString())
: editedRecipe.difficulty_level,
}; };
await updateRecipeMutation.mutateAsync({ await updateRecipeMutation.mutateAsync({
@@ -423,31 +429,28 @@ const RecipesPage: React.FC = () => {
`${recipe.ingredients?.length || 0} ingredientes principales` `${recipe.ingredients?.length || 0} ingredientes principales`
]} ]}
actions={[ actions={[
// Primary action - View recipe details
{ {
label: 'Ver', label: 'Ver Detalles',
icon: Eye, icon: Eye,
variant: 'outline', variant: 'primary',
priority: 'primary',
onClick: () => { onClick: () => {
setSelectedRecipe(recipe); setSelectedRecipe(recipe);
setModalMode('view'); setModalMode('view');
setShowForm(true); setShowForm(true);
} }
}, },
// Secondary action - Edit recipe
{ {
label: 'Editar', label: 'Editar',
icon: Edit, icon: Edit,
variant: 'outline', priority: 'secondary',
onClick: () => { onClick: () => {
setSelectedRecipe(recipe); setSelectedRecipe(recipe);
setModalMode('edit'); setModalMode('edit');
setShowForm(true); setShowForm(true);
} }
},
{
label: 'Producir',
icon: ChefHat,
variant: 'primary',
onClick: () => console.log('Produce recipe', recipe.id)
} }
]} ]}
/> />
@@ -483,45 +486,20 @@ const RecipesPage: React.FC = () => {
setEditedRecipe({}); setEditedRecipe({});
}} }}
mode={modalMode} mode={modalMode}
onModeChange={setModalMode} onModeChange={(newMode) => {
setModalMode(newMode);
if (newMode === 'view') {
setEditedRecipe({});
}
}}
title={selectedRecipe.name} title={selectedRecipe.name}
subtitle={selectedRecipe.description || ''} subtitle={selectedRecipe.description || ''}
statusIndicator={getRecipeStatusConfig(selectedRecipe)} statusIndicator={getRecipeStatusConfig(selectedRecipe)}
size="xl" size="xl"
sections={getModalSections()} sections={getModalSections()}
onFieldChange={handleFieldChange} onFieldChange={handleFieldChange}
actions={modalMode === 'edit' ? [ showDefaultActions={true}
{ onSave={handleSaveRecipe}
label: 'Guardar',
icon: ChefHat,
variant: 'primary',
onClick: handleSaveRecipe,
disabled: updateRecipeMutation.isPending
},
{
label: 'Cancelar',
variant: 'outline',
onClick: () => {
setModalMode('view');
setEditedRecipe({});
}
}
] : [
{
label: 'Producir',
icon: ChefHat,
variant: 'primary',
onClick: () => {
console.log('Producing recipe:', selectedRecipe.id);
setShowForm(false);
setSelectedRecipe(null);
}
}
]}
onEdit={() => {
setModalMode('edit');
setEditedRecipe({});
}}
/> />
)} )}