From db431553b96355b35de6801a59db9022b34f7b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Thu, 25 Sep 2025 19:52:55 +0000 Subject: [PATCH] Last Used implementiert --- NODEJS_README.md | 23 ++++++++ backend/dist/routes/recipes.d.ts.map | 2 +- backend/dist/routes/recipes.js | 7 +++ backend/dist/routes/recipes.js.map | 2 +- backend/prisma/schema.prisma | 2 + backend/src/routes/recipes.ts | 68 +++++++++++++++++++++- frontend/src/components/RecipeDetail.css | 36 ++++++++++++ frontend/src/components/RecipeDetail.tsx | 74 ++++++++++++++++++++++++ frontend/src/components/RecipeList.css | 23 ++++++++ frontend/src/components/RecipeList.tsx | 33 ++++++++++- frontend/src/services/api.ts | 7 +++ 11 files changed, 270 insertions(+), 7 deletions(-) diff --git a/NODEJS_README.md b/NODEJS_README.md index ae2cf69..387685c 100644 --- a/NODEJS_README.md +++ b/NODEJS_README.md @@ -118,6 +118,29 @@ npm run dev ### Recipes - `GET /api/recipes` - List recipes (pagination, search, filter) - `GET /api/recipes/:id` - Get single recipe +- `lastUsed` wird nur noch gesetzt/aktualisiert wenn ein Rezept aktiv als "zubereitet" markiert wird (siehe Endpoint unten) – reines Ansehen ändert den Wert nicht mehr. + +### Rezept als zubereitet markieren + +POST `/api/recipes/:id/cooked` + +Antwort: +``` +{ "success": true, "message": "Rezept als zubereitet markiert", "data": { "id": 123, "lastUsed": "2025-09-25T18:47:12.123Z" } } +``` + +Verwendung: Button "Zubereitet" auf der Detailseite oder manuell per API. Dadurch kann die Liste nach "Zuletzt zubereitet" sortiert werden (`?sortBy=lastUsed&sortOrder=desc`). + +### Zuletzt zubereitete Rezepte + +`GET /api/recipes/recent?limit=10` + +Liefert die zuletzt markierten Rezepte (nur solche mit gesetztem `lastUsed`) absteigend sortiert. `limit` (1–50) steuert die Anzahl. + +Validierungen / Hinweise: +- Zukunftsdaten werden (±1 Minute Toleranz) mit 400 abgelehnt. +- Rezepte ohne `lastUsed` können in der Liste mit einem "Nie" Badge erscheinen. +- Index auf `last_used` verbessert Sortier-Performance (`CREATE INDEX idx_rezepte_last_used ON Rezepte (last_used);`). - `POST /api/recipes` - Create recipe - `PUT /api/recipes/:id` - Update recipe - `DELETE /api/recipes/:id` - Delete recipe diff --git a/backend/dist/routes/recipes.d.ts.map b/backend/dist/routes/recipes.d.ts.map index 9eeebde..9404d49 100644 --- a/backend/dist/routes/recipes.d.ts.map +++ b/backend/dist/routes/recipes.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAoQxB,eAAe,MAAM,CAAC"} \ No newline at end of file +{"version":3,"file":"recipes.d.ts","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA6QxB,eAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/backend/dist/routes/recipes.js b/backend/dist/routes/recipes.js index bdfa36f..c7be8ca 100644 --- a/backend/dist/routes/recipes.js +++ b/backend/dist/routes/recipes.js @@ -109,6 +109,13 @@ router.get('/:id', async (req, res, next) => { console.log(`Could not load separate ingredients for recipe ${recipe.recipeNumber}:`, ingredientError); } } + const now = new Date(); + prisma.recipe.update({ + where: { id: recipeId }, + data: { lastUsed: now }, + select: { id: true } + }).catch(err => console.warn('lastUsed update failed for recipe', recipeId, err.message)); + recipe.lastUsed = now; return res.json({ success: true, data: recipe, diff --git a/backend/dist/routes/recipes.js.map b/backend/dist/routes/recipes.js.map index 43ee7a7..f026a2a 100644 --- a/backend/dist/routes/recipes.js.map +++ b/backend/dist/routes/recipes.js.map @@ -1 +1 @@ -{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/routes/recipes.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index bcf0530..4c9151d 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -22,12 +22,14 @@ model Recipe { ingredients String? @map("Zutaten") @db.Text instructions String? @map("Zubereitung") @db.Text comment String? @map("Kommentar") @db.Text + lastUsed DateTime? @map("last_used") @db.DateTime(0) // Relations images RecipeImage[] ingredientsList Ingredient[] @@map("Rezepte") + @@index([lastUsed]) } model Ingredient { diff --git a/backend/src/routes/recipes.ts b/backend/src/routes/recipes.ts index b455152..7af3ecc 100644 --- a/backend/src/routes/recipes.ts +++ b/backend/src/routes/recipes.ts @@ -50,6 +50,10 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => { where.category = { contains: category as string }; } + // Whitelist allowed sort fields to prevent SQL injection vectors via Prisma + const allowedSortFields = new Set(['title','category','servings','recipeNumber','lastUsed','id']); + const chosenSortField = allowedSortFields.has(String(sortBy)) ? String(sortBy) : 'title'; + const [recipes, total] = await Promise.all([ prisma.recipe.findMany({ where, @@ -57,7 +61,8 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => { images: true, ingredientsList: true, }, - orderBy: { [sortBy as string]: sortOrder as 'asc' | 'desc' }, + // @ts-ignore lastUsed exists in DB schema; Prisma typing cache issue workaround + orderBy: { [chosenSortField]: sortOrder === 'desc' ? 'desc' : 'asc' }, skip, take: limitNum, }), @@ -79,8 +84,24 @@ router.get('/', async (req: Request, res: Response, next: NextFunction) => { } }); +// Neueste zubereitete Rezepte (nach lastUsed sortiert) – vor :id Route platzieren +router.get('/recent', async (req: Request, res: Response, next: NextFunction) => { + try { + const { limit = '10' } = req.query; + const take = Math.min(Math.max(parseInt(limit as string) || 10, 1), 50); + const recipes = await prisma.recipe.findMany({ + // @ts-ignore lastUsed exists + where: { lastUsed: { not: null } }, + // @ts-ignore lastUsed exists + orderBy: { lastUsed: 'desc' }, + take, + include: { images: true } + }); + return res.json({ success: true, data: recipes }); + } catch (error) { next(error); } +}); + // Get single recipe by ID -// Get recipe by ID router.get('/:id', async (req: Request, res: Response, next: NextFunction) => { try { const { id } = req.params; @@ -135,6 +156,8 @@ router.get('/:id', async (req: Request, res: Response, next: NextFunction) => { } } + // lastUsed wird nicht mehr beim Ansehen aktualisiert, sondern explizit über einen "cooked" Endpoint + return res.json({ success: true, data: recipe, @@ -262,4 +285,45 @@ router.delete('/:id', async (req: Request, res: Response, next: NextFunction) => } }); +// Endpoint um ein Rezept als "zubereitet" zu markieren -> aktualisiert lastUsed +router.post('/:id/cooked', async (req: Request, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + if (!id) { + return res.status(400).json({ success: false, message: 'Recipe ID is required' }); + } + const recipeId = parseInt(id, 10); + if (isNaN(recipeId)) { + return res.status(400).json({ success: false, message: 'Invalid recipe ID format' }); + } + + let customDate: Date | null = null; + if (req.body && req.body.lastUsed) { + const parsed = new Date(req.body.lastUsed); + if (isNaN(parsed.getTime())) { + return res.status(400).json({ success: false, message: 'Ungültiges Datum (lastUsed)' }); + } + customDate = parsed; + } + + const targetDate = customDate || new Date(); + if (targetDate.getTime() > Date.now() + 60_000) { // 1 Minute Puffer + return res.status(400).json({ success: false, message: 'Datum liegt in der Zukunft' }); + } + const updated = await prisma.recipe.update({ + where: { id: recipeId }, + // @ts-ignore lastUsed exists + data: { lastUsed: targetDate } + }); + + // @ts-ignore lastUsed field is present in database + return res.json({ success: true, message: 'Rezept als zubereitet markiert', data: { id: updated.id, lastUsed: (updated as any).lastUsed } }); + } catch (error) { + if ((error as any).code === 'P2025') { + return res.status(404).json({ success: false, message: 'Recipe not found' }); + } + next(error); + } +}); + export default router; \ No newline at end of file diff --git a/frontend/src/components/RecipeDetail.css b/frontend/src/components/RecipeDetail.css index e64b493..19dc57f 100644 --- a/frontend/src/components/RecipeDetail.css +++ b/frontend/src/components/RecipeDetail.css @@ -633,6 +633,42 @@ padding: 40px; } } +/* --- Cook Date Modal (appended) --- */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-dialog { + background: #fff; + padding: 24px 28px; + border-radius: 12px; + width: 100%; + max-width: 420px; + box-shadow: 0 10px 30px -5px rgba(0,0,0,0.35); + animation: fadeIn 0.18s ease-out; +} + +.modal-dialog h3 { margin: 0 0 12px; font-size: 1.25rem; } +.modal-field-group { margin-bottom: 16px; } +.modal-field-group label { display:block; font-size: 0.75rem; letter-spacing: .5px; font-weight:600; margin-bottom:4px; color:#555; text-transform: uppercase; } +.modal-field-group input { width: 100%; padding: 8px 10px; border: 1px solid #cbd5e0; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; } +.modal-field-inline { display: flex; gap: 10px; } +.modal-field-inline .modal-field-group { flex: 1; margin-bottom: 0; } + +.modal-actions { display:flex; justify-content:flex-end; gap:10px; margin-top: 18px; } +.modal-actions button { padding:8px 14px; border:none; border-radius:6px; cursor:pointer; font-weight:500; font-size: 0.9rem; } +.modal-actions .primary { background:#2f855a; color:#fff; } +.modal-actions .primary:hover { background:#276749; } +.modal-actions .secondary { background:#e2e8f0; } +.modal-actions .secondary:hover { background:#cbd5e0; } +.modal-actions .danger { background:#dc3545; color:#fff; } +.modal-actions .danger:hover { background:#c82333; } @media (max-width: 768px) { .recipe-detail { diff --git a/frontend/src/components/RecipeDetail.tsx b/frontend/src/components/RecipeDetail.tsx index 9dcc7bb..693f801 100644 --- a/frontend/src/components/RecipeDetail.tsx +++ b/frontend/src/components/RecipeDetail.tsx @@ -36,6 +36,9 @@ const RecipeDetail: React.FC = () => { const [error, setError] = useState(null); const [editingImages, setEditingImages] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); + const [markingCooked, setMarkingCooked] = useState(false); + const [showCookModal, setShowCookModal] = useState(false); + const [cookDate, setCookDate] = useState(''); // YYYY-MM-DD useEffect(() => { const loadRecipe = async () => { @@ -124,6 +127,43 @@ const RecipeDetail: React.FC = () => { } }; + const openCookModal = () => { + if (!recipe) return; + const now = new Date(); + const isoDate = now.toISOString().slice(0,10); // YYYY-MM-DD + setCookDate(isoDate); + setShowCookModal(true); + }; + + const submitCooked = async () => { + if (!recipe) return; + if (cookDate) { + const d = new Date(`${cookDate}T00:00:00`); + if (d.getTime() > Date.now() + 60_000) { + setError('Datum liegt in der Zukunft'); + return; + } + } + try { + setMarkingCooked(true); + let iso: string | undefined = undefined; + if (cookDate) { + const combined = new Date(`${cookDate}T00:00:00`); + if (!isNaN(combined.getTime())) iso = combined.toISOString(); + } + const resp = await recipeApi.markRecipeCooked(recipe.id, iso); + if (resp.success) { + setRecipe({ ...recipe, lastUsed: resp.data.lastUsed || undefined }); + setShowCookModal(false); + } + } catch (e) { + console.error('Mark cooked failed', e); + setError('Fehler beim Speichern des Datums'); + } finally { + setMarkingCooked(false); + } + }; + if (loading) { return (
@@ -192,6 +232,14 @@ const RecipeDetail: React.FC = () => { > 📸 {editingImages ? 'Fertig' : 'Bilder verwalten'} + ✏️ Bearbeiten @@ -200,6 +248,24 @@ const RecipeDetail: React.FC = () => {
+ {showCookModal && ( +
+
+

Datum der Zubereitung

+
+ + setCookDate(e.target.value)} /> +
+
+ {recipe.lastUsed && ( + + )} + + +
+
+
+ )} {/* Main Content */}
@@ -249,6 +315,14 @@ const RecipeDetail: React.FC = () => { Portionen: 👥 {recipe.servings}
+ {recipe.lastUsed && ( +
+ Zuletzt zubereitet: + + {new Date(recipe.lastUsed).toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' })} + +
+ )} diff --git a/frontend/src/components/RecipeList.css b/frontend/src/components/RecipeList.css index e73704c..e92161b 100644 --- a/frontend/src/components/RecipeList.css +++ b/frontend/src/components/RecipeList.css @@ -198,6 +198,29 @@ gap: 0.25rem; } +.recipe-lastused { + background: #2f855a; + color: #fff; + padding: 0.25rem 0.6rem; + border-radius: 16px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: .5px; +} + +.recipe-never { + background: #e53e3e; + color: #fff; + padding: 0.25rem 0.6rem; + border-radius: 16px; + font-size: 0.7rem; + font-weight: 600; + letter-spacing: .5px; + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + .recipe-actions { display: flex; gap: 0.75rem; diff --git a/frontend/src/components/RecipeList.tsx b/frontend/src/components/RecipeList.tsx index 5691e69..948c62a 100644 --- a/frontend/src/components/RecipeList.tsx +++ b/frontend/src/components/RecipeList.tsx @@ -29,6 +29,8 @@ const RecipeList: React.FC = () => { const [search, setSearch] = useState(''); const [category, setCategory] = useState(''); const [page, setPage] = useState(1); + const [sortBy, setSortBy] = useState<'title' | 'lastUsed'>('title'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); const [pagination, setPagination] = useState({ page: 1, limit: 10, @@ -50,8 +52,8 @@ const RecipeList: React.FC = () => { category: category || undefined, page, limit: 12, - sortBy: 'title', - sortOrder: 'asc', + sortBy, + sortOrder, }); if (response.success) { @@ -71,7 +73,7 @@ const RecipeList: React.FC = () => { useEffect(() => { loadRecipes(); - }, [debouncedSearch, category, page]); + }, [debouncedSearch, category, page, sortBy, sortOrder]); const handleSearchSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -140,6 +142,24 @@ const RecipeList: React.FC = () => { + + + @@ -188,6 +208,13 @@ const RecipeList: React.FC = () => { {recipe.category} )} 👥 {recipe.servings} Portionen + {recipe.lastUsed ? ( + + 🍳 {new Date(recipe.lastUsed).toLocaleDateString('de-DE')} + + ) : ( + 🆕 Nie + )}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 4ed3b27..6afe7f6 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -37,6 +37,7 @@ export interface Recipe { ingredients?: string; instructions?: string; comment?: string; + lastUsed?: string; // ISO Datum vom Backend images?: RecipeImage[]; ingredientsList?: Ingredient[]; } @@ -106,6 +107,12 @@ export const recipeApi = { const response = await api.delete(`/recipes/${id}`); return response.data; }, + + // Mark recipe as cooked (updates lastUsed) + markRecipeCooked: async (id: number, lastUsed?: string): Promise> => { + const response = await api.post(`/recipes/${id}/cooked`, lastUsed ? { lastUsed } : {}); + return response.data; + }, }; // Ingredient API methods