Files
bakery-ia/frontend/src/examples/RecipesExample.tsx

542 lines
14 KiB
TypeScript
Raw Normal View History

/**
* Example usage of the restructured recipes API
* Demonstrates tenant-dependent routing and React Query hooks
*/
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
useRecipes,
useRecipe,
useCreateRecipe,
useUpdateRecipe,
useDeleteRecipe,
useDuplicateRecipe,
useActivateRecipe,
useRecipeStatistics,
useRecipeCategories,
useRecipeFeasibility,
type RecipeResponse,
type RecipeCreate,
type RecipeSearchParams,
MeasurementUnit,
} from '../api';
import { useCurrentTenant } from '../stores/tenant.store';
/**
* Example: Recipe List Component
* Shows how to use the tenant-dependent useRecipes hook
*/
export const RecipesList: React.FC = () => {
const { t } = useTranslation('recipes');
const currentTenant = useCurrentTenant();
const [filters, setFilters] = useState<RecipeSearchParams>({
limit: 20,
offset: 0,
});
// Use tenant-dependent recipes hook
const {
data: recipes,
isLoading,
error,
refetch,
} = useRecipes(currentTenant?.id || '', filters, {
enabled: !!currentTenant?.id,
});
if (!currentTenant) {
return <div>{t('messages.no_tenant_selected')}</div>;
}
if (isLoading) {
return <div>{t('messages.loading_recipes')}</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div className="recipes-list">
<h2>{t('title')}</h2>
{/* Search and filters */}
<div className="filters">
<input
type="text"
placeholder={t('filters.search_placeholder')}
value={filters.search_term || ''}
onChange={(e) => setFilters({ ...filters, search_term: e.target.value })}
/>
<select
value={filters.status || ''}
onChange={(e) => setFilters({ ...filters, status: e.target.value })}
>
<option value="">{t('filters.all')}</option>
<option value="active">{t('status.active')}</option>
<option value="draft">{t('status.draft')}</option>
</select>
</div>
{/* Recipe cards */}
<div className="recipe-grid">
{recipes?.map((recipe) => (
<RecipeCard key={recipe.id} recipe={recipe} />
))}
</div>
{(!recipes || recipes.length === 0) && (
<div className="no-results">{t('messages.no_recipes_found')}</div>
)}
</div>
);
};
/**
* Example: Individual Recipe Card
*/
interface RecipeCardProps {
recipe: RecipeResponse;
}
const RecipeCard: React.FC<RecipeCardProps> = ({ recipe }) => {
const { t } = useTranslation('recipes');
const currentTenant = useCurrentTenant();
// Mutation hooks for recipe actions
const duplicateRecipe = useDuplicateRecipe(currentTenant?.id || '');
const activateRecipe = useActivateRecipe(currentTenant?.id || '');
const deleteRecipe = useDeleteRecipe(currentTenant?.id || '');
const handleDuplicate = () => {
if (!currentTenant) return;
duplicateRecipe.mutate({
id: recipe.id,
data: { new_name: `${recipe.name} (Copy)` }
}, {
onSuccess: () => {
alert(t('messages.recipe_duplicated'));
},
onError: (error) => {
alert(error.message);
}
});
};
const handleActivate = () => {
if (!currentTenant) return;
activateRecipe.mutate(recipe.id, {
onSuccess: () => {
alert(t('messages.recipe_activated'));
}
});
};
const handleDelete = () => {
if (!currentTenant) return;
if (confirm(t('messages.confirm_delete'))) {
deleteRecipe.mutate(recipe.id, {
onSuccess: () => {
alert(t('messages.recipe_deleted'));
}
});
}
};
return (
<div className="recipe-card">
<h3>{recipe.name}</h3>
<p>{recipe.description}</p>
<div className="recipe-meta">
<span className={`status status-${recipe.status}`}>
{t(`status.${recipe.status}`)}
</span>
<span className="category">{recipe.category}</span>
<span className="difficulty">
{t(`difficulty.${recipe.difficulty_level}`)}
</span>
</div>
<div className="recipe-actions">
<button onClick={handleDuplicate} disabled={duplicateRecipe.isPending}>
{t('actions.duplicate_recipe')}
</button>
{recipe.status === 'draft' && (
<button onClick={handleActivate} disabled={activateRecipe.isPending}>
{t('actions.activate_recipe')}
</button>
)}
<button onClick={handleDelete} disabled={deleteRecipe.isPending}>
{t('actions.delete_recipe')}
</button>
</div>
</div>
);
};
/**
* Example: Recipe Detail View
*/
interface RecipeDetailProps {
recipeId: string;
}
export const RecipeDetail: React.FC<RecipeDetailProps> = ({ recipeId }) => {
const { t } = useTranslation('recipes');
const currentTenant = useCurrentTenant();
// Get individual recipe with tenant context
const {
data: recipe,
isLoading,
error,
} = useRecipe(currentTenant?.id || '', recipeId, {
enabled: !!(currentTenant?.id && recipeId),
});
// Check feasibility
const {
data: feasibility,
} = useRecipeFeasibility(
currentTenant?.id || '',
recipeId,
1.0, // batch multiplier
{
enabled: !!(currentTenant?.id && recipeId),
}
);
if (!currentTenant) {
return <div>{t('messages.no_tenant_selected')}</div>;
}
if (isLoading) {
return <div>{t('messages.loading_recipe')}</div>;
}
if (error || !recipe) {
return <div>Recipe not found</div>;
}
return (
<div className="recipe-detail">
<header className="recipe-header">
<h1>{recipe.name}</h1>
<p>{recipe.description}</p>
<div className="recipe-info">
<span>Yield: {recipe.yield_quantity} {recipe.yield_unit}</span>
<span>Prep: {recipe.prep_time_minutes}min</span>
<span>Cook: {recipe.cook_time_minutes}min</span>
<span>Total: {recipe.total_time_minutes}min</span>
</div>
</header>
{/* Feasibility check */}
{feasibility && (
<div className={`feasibility ${feasibility.feasible ? 'feasible' : 'not-feasible'}`}>
<h3>{t('feasibility.title')}</h3>
<p>
{feasibility.feasible
? t('feasibility.feasible')
: t('feasibility.not_feasible')
}
</p>
{feasibility.missing_ingredients.length > 0 && (
<div>
<h4>{t('feasibility.missing_ingredients')}</h4>
<ul>
{feasibility.missing_ingredients.map((ingredient, index) => (
<li key={index}>{JSON.stringify(ingredient)}</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Ingredients */}
<section className="ingredients">
<h2>{t('ingredients.title')}</h2>
<ul>
{recipe.ingredients?.map((ingredient) => (
<li key={ingredient.id}>
{ingredient.quantity} {ingredient.unit} - {ingredient.ingredient_id}
{ingredient.preparation_method && (
<span className="prep-method"> ({ingredient.preparation_method})</span>
)}
{ingredient.is_optional && (
<span className="optional"> ({t('ingredients.is_optional')})</span>
)}
</li>
))}
</ul>
</section>
{/* Instructions */}
{recipe.instructions && (
<section className="instructions">
<h2>{t('fields.instructions')}</h2>
<div>{JSON.stringify(recipe.instructions, null, 2)}</div>
</section>
)}
</div>
);
};
/**
* Example: Recipe Creation Form
*/
export const CreateRecipeForm: React.FC = () => {
const { t } = useTranslation('recipes');
const currentTenant = useCurrentTenant();
const [formData, setFormData] = useState<RecipeCreate>({
name: '',
finished_product_id: '',
yield_quantity: 1,
yield_unit: MeasurementUnit.UNITS,
difficulty_level: 1,
batch_size_multiplier: 1.0,
is_seasonal: false,
is_signature_item: false,
ingredients: [],
});
const createRecipe = useCreateRecipe(currentTenant?.id || '', {
onSuccess: (data) => {
alert(t('messages.recipe_created'));
// Reset form or redirect
setFormData({
name: '',
finished_product_id: '',
yield_quantity: 1,
yield_unit: MeasurementUnit.UNITS,
difficulty_level: 1,
batch_size_multiplier: 1.0,
is_seasonal: false,
is_signature_item: false,
ingredients: [],
});
},
onError: (error) => {
alert(error.message);
}
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!currentTenant) {
alert('No tenant selected');
return;
}
if (!formData.name.trim()) {
alert(t('messages.recipe_name_required'));
return;
}
if (formData.ingredients.length === 0) {
alert(t('messages.at_least_one_ingredient'));
return;
}
createRecipe.mutate(formData);
};
if (!currentTenant) {
return <div>{t('messages.no_tenant_selected')}</div>;
}
return (
<form onSubmit={handleSubmit} className="create-recipe-form">
<h2>{t('actions.create_recipe')}</h2>
<div className="form-group">
<label htmlFor="name">{t('fields.name')}</label>
<input
id="name"
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={t('placeholders.recipe_name')}
required
/>
</div>
<div className="form-group">
<label htmlFor="description">{t('fields.description')}</label>
<textarea
id="description"
value={formData.description || ''}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder={t('placeholders.description')}
/>
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="yield_quantity">{t('fields.yield_quantity')}</label>
<input
id="yield_quantity"
type="number"
min="0.1"
step="0.1"
value={formData.yield_quantity}
onChange={(e) => setFormData({ ...formData, yield_quantity: parseFloat(e.target.value) })}
required
/>
</div>
<div className="form-group">
<label htmlFor="yield_unit">{t('fields.yield_unit')}</label>
<select
id="yield_unit"
value={formData.yield_unit}
onChange={(e) => setFormData({ ...formData, yield_unit: e.target.value as MeasurementUnit })}
required
>
{Object.values(MeasurementUnit).map((unit) => (
<option key={unit} value={unit}>
{t(`units.${unit}`)}
</option>
))}
</select>
</div>
</div>
<div className="form-group">
<label htmlFor="difficulty_level">{t('fields.difficulty_level')}</label>
<select
id="difficulty_level"
value={formData.difficulty_level}
onChange={(e) => setFormData({ ...formData, difficulty_level: parseInt(e.target.value) })}
>
{[1, 2, 3, 4, 5].map((level) => (
<option key={level} value={level}>
{t(`difficulty.${level}`)}
</option>
))}
</select>
</div>
<div className="form-checkboxes">
<label>
<input
type="checkbox"
checked={formData.is_signature_item}
onChange={(e) => setFormData({ ...formData, is_signature_item: e.target.checked })}
/>
{t('fields.is_signature')}
</label>
<label>
<input
type="checkbox"
checked={formData.is_seasonal}
onChange={(e) => setFormData({ ...formData, is_seasonal: e.target.checked })}
/>
{t('fields.is_seasonal')}
</label>
</div>
{/* Ingredients section would go here */}
<div className="ingredients-section">
<h3>{t('ingredients.title')}</h3>
<p>{t('messages.no_ingredients')}</p>
{/* Add ingredient form components here */}
</div>
<div className="form-actions">
<button
type="submit"
disabled={createRecipe.isPending}
className="btn-primary"
>
{createRecipe.isPending ? 'Creating...' : t('actions.create_recipe')}
</button>
</div>
</form>
);
};
/**
* Example: Recipe Statistics Dashboard
*/
export const RecipeStatistics: React.FC = () => {
const { t } = useTranslation('recipes');
const currentTenant = useCurrentTenant();
const { data: stats, isLoading } = useRecipeStatistics(currentTenant?.id || '', {
enabled: !!currentTenant?.id,
});
const { data: categories } = useRecipeCategories(currentTenant?.id || '', {
enabled: !!currentTenant?.id,
});
if (!currentTenant) {
return <div>{t('messages.no_tenant_selected')}</div>;
}
if (isLoading) {
return <div>Loading statistics...</div>;
}
return (
<div className="recipe-statistics">
<h2>{t('statistics.title')}</h2>
{stats && (
<div className="stats-grid">
<div className="stat-card">
<h3>{t('statistics.total_recipes')}</h3>
<span className="stat-number">{stats.total_recipes}</span>
</div>
<div className="stat-card">
<h3>{t('statistics.active_recipes')}</h3>
<span className="stat-number">{stats.active_recipes}</span>
</div>
<div className="stat-card">
<h3>{t('statistics.signature_recipes')}</h3>
<span className="stat-number">{stats.signature_recipes}</span>
</div>
<div className="stat-card">
<h3>{t('statistics.seasonal_recipes')}</h3>
<span className="stat-number">{stats.seasonal_recipes}</span>
</div>
</div>
)}
{categories && (
<div className="categories-list">
<h3>Categories</h3>
<ul>
{categories.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
</div>
)}
</div>
);
};
export default {
RecipesList,
RecipeDetail,
CreateRecipeForm,
RecipeStatistics,
};