diff --git a/apps/api/src/routes/inventory.ts b/apps/api/src/routes/inventory.ts
index e8d90ec..05db897 100644
--- a/apps/api/src/routes/inventory.ts
+++ b/apps/api/src/routes/inventory.ts
@@ -1,5 +1,5 @@
import type { FastifyInstance } from "fastify";
-import { and, desc, eq, gt, ilike, isNull, or, sql } from "drizzle-orm";
+import { and, desc, eq, gt, ilike, inArray, isNull, or, sql } from "drizzle-orm";
import { schema } from "@app/database";
import {
createInventoryItemInputSchema,
@@ -603,6 +603,58 @@ export async function inventoryRoutes(app: FastifyInstance) {
.where(eq(schema.inventoryItems.id, id));
return { ok: true };
});
+
+ // Massradering: markera flera eller töm en hel vy. Mjuk radering som ovan.
+ app.post("/v1/inventory/bulk-delete", auth, async (req) => {
+ const body = req.body as { ids?: unknown };
+ const ids = Array.isArray(body?.ids)
+ ? body.ids.filter((x): x is string => typeof x === "string").slice(0, 1000)
+ : [];
+ if (ids.length === 0) throw errors.badRequest("ids saknas.");
+ const householdId = await requireActiveHousehold(app.db, req.userId);
+ const owned = await app.db
+ .select({
+ id: schema.inventoryItems.id,
+ quantity: schema.inventoryItems.quantity,
+ unit: schema.inventoryItems.unit,
+ })
+ .from(schema.inventoryItems)
+ .where(
+ and(
+ inArray(schema.inventoryItems.id, ids),
+ eq(schema.inventoryItems.householdId, householdId),
+ isNull(schema.inventoryItems.depletedAt),
+ ),
+ );
+ if (owned.length === 0) return { ok: true, deleted: 0 };
+ const corrections = owned
+ .filter((i) => i.quantity > 0)
+ .map((i) => ({
+ householdId,
+ inventoryItemId: i.id,
+ type: "correction" as const,
+ quantityDelta: -i.quantity,
+ unit: i.unit,
+ actorUserId: req.userId,
+ note: "Massradering av användare",
+ }));
+ if (corrections.length > 0) {
+ await app.db.insert(schema.inventoryTransactions).values(corrections);
+ }
+ await app.db
+ .update(schema.inventoryItems)
+ .set({ quantity: 0, depletedAt: new Date(), updatedAt: new Date() })
+ .where(
+ and(
+ inArray(
+ schema.inventoryItems.id,
+ owned.map((i) => i.id),
+ ),
+ eq(schema.inventoryItems.householdId, householdId),
+ ),
+ );
+ return { ok: true, deleted: owned.length };
+ });
}
async function getOwnedItem(app: FastifyInstance, itemId: string, userId: string) {
diff --git a/apps/mobile/src/app/(tabs)/home.tsx b/apps/mobile/src/app/(tabs)/home.tsx
index 2f2321d..8fdd20a 100644
--- a/apps/mobile/src/app/(tabs)/home.tsx
+++ b/apps/mobile/src/app/(tabs)/home.tsx
@@ -188,7 +188,9 @@ export default function HomeScreen() {
))}
{locationItems.length > 12 && (
- {t("home.moreItems", { count: locationItems.length - 12 })}
+ router.push("/kitchen")}>
+ {t("home.moreItems", { count: locationItems.length - 12 })} ›
+
)}
))}
diff --git a/apps/mobile/src/app/kitchen.tsx b/apps/mobile/src/app/kitchen.tsx
index a7ed93f..688be88 100644
--- a/apps/mobile/src/app/kitchen.tsx
+++ b/apps/mobile/src/app/kitchen.tsx
@@ -1,87 +1,177 @@
-import { Alert } from "react-native";
+import { useState } from "react";
+import { Alert, TextInput } from "react-native";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { t } from "@/lib/i18n";
import { formatQuantity } from "@/lib/units";
import {
- Body,
- Button,
- Card,
- EmptyState,
- ErrorView,
- LoadingView,
- Row,
- Screen,
- Small,
+ Body,
+ Button,
+ Card,
+ EmptyState,
+ ErrorView,
+ LoadingView,
+ Row,
+ Screen,
+ Small,
} from "@/components/ui";
+import { colors, radius, spacing } from "@/lib/theme";
-/** Ditt kök: se och ta bort varor i lagret. Radering = mjuk (kvantitet → 0, historik bevaras). */
+/** Ditt kök: sök, sortera, markera flera och ta bort. Radering = mjuk (kvantitet → 0, historik bevaras). */
interface InventoryItem {
- id: string;
- displayName: string;
- quantity: number;
- unit: string;
- expiry: { status: string; daysLeft: number | null; pastBestBefore: boolean };
+ id: string;
+ displayName: string;
+ quantity: number;
+ unit: string;
+ locationName: string;
+ locationType: string;
+ expiry: { status: string; daysLeft: number | null; pastBestBefore: boolean };
}
+const LOCATION_LABELS: Record = {
+ fridge: "Kyl",
+ freezer: "Frys",
+ pantry: "Skafferi",
+ garage_freezer: "Garagefrys",
+ wine_fridge: "Vinkyl",
+};
+const locLabel = (type: string) => LOCATION_LABELS[type] ?? type;
+
export default function KitchenScreen() {
- const queryClient = useQueryClient();
+ const queryClient = useQueryClient();
+ const [search, setSearch] = useState("");
+ const [locFilter, setLocFilter] = useState(null);
+ const [selected, setSelected] = useState>(new Set());
- const query = useQuery({
- queryKey: ["inventory", "all"],
- queryFn: () => api<{ items: InventoryItem[] }>("/v1/inventory?limit=200"),
- });
+ const query = useQuery({
+ queryKey: ["inventory", "all"],
+ queryFn: () => api<{ items: InventoryItem[] }>("/v1/inventory?limit=500"),
+ });
- const remove = useMutation({
- mutationFn: (id: string) => api(`/v1/inventory/items/${id}`, { method: "DELETE" }),
- onMutate: async (id: string) => {
- await queryClient.cancelQueries({ queryKey: ["inventory", "all"] });
- const previous = queryClient.getQueryData<{ items: InventoryItem[] }>(["inventory", "all"]);
- queryClient.setQueryData<{ items: InventoryItem[] }>(["inventory", "all"], (old) =>
- old ? { ...old, items: old.items.filter((i) => i.id !== id) } : old,
- );
- return { previous };
- },
- onError: (err, _id, context) => {
- if (context?.previous) queryClient.setQueryData(["inventory", "all"], context.previous);
- Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error"));
- },
- onSettled: () => {
- void queryClient.invalidateQueries({ queryKey: ["inventory"] });
- void queryClient.invalidateQueries({ queryKey: ["inventory-expiring"] });
- },
- });
+ const bulkRemove = useMutation({
+ mutationFn: (ids: string[]) =>
+ api<{ deleted: number }>("/v1/inventory/bulk-delete", { method: "POST", body: { ids } }),
+ onSuccess: () => setSelected(new Set()),
+ onError: (err) =>
+ Alert.alert(t("common.oops"), err instanceof Error ? err.message : t("common.error")),
+ onSettled: () => {
+ void queryClient.invalidateQueries({ queryKey: ["inventory"] });
+ void queryClient.invalidateQueries({ queryKey: ["inventory-expiring"] });
+ },
+ });
- if (query.isLoading) return ;
- if (query.isError) return void query.refetch()} />;
- const items = query.data?.items ?? [];
+ if (query.isLoading) return ;
+ if (query.isError) return void query.refetch()} />;
+ const all = query.data?.items ?? [];
- return (
-
- {items.length === 0 && (
-
- )}
- {items.map((item) => (
-
-
-
- {item.displayName} · {formatQuantity(item.quantity, item.unit)}
-
-
- {item.expiry.pastBestBefore && ⚠︎ bäst före har passerat}
-
- ))}
-
- );
+ const locTypes = Array.from(new Set(all.map((i) => i.locationType)));
+ const q = search.trim().toLowerCase();
+ const visible = all
+ .filter((i) => (locFilter ? i.locationType === locFilter : true))
+ .filter((i) => (q ? i.displayName.toLowerCase().includes(q) : true))
+ .sort(
+ (a, b) =>
+ a.locationName.localeCompare(b.locationName, "sv") ||
+ a.displayName.localeCompare(b.displayName, "sv"),
+ );
+
+ const toggle = (id: string) =>
+ setSelected((prev) => {
+ const next = new Set(prev);
+ if (next.has(id)) next.delete(id);
+ else next.add(id);
+ return next;
+ });
+
+ const confirmDelete = (ids: string[], label: string) => {
+ if (ids.length === 0) return;
+ Alert.alert("Ta bort", `Är du säker på att du vill ta bort ${ids.length} st ur ${label}?`, [
+ { text: t("common.cancel"), style: "cancel" },
+ { text: "Ta bort", style: "destructive", onPress: () => bulkRemove.mutate(ids) },
+ ]);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {visible.length} varor{selected.size > 0 ? ` · ${selected.size} valda` : ""}
+
+
+ {selected.size > 0 && (
+ confirmDelete([...selected], "markeringen")}
+ />
+ )}
+
+ confirmDelete(
+ visible.map((i) => i.id),
+ locFilter ? locLabel(locFilter) : "hela lagret",
+ )
+ }
+ />
+
+
+
+ {visible.length === 0 && (
+
+ )}
+ {visible.map((item) => {
+ const isSel = selected.has(item.id);
+ return (
+ toggle(item.id)}>
+
+
+ {isSel ? "☑ " : "☐ "}
+ {item.displayName} · {formatQuantity(item.quantity, item.unit)}
+
+ {locLabel(item.locationType)}
+
+ {item.expiry.pastBestBefore && ⚠︎ bäst före har passerat}
+
+ );
+ })}
+
+ );
}