feat(households): hane seçici, ayarlar, admin hane yönetimi ve hane-yok sayfası

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 02:35:01 +03:00
parent de91f2505a
commit 3644e081b1
21 changed files with 1427 additions and 29 deletions
+7 -2
View File
@@ -11,18 +11,23 @@ import { UserMenu } from "./user-menu";
export function AppHeader({
user,
households,
activeHouseholdId,
canWrite,
version,
}: {
user: SessionUser;
households: HouseholdSummary[];
activeHouseholdId: string | undefined;
/** Aktif hanede en az "member" mi? (İzleyici veri giremez.) */
canWrite: boolean;
version: string;
}) {
return (
<header className="sticky top-0 z-30 flex h-14 items-center gap-2 border-b bg-background/95 px-3 backdrop-blur supports-backdrop-filter:bg-background/80 md:px-4">
<SidebarTrigger className="hidden md:inline-flex" aria-label={tr.nav.toggleSidebar} />
<HouseholdSwitcher households={households} />
<HouseholdSwitcher households={households} activeId={activeHouseholdId} />
<div className="ml-auto flex items-center gap-1">
<QuickAddButton className="hidden md:inline-flex" />
{canWrite ? <QuickAddButton className="hidden md:inline-flex" /> : null}
<ThemeToggle />
<UserMenu user={user} version={version} />
</div>
+4 -2
View File
@@ -40,7 +40,7 @@ function BottomNavLink({ item, pathname }: { item: NavItem; pathname: string })
}
/** Mobil alt gezinme çubuğu (md altı). */
export function BottomNav() {
export function BottomNav({ canWrite }: { canWrite: boolean }) {
const pathname = usePathname();
const { openQuickAdd } = useQuickAdd();
const { setOpenMobile } = useSidebar();
@@ -57,8 +57,10 @@ export function BottomNav() {
<button
type="button"
onClick={openQuickAdd}
disabled={!canWrite}
title={canWrite ? undefined : tr.households.viewerReadOnly}
aria-label={tr.quickAdd.button}
className="-mt-5 flex size-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg outline-none focus-visible:ring-4 focus-visible:ring-ring/50"
className="-mt-5 flex size-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg outline-none focus-visible:ring-4 focus-visible:ring-ring/50 disabled:opacity-40"
>
<Plus className="size-6" />
</button>
+29 -7
View File
@@ -1,7 +1,8 @@
"use client";
import { House } from "lucide-react";
import { useState } from "react";
import { useTransition } from "react";
import { toast } from "sonner";
import {
Select,
SelectContent,
@@ -9,17 +10,38 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { switchHouseholdAction } from "@/features/households/actions";
import type { HouseholdSummary } from "@/features/households/types";
import { tr } from "@/i18n/tr";
/** Üst bar aktif hane seçici. FAZ 5: seçim sunucuda (cookie) saklanacak. */
export function HouseholdSwitcher({ households }: { households: HouseholdSummary[] }) {
const [value, setValue] = useState(households[0]?.id);
const current = households.find((h) => h.id === value);
/** Üst bar aktif hane seçici. Seçim sunucuda doğrulanır ve cookie'ye yazılır. */
export function HouseholdSwitcher({
households,
activeId,
}: {
households: HouseholdSummary[];
activeId: string | undefined;
}) {
const [pending, startTransition] = useTransition();
const current = households.find((h) => h.id === activeId);
if (households.length === 0) return null;
function onChange(id: string) {
if (id === activeId) return;
startTransition(async () => {
const result = await switchHouseholdAction(id);
if (result.ok) toast.success(tr.household.switched(result.data.name));
else toast.error(result.error);
});
}
return (
<Select value={value} onValueChange={setValue}>
<SelectTrigger aria-label={tr.household.label} className="w-[min(46vw,13rem)]">
<Select value={activeId} onValueChange={onChange} disabled={pending || households.length < 2}>
<SelectTrigger
aria-label={tr.household.label}
className="w-[min(46vw,13rem)]"
aria-busy={pending}
>
<House className="text-muted-foreground" />
{/* Etiket children ile verilir: Radix aksi halde hydration'a kadar boş gösterir. */}
<SelectValue placeholder={tr.household.select}>{current?.name}</SelectValue>
+1 -1
View File
@@ -25,7 +25,7 @@ export type NavItem = {
};
/** Şu an tamamlanmış en yüksek faz — sayfalar eklendikçe artar. */
export const CURRENT_PHASE = 2;
export const CURRENT_PHASE = 5;
export const isAvailable = (item: NavItem) => item.phase <= CURRENT_PHASE;