Create the frontend receipes page to use real API 2
This commit is contained in:
@@ -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()
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
122
frontend/src/components/ui/Tabs/Tabs.tsx
Normal file
122
frontend/src/components/ui/Tabs/Tabs.tsx
Normal 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;
|
||||||
2
frontend/src/components/ui/Tabs/index.ts
Normal file
2
frontend/src/components/ui/Tabs/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Tabs } from './Tabs';
|
||||||
|
export type { TabsProps, TabItem } from './Tabs';
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user