Add recipe deletion functionality and enhance inventory consumption details
This commit is contained in:
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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,8 +114,11 @@ 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 style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: '1rem', alignItems: 'start' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
@@ -149,6 +178,45 @@ export default function RecipePreview({ recipes }: Props) {
|
|||||||
{error ? <p style={{ color: 'crimson', margin: 0 }}>{error}</p> : null}
|
{error ? <p style={{ color: 'crimson', margin: 0 }}>{error}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
{preview ? (
|
{preview ? (
|
||||||
<section style={{ display: 'grid', gap: '1rem' }}>
|
<section style={{ display: 'grid', gap: '1rem' }}>
|
||||||
<article
|
<article
|
||||||
|
|||||||
@@ -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,11 +240,12 @@ export default function EditRecipePage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
<button type="button" onClick={addIngredient} style={{ padding: '0.5rem' }}>
|
<button type="button" onClick={addIngredient} style={{ padding: '0.5rem' }}>
|
||||||
Lägg till ingrediens
|
Lägg till ingrediens
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" disabled={isSaving} style={{ padding: '0.5rem' }}>
|
<button type="submit" disabled={isSaving || isDeleting} style={{ padding: '0.5rem' }}>
|
||||||
{isSaving ? 'Uppdaterar...' : 'Uppdatera recept'}
|
{isSaving ? 'Uppdaterar...' : 'Uppdatera recept'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -254,6 +256,31 @@ export default function EditRecipePage() {
|
|||||||
Avbryt
|
Avbryt
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isSaving || isDeleting}
|
||||||
|
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' }}
|
||||||
|
>
|
||||||
|
{isDeleting ? 'Raderar...' : 'Radera recept'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user