feat(shopping): alışverişi tamamla transaction'ı ve karşılaştırma motoru

- tek transaction: stok alımı, fiş kaynaklı fiyat, mağaza başına bölünmüş gider, fiş ekleri, kalanların taşınması
- muadil, markasız ürün tipi ve plansız ekstra satırları; stok → gider kategorisi eşlemesi ve yedek kategori
- compareShopping saf fonksiyonu: aynı/muadil/alınmadı/ekstra, farkın nedenlere tam ayrışması
- tamamlanan listede rapor görüntüsü; testler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 23:57:50 +03:00
parent 3dd9834a3d
commit bd5a20ce64
12 changed files with 1291 additions and 4 deletions
+24 -4
View File
@@ -29,8 +29,10 @@ import {
suggestionHidden,
type ShoppingItemKind,
type ShoppingItemSource,
type ShoppingCompletion,
type ShoppingListStatus,
} from "@/shared/shopping";
import { compareShopping, type ComparisonSummary } from "@/shared/shopping-compare";
import { planShopping, type PlanResult } from "@/shared/shopping-plan";
import { compareTr } from "@/shared/search";
import { formatStock, type PurchaseSuggestion } from "@/shared/stock";
@@ -44,7 +46,7 @@ import type { StoreKind } from "@/shared/stores";
const t = tr.shopping;
type ItemLean = {
export type ItemLean = {
_id: Types.ObjectId;
kind: ShoppingItemKind;
productTypeId: Types.ObjectId | null;
@@ -59,13 +61,14 @@ type ItemLean = {
addedAt: Date;
};
type ListLean = {
export type ListLean = {
_id: Types.ObjectId;
name: string;
status: ShoppingListStatus;
note: string;
items: ItemLean[];
completedAt: Date | null;
completion: ShoppingCompletion | null;
createdAt: Date;
updatedAt: Date;
};
@@ -154,6 +157,8 @@ export type ShoppingListDetail = {
/** Kalem taşımak için diğer aktif listeler. */
otherLists: { id: string; name: string }[];
canWrite: boolean;
/** Tamamlanmış listede karşılaştırma raporu. */
report: { completion: ShoppingCompletion; summary: ComparisonSummary } | null;
};
// --- Yardımcılar ---
@@ -401,7 +406,7 @@ function itemView(
};
}
async function buildPlan(
export async function buildPlan(
userId: string,
m: Membership,
items: ItemLean[],
@@ -576,12 +581,27 @@ export async function getShoppingList(
},
otherLists: others.map((o) => ({ id: String(o._id), name: o.name })),
canWrite,
report: list.completion
? {
completion: list.completion,
summary: compareShopping(
list.completion.rows.map((r) => ({
match: r.match,
plan: r.plan,
actual:
r.amountMinor === null
? null
: { productId: r.productId, packages: r.packages, amountMinor: r.amountMinor },
})),
),
}
: null,
};
}
// --- Yazma: listeler ---
async function writableList(actorId: string, householdId: string, listId: string) {
export async function writableList(actorId: string, householdId: string, listId: string) {
const m = await resolveMembership(actorId, householdId, "member");
if (!m) return { error: fail(tr.common.unauthorized) } as const;
const list = await loadList(m, listId);