Files
isletmem-kovakcrm/src/components/finance/scope-toggle.tsx
T
kovakmedya 1f79abe404 feat(finance): personal vs company scope for banking + finance entries
User-level data privacy on finance entities. Bireysel = sadece sahibi
görür/düzenler/siler, Şirket = takım görür (mevcut davranış).

Schema additions (4 tables, all enum company|personal default 'company'):
- bank_accounts.scope
- bank_loans.scope
- credit_cards.scope
- finance_entries.scope
+ tenantId_scope index on each.

Inherited fields (no own scope, parent's used):
- loan_installments → from bank_loan
- credit_card_statements → from credit_card

Permissions (lib/appwrite/scope-permissions.ts):
- scopedRowPermissions(tenantId, createdBy, scope):
  * company: Permission.read/update Role.team(tenantId), delete Role.team
    owner|admin (current behavior)
  * personal: read/update/delete Role.user(createdBy) only
- canAccessRow(row, userId): true if scope=company OR createdBy=userId.
  Used as a defense-in-depth check inside actions because we use the
  admin SDK (which bypasses row-level perms).

Action updates:
- bank-account-actions, loan-actions, credit-card-actions, finance-actions:
  pickFormFields includes scope; create uses scopedRowPermissions; update
  re-applies perms when scope changes; update/delete check canAccessRow
  on top of the existing tenantId check.
- loan installment payment & credit card statement payment auto-create
  finance entries that inherit the parent's scope, so a personal loan
  installment doesn't create a company income/expense.

Query updates (all accept optional currentUserId):
- listBankAccounts, listLoans, listCreditCards, listFinanceEntries:
  pull all tenant rows then in-JS filter via canAccessRow.
- getBankAccountBalances respects visible accounts only.
- listAllInstallments / listStatements: filter to only those whose
  parent loan/card is visible.

UI:
- New shared component components/finance/scope-toggle.tsx with
  ScopeToggle (form input) and ScopeBadge (visual marker).
- Bank, loan, card form sheets and the finance form sheet now include
  a Şirket/Bireysel toggle at the top.
- Bank account cards display ScopeBadge for personal entries.
- Page-level queries everywhere now pass ctx.user.id so each user only
  sees their personal rows + the team's company rows.

Reports & Dashboard:
- getDashboardData filters finance entries to scope=company only — so
  team-level metrics never include any user's personal data.
- getFinancialReport (CFO view): bank accounts, loans, cards, finance
  entries, installments and statements all filtered to company scope.
  Personal entities never appear in reports anywhere.

Invoice → finance entry sync explicitly tags scope=company since invoices
are inherently company-scope.
2026-04-30 08:36:01 +03:00

81 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { Building2, User } from "lucide-react";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
type Scope = "company" | "personal";
export function ScopeToggle({
name = "scope",
defaultValue = "company",
label = "Kapsam",
description,
}: {
name?: string;
defaultValue?: Scope;
label?: string;
description?: string;
}) {
const [value, setValue] = useState<Scope>(defaultValue);
return (
<div className="grid gap-2">
<Label>{label}</Label>
<input type="hidden" name={name} value={value} />
<div className="grid grid-cols-2 gap-2">
<button
type="button"
onClick={() => setValue("company")}
className={cn(
"border-input flex flex-col items-start gap-1 rounded-md border p-3 text-left transition-colors",
value === "company"
? "border-primary bg-primary/5"
: "hover:bg-muted/50",
)}
>
<div className="flex items-center gap-2 text-sm font-medium">
<Building2 className="size-4" />
Şirket
</div>
<p className="text-muted-foreground text-xs">
Ekipteki herkes görür ve düzenleyebilir.
</p>
</button>
<button
type="button"
onClick={() => setValue("personal")}
className={cn(
"border-input flex flex-col items-start gap-1 rounded-md border p-3 text-left transition-colors",
value === "personal"
? "border-primary bg-primary/5"
: "hover:bg-muted/50",
)}
>
<div className="flex items-center gap-2 text-sm font-medium">
<User className="size-4" />
Bireysel
</div>
<p className="text-muted-foreground text-xs">
Yalnızca siz görürsünüz, ekibe yansımaz.
</p>
</button>
</div>
{description && <p className="text-muted-foreground text-xs">{description}</p>}
</div>
);
}
export function ScopeBadge({ scope }: { scope?: Scope }) {
if (scope === "personal") {
return (
<span className="bg-violet-500/15 text-violet-700 dark:text-violet-300 border-violet-500/30 inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] font-medium">
<User className="size-2.5" />
Bireysel
</span>
);
}
return null; // Company is default — no badge needed
}