Add recipe deletion functionality and enhance inventory consumption details

This commit is contained in:
Nils-Johan Gynther
2026-04-10 18:44:06 +02:00
parent a743f832a2
commit dd17656e4c
7 changed files with 174 additions and 49 deletions
@@ -94,6 +94,16 @@ export class InventoryService {
where: { where: {
inventoryItemId: id, inventoryItemId: id,
}, },
select: {
id: true,
inventoryItemId: true,
amountUsed: true,
comment: true,
createdAt: true,
inventoryItem: {
select: { unit: true },
},
},
orderBy: { orderBy: {
createdAt: 'desc', createdAt: 'desc',
}, },
+7 -1
View File
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, ParseIntPipe, Post, Patch } from '@nestjs/common'; import { Body, Controller, Delete, Get, HttpCode, Param, ParseIntPipe, Post, Patch } from '@nestjs/common';
import { RecipesService } from './recipes.service'; import { RecipesService } from './recipes.service';
import { CreateRecipeDto } from './dto/create-recipe.dto'; import { CreateRecipeDto } from './dto/create-recipe.dto';
@@ -33,4 +33,10 @@ export class RecipesController {
) { ) {
return this.recipesService.update(id, createRecipeDto); return this.recipesService.update(id, createRecipeDto);
} }
@Delete(':id')
@HttpCode(204)
async remove(@Param('id', ParseIntPipe) id: number) {
return this.recipesService.remove(id);
}
} }
+13
View File
@@ -345,6 +345,19 @@ export class RecipesService {
return recipe; return recipe;
} }
async remove(id: number) {
const existingRecipe = await this.prisma.recipe.findUnique({
where: { id },
});
if (!existingRecipe) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
await this.prisma.recipeIngredient.deleteMany({ where: { recipeId: id } });
await this.prisma.recipe.delete({ where: { id } });
}
async create(createRecipeDto: CreateRecipeDto) { async create(createRecipeDto: CreateRecipeDto) {
const recipe = await this.prisma.recipe.create({ const recipe = await this.prisma.recipe.create({
data: { data: {
@@ -104,7 +104,7 @@ export default function InventoryConsumptionHistory({ id }: Props) {
}} }}
> >
<div> <div>
<strong>Använt:</strong> {entry.amountUsed} <strong>Använt:</strong> {entry.amountUsed}{entry.inventoryItem?.unit ? ` ${entry.inventoryItem.unit}` : ''}
</div> </div>
<div> <div>
<strong>Tid:</strong> {formatDateTime(entry.createdAt)} <strong>Tid:</strong> {formatDateTime(entry.createdAt)}
+105 -37
View File
@@ -58,6 +58,32 @@ export default function RecipePreview({ recipes }: Props) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const selectAndLoad = (id: string) => {
setSelectedRecipeId(id);
setError(null);
setPreview(null);
startTransition(async () => {
try {
const res = await fetch(`/api/recipe-preview-proxy?id=${id}`, {
method: 'GET',
cache: 'no-store',
});
if (!res.ok) {
const errorMessage = await parseErrorResponse(res);
throw new Error(errorMessage);
}
const data: RecipeInventoryPreview = await res.json();
setPreview(data);
} catch (err) {
const message = err instanceof Error ? err.message : 'Ett okänt fel inträffade.';
setError(message);
}
});
};
const loadPreview = () => { const loadPreview = () => {
setError(null); setError(null);
setPreview(null); setPreview(null);
@@ -88,46 +114,49 @@ export default function RecipePreview({ recipes }: Props) {
}); });
}; };
const listedRecipes = recipes.slice(0, 10);
return ( return (
<section style={{ display: 'grid', gap: '1rem' }}> <section style={{ display: 'grid', gap: '1rem' }}>
<div <div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '1rem', alignItems: 'start' }}>
style={{ <div
border: '1px solid #ddd', style={{
borderRadius: '8px', border: '1px solid #ddd',
padding: '1rem', borderRadius: '8px',
display: 'grid', padding: '1rem',
gap: '0.75rem', display: 'grid',
}} gap: '0.75rem',
> }}
<h2 style={{ margin: 0 }}>Recept mot hemmavaror</h2> >
<h2 style={{ margin: 0 }}>Recept mot hemmavaror</h2>
<label> <label>
Recept Recept
<br /> <br />
<select <select
value={selectedRecipeId} value={selectedRecipeId}
onChange={(e) => setSelectedRecipeId(e.target.value)} onChange={(e) => setSelectedRecipeId(e.target.value)}
style={{ width: '100%', padding: '0.5rem' }} style={{ width: '100%', padding: '0.5rem' }}
> >
<option value="">Välj recept</option> <option value="">Välj recept</option>
{recipes.map((recipe) => ( {recipes.map((recipe) => (
<option key={recipe.id} value={recipe.id}> <option key={recipe.id} value={recipe.id}>
{recipe.name} {recipe.name}
</option> </option>
))} ))}
</select> </select>
</label> </label>
<div style={{ display: 'flex', gap: '0.75rem' }}> <div style={{ display: 'flex', gap: '0.75rem' }}>
<button <button
type="button" type="button"
onClick={loadPreview} onClick={loadPreview}
disabled={isPending} disabled={isPending}
style={{ padding: '0.6rem 1rem' }} style={{ padding: '0.6rem 1rem' }}
> >
{isPending ? 'Hämtar preview...' : 'Visa preview'} {isPending ? 'Hämtar preview...' : 'Visa preview'}
</button> </button>
{selectedRecipeId && ( {selectedRecipeId && (
<Link <Link
href={`/recipes/${selectedRecipeId}/edit`} href={`/recipes/${selectedRecipeId}/edit`}
style={{ style={{
@@ -144,9 +173,48 @@ export default function RecipePreview({ recipes }: Props) {
Redigera recept Redigera recept
</Link> </Link>
)} )}
</div>
{error ? <p style={{ color: 'crimson', margin: 0 }}>{error}</p> : null}
</div> </div>
{error ? <p style={{ color: 'crimson', margin: 0 }}>{error}</p> : null} {/* Receptlista till höger */}
<div
style={{
border: '1px solid #ddd',
borderRadius: '8px',
padding: '1rem',
minWidth: '180px',
maxWidth: '220px',
}}
>
<h3 style={{ margin: '0 0 0.75rem 0', fontSize: '1rem' }}>Mina recept</h3>
<ul style={{ margin: 0, padding: 0, listStyle: 'none', display: 'grid', gap: '0.4rem' }}>
{listedRecipes.map((recipe) => (
<li key={recipe.id}>
<button
type="button"
disabled={isPending}
onClick={() => selectAndLoad(String(recipe.id))}
style={{
width: '100%',
textAlign: 'left',
padding: '0.4rem 0.6rem',
background: String(recipe.id) === selectedRecipeId ? '#e8f0fe' : 'transparent',
border: `1px solid ${String(recipe.id) === selectedRecipeId ? '#4285f4' : '#eee'}`,
borderRadius: '4px',
cursor: 'pointer',
fontWeight: String(recipe.id) === selectedRecipeId ? 600 : 400,
color: String(recipe.id) === selectedRecipeId ? '#1a56db' : '#333',
fontSize: '0.9rem',
}}
>
{recipe.name}
</button>
</li>
))}
</ul>
</div>
</div> </div>
{preview ? ( {preview ? (
+37 -10
View File
@@ -20,6 +20,7 @@ export default function EditRecipePage() {
const [products, setProducts] = useState<Product[]>([]); const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
@@ -239,19 +240,45 @@ export default function EditRecipePage() {
</div> </div>
)} )}
<div style={{ display: 'flex', gap: '1rem' }}> <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', justifyContent: 'space-between' }}>
<button type="button" onClick={addIngredient} style={{ padding: '0.5rem' }}> <div style={{ display: 'flex', gap: '1rem' }}>
Lägg till ingrediens <button type="button" onClick={addIngredient} style={{ padding: '0.5rem' }}>
</button> Lägg till ingrediens
<button type="submit" disabled={isSaving} style={{ padding: '0.5rem' }}> </button>
{isSaving ? 'Uppdaterar...' : 'Uppdatera recept'} <button type="submit" disabled={isSaving || isDeleting} style={{ padding: '0.5rem' }}>
</button> {isSaving ? 'Uppdaterar...' : 'Uppdatera recept'}
</button>
<button
type="button"
onClick={() => router.push('/recipes')}
style={{ padding: '0.5rem', background: '#f0f0f0', color: '#333', border: '1px solid #ccc', borderRadius: '4px', cursor: 'pointer' }}
>
Avbryt
</button>
</div>
<button <button
type="button" type="button"
onClick={() => router.push('/recipes')} disabled={isSaving || isDeleting}
style={{ padding: '0.5rem', background: '#f0f0f0', color: '#333', border: '1px solid #ccc', borderRadius: '4px', cursor: 'pointer' }} onClick={async () => {
if (!confirm('Är du säker på att du vill radera detta recept? Detta kan inte ångras.')) return;
setIsDeleting(true);
setError(null);
try {
const res = await fetch(`/api/recipes/${recipeId}`, { method: 'DELETE' });
if (!res.ok) {
const errorMessage = await parseErrorResponse(res);
throw new Error(errorMessage);
}
router.push('/recipes');
} catch (err) {
setError(err instanceof Error ? err.message : 'Kunde inte radera receptet.');
} finally {
setIsDeleting(false);
}
}}
style={{ padding: '0.5rem 1rem', background: '#c0392b', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
> >
Avbryt {isDeleting ? 'Raderar...' : 'Radera recept'}
</button> </button>
</div> </div>
</form> </form>
+1
View File
@@ -53,6 +53,7 @@ export type InventoryConsumption = {
amountUsed: string; amountUsed: string;
comment: string | null; comment: string | null;
createdAt: string; createdAt: string;
inventoryItem?: { unit: string };
}; };
export type RecipeIngredient = { export type RecipeIngredient = {
id: number; id: number;