+
+
+
+ {t.title}: {sheet.list.name}
+
+
{t.description}
+
+ {sheet.stores.length === 0 ? (
+
{t.noStores}
+ ) : null}
+
+ {t.date}
+ setDate(e.target.value)}
+ />
+ {errors.date ? {errors.date} : null}
+
+
+
+
+ {t.itemsTitle}
+
+
+ {sheet.items.map((item) => {
+ const row = rows[item.id]!;
+ const plan = sheet.planned[item.id];
+ const planProduct = plan ? sheet.products.find((p) => p.id === plan.productId) : null;
+ const planStore = plan ? sheet.stores.find((s) => s.id === plan.storeId) : null;
+ const substitute =
+ row.bought &&
+ item.kind !== "text" &&
+ (item.kind === "product"
+ ? row.productId !== item.productId
+ : plan && row.productId !== plan.productId);
+ const key = `item:${item.id}`;
+ return (
+ -
+
+ setRow(item.id, { bought: x === true })}
+ className="mt-0.5 size-5"
+ />
+
+ {substitute ? {t.substitute} : null}
+
+ {row.bought ? (
+ setRow(item.id, patch)}
+ errors={errs(key)}
+ idPrefix={`l-${item.id}`}
+ />
+ ) : null}
+
+ );
+ })}
+
+
+
+
+
+ {extras.length ? (
+
+ {extras.map((e) => (
+ -
+
+
+ setExtras((s) =>
+ s.map((x) =>
+ x.key === e.key ? { ...x, ...withPrice(sheet, x, patch) } : x,
+ ),
+ )
+ }
+ errors={errs(`extra:${e.key}`)}
+ idPrefix={`e-${e.key}`}
+ />
+
+
+
+ ))}
+
+ ) : null}
+
+
+
+ {storeIds.length ? (
+
+
+
+ {t.storesTitle}
+
+
{t.storesDescription}
+
+
+
+ ) : null}
+
+ {needsFallback ? (
+
+ {t.fallbackCategory}
+
+ {t.fallbackHint}
+ {errors.fallbackCategoryId ? {errors.fallbackCategoryId} : null}
+
+ ) : null}
+
+ {unbought.length ? (
+
+ ) : null}
+
+
+
+
+
+ {t.total}: {formatMoney(total)}
+
+ {plannedTotal ? (
+
+ {" · "}
+ {t.plannedTotal}: {formatMoney(plannedTotal)}
+
+ ) : null}
+ {errors.root ? (
+ {errors.root}
+ ) : null}
+
+
+
+
+
+ );
+}
diff --git a/src/features/shopping/components/list-detail.tsx b/src/features/shopping/components/list-detail.tsx
index f3c8bdb..f6bbcb7 100644
--- a/src/features/shopping/components/list-detail.tsx
+++ b/src/features/shopping/components/list-detail.tsx
@@ -4,6 +4,7 @@ import {
ArrowLeft,
ArrowRightLeft,
Check,
+ ClipboardCheck,
EllipsisVertical,
Package,
Pencil,
@@ -62,6 +63,7 @@ import type { ShoppingItemView, ShoppingListDetail, SuggestionView } from "../se
import { ShoppingItemDialog } from "./item-dialog";
import { ShoppingListDialog } from "./list-dialog";
import { PlanPanel } from "./plan-panel";
+import { ShoppingReport } from "./report-view";
const t = tr.shopping;
@@ -411,34 +413,50 @@ export function ShoppingListDetailView({ data }: { data: ShoppingListDetail }) {
) : null}
+ {items.length && canWrite ? (
+
-
- {data.suggestions.length ? (
-
- ) : null}
- {items.length ? (
-
- {items.map((i) => (
- setEditing(i)}
- onMove={() => setMoving(i)}
- onRemove={() => remove(i)}
- />
- ))}
-
- ) : (
-
- {t.emptyList}
-
- )}
+ {data.report ? (
+
+ ) : (
+
+
+ {data.suggestions.length ? (
+
+ ) : null}
+ {items.length ? (
+
+ {items.map((i) => (
+ setEditing(i)}
+ onMove={() => setMoving(i)}
+ onRemove={() => remove(i)}
+ />
+ ))}
+
+ ) : (
+
+ {t.emptyList}
+
+ )}
+
+
-
-
+ )}
{adding || editing ? (
= {
+ same: "border-income/40 text-income",
+ substitute: "border-amber-500/50 text-amber-700 dark:text-amber-400",
+ skipped: "text-muted-foreground",
+ extra: "border-budget/40 text-budget",
+};
+
+/** İşaretli tutar: "+₺12,00" / "−₺5,00". Renk tek başına anlam taşımaz. */
+function Signed({ value, className }: { value: number; className?: string }) {
+ return (
+ 0 ? "text-expense" : value < 0 ? "text-income" : "text-muted-foreground",
+ className,
+ )}
+ >
+ {value > 0 ? "+" : value < 0 ? "−" : ""}
+ {formatMoney(Math.abs(value))}
+
+ );
+}
+
+function Cell({ label, sub, amount }: { label: string; sub: string; amount: number | null }) {
+ return (
+
+ {label || "—"}
+ {sub ? {sub} : null}
+ {amount !== null ? {formatMoney(amount)} : null}
+
+ );
+}
+
+const pkgs = (packages: number) => `${formatQuantity(packages)} ${tr.shopping.packageUnit}`;
+
+function Row({ row }: { row: CompletionRow }) {
+ return (
+
+ {row.label}
+
+
+ {t.matches[row.match]}
+
+
+
+ {t.plan}:
+ {row.plan ? (
+ |
+ ) : (
+ {t.noPlanCell}
+ )}
+
+
+ {t.bought}:
+ {row.amountMinor !== null ? (
+ |
+ ) : (
+ {t.noPlanCell}
+ )}
+
+
+ );
+}
+
+/** Tamamlanan liste: planlanan vs. gerçek, farkın nedenleri, kalem bazında eşleşme, giderler. */
+export function ShoppingReport({
+ completion,
+ summary,
+}: {
+ completion: ShoppingCompletion;
+ summary: ComparisonSummary;
+}) {
+ const reasons = (Object.entries(summary.reasons) as [keyof ComparisonReasons, number][]).filter(
+ ([, v]) => v !== 0,
+ );
+ return (
+
+
+
+ {t.title}
+
+
+ {t.description(formatDate(`${completion.date}T12:00:00+03:00`))}
+
+
+
+ {[
+ { label: t.planned, node: formatMoney(summary.plannedMinor) },
+ { label: t.actual, node: formatMoney(summary.actualMinor) },
+ { label: t.diff, node: },
+ ].map((x) => (
+
+
- {x.label}
+ - {x.node}
+
+ ))}
+
+ {reasons.length ? (
+
+
{t.reasonsTitle}
+
+ {reasons.map(([k, v]) => (
+ -
+ {t.reasons[k]}
+
+
+ ))}
+
+
+ ) : null}
+
+
+
+ {t.item}
+
+ {t.plan}
+ {t.bought}
+
+
+ {completion.rows.map((r, i) => (
+
+ ))}
+
+
+
+
+
{t.expenses}
+
+ {completion.stores.map((s) => (
+ -
+
+
+ {s.label}
+
+ {t.expenseLine(s.accountName, s.attachmentCount)}
+
+
+
+ {formatMoney(s.amountMinor)}
+
+
+ ))}
+
+
+ {completion.movedTo ? (
+
+ {t.movedTo(completion.movedTo.name)}
+
+
+ ) : null}
+
+ );
+}
diff --git a/src/features/shopping/components/shopping-mode.tsx b/src/features/shopping/components/shopping-mode.tsx
index 7ec8827..a3da0d9 100644
--- a/src/features/shopping/components/shopping-mode.tsx
+++ b/src/features/shopping/components/shopping-mode.tsx
@@ -1,6 +1,6 @@
"use client";
-import { ArrowLeft, Check, Globe } from "lucide-react";
+import { ArrowLeft, Check, ClipboardCheck, Globe } from "lucide-react";
import Link from "next/link";
import { startTransition, useOptimistic } from "react";
import { toast } from "sonner";
@@ -244,6 +244,14 @@ export function ShoppingModeView({
);
})}
+ {canWrite && done > 0 ? (
+
+ ) : null}
);
}