Commit Graph

12 Commits

Author SHA1 Message Date
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
kovakmedya 121fbdba9d feat(banking C): credit cards + monthly statements
Final banking step. User-managed: each month, paste your bank statement
totals into the form. We don't auto-compute interest — bank's number is
authoritative.

Schema:
- credit_cards: bankName, cardName, last4, creditLimit, statementDay,
  dueDay, interestRate (monthly nominal %), bankAccountId (optional FK,
  payments expense from this account), archived, notes.
- credit_card_statements: cardId, period (YYYY-MM), statementDate,
  dueDate, totalDebt, minimumPayment, paidAmount, status
  (pending/partial/paid/overdue), financeEntryId (link to last payment).
- Indexes on (tenantId, archived) for cards and (tenantId, cardId) +
  (tenantId, status, dueDate) for statements.

Server (lib/appwrite/credit-card-actions.ts):
- create/update/archive(toggle)/delete for cards. Card delete cascades
  through statements + their finance_entries.
- createStatementAction: computes status from dueDate + paidAmount.
- payStatementAction: partial-payment friendly. Creates a single
  finance_entry expense for the pay amount, capped at remaining balance,
  bankAccountId carried from the card. Recomputes status: paid (full),
  partial (some + pre-due), overdue (past due with anything < total).
- deleteStatementAction: removes linked finance_entry too.
- All audit-logged.

UI (/finance/cards):
- 3 stat cards: aktif kart sayısı, bekleyen toplam, vadesi geçmiş ekstre.
- Per-card panel: bank/name/last4, limit + statement/due day + monthly
  interest, current outstanding. Statement table inside card with status
  badges, Öde button (opens partial-payment dialog), delete button.
- CardFormSheet for card CRUD, StatementFormSheet for statement creation
  with default period/dates derived from card's statementDay/dueDay.

Sidebar Finans submenu now functional: Banka hesapları → /finance/banks,
Krediler → /finance/loans, Kredi kartları → /finance/cards.
2026-04-30 07:36:31 +03:00
kovakmedya b632ae8a73 feat(banking B): bank loans + amortization schedule
Step 2 of banking. Loan creation auto-generates the full installment
schedule using standard amortization (eşit taksitli kredi):
  monthlyPayment = P × r × (1+r)^n / ((1+r)^n − 1)

Schema:
- bank_loans: bankAccountId (optional FK), bankName, loanName, loanType
  enum (consumer/vehicle/housing/commercial/kmh/other), principal,
  interestRate (monthly nominal %), termMonths, monthlyPayment, startDate,
  paymentDay (1-28, clamped per month), status (active/closed/defaulted).
- loan_installments: loanId, installmentNo, dueDate, amount, principalPart,
  interestPart, paid, paidAt, financeEntryId.
- Indexes on bank_loans(tenantId, status) and loan_installments(tenantId,
  loanId) and (tenantId, paid, dueDate).

Server (lib/appwrite/loan-actions.ts):
- createLoanAction: validates with Zod, computes amortization including
  rounding-drift handling on the last installment, persists loan + N
  installments, audit-logs. Atomic rollback on failure (deletes any
  partially-created installments and the loan).
- payInstallmentAction: atomically creates a finance_entry (expense,
  bankAccountId carried over from the loan), updates installment with
  paid=true + financeEntryId. If it was the last unpaid installment,
  marks loan status='closed'.
- unpayInstallmentAction: deletes the linked finance_entry, clears paid
  fields, reopens the loan if it was closed.
- deleteLoanAction: cascade-deletes all installments first, then the loan.

UI (/finance/loans):
- 3 stat cards: aktif kredi sayısı, toplam çekilen, kalan ödeme.
- Loan card per loan with bank/name/type/status badges, anapara/aylık
  taksit/faiz/sonraki ödeme grid, progress bar (paid/total), expandable
  installment table.
- Installment row: # / vade (red if overdue) / anapara / faiz / toplam /
  Ödendi-Geri al toggle.
- LoanFormSheet: live preview of monthly payment, total payment, total
  interest as user changes principal/rate/term. paymentDay clamped 1-28
  to avoid month-length issues.
2026-04-30 07:29:24 +03:00
kovakmedya 7b6be623ae feat(banking A): bank accounts module + finance integration
First of 3-step banking expansion. Banks tracked separately from
customer/supplier debts so we can compute real cash position later.

Schema:
- New bank_accounts table: bankName, accountName, iban, openingBalance,
  notes, archived. Indexes on (tenantId, archived).
- New column finance_entries.bankAccountId (FK, optional). Index on
  (tenantId, bankAccountId).
- schema.ts: TABLES.bankAccounts, BankAccount type, FinanceEntry gains
  bankAccountId.

Server side:
- lib/validation/bank-accounts.ts (Zod): IBAN normalized to upper-case
  no-spaces; openingBalance defaults to 0.
- lib/appwrite/bank-account-actions.ts: create/update/archive(toggle)/
  delete with audit. Delete refuses if any finance_entry still references
  the account; archive toggle replaces it for safe disable.
- lib/appwrite/bank-account-queries.ts:
  * listBankAccounts
  * getBankAccountBalances — computes opening + Σ(income) − Σ(expense)
    per account by scanning up to 5000 entries with bankAccountId set.
    Pure cash flow; debt/receivable don't move balance.
  * listEntriesForAccount

UI:
- /finance/banks server page renders BanksClient with computed balances.
- BanksClient: card grid for active accounts, collapsed details for
  archived. Sum card on top showing total active balance (color-coded by
  sign). Each card shows bank, account name, formatted IBAN, current
  balance + opening (if drifted). Dropdown: Düzenle / Arşivle / Sil.
- BankFormSheet: bank/account/IBAN/openingBalance/notes form.
- Finance form gets a bank-account Select (sentinel-stripped). Existing
  finance entries get a 'bankAccountLabel' subtitle in their row.

Sidebar: Finans group expanded with Bankalar submenu (Banka hesapları
/ Krediler / Kredi kartları). The latter two land in B and C.
2026-04-30 07:22:51 +03:00
kovakmedya fc091b9e0d feat(settings): /settings/workspace — edit company info + invoice defaults
Owner/admin edit, member read-only.

Schema/validation:
- lib/validation/workspace.ts (workspaceSettingsSchema)
- lib/appwrite/workspace-actions.ts:
  * updateWorkspaceSettingsAction — requireRole owner|admin, upserts the
    tenant_settings row (creates one with team-scoped perms if absent,
    e.g. for tenants created before tenant_settings was a table; just
    defense). Audit-logged.
  * Forces invoicePrefix to uppercase. defaultVatRate clamped to [0, 100].
  * revalidatePath('/', 'layout') so the new company name updates in
    sidebar header and dashboard greeting on next render.

UI:
- /settings/workspace page (server) — pulls active tenant settings
  via requireTenant, shows form pre-filled.
- WorkspaceSettingsForm: 2 cards
  * Şirket — name (required), tax id, phone, email, address
  * Faturalama — invoicePrefix, defaultVatRate, read-only invoiceCounter
- All inputs disabled if user is a member (canEdit=false). Submit button
  hidden in that case. Description on the page changes accordingly.
- Toast feedback for success/error.

Skipped: logo upload (storage bucket pending). Will revisit.
2026-04-30 06:44:11 +03:00
kovakmedya d99daca3ca feat(invoices): full invoice + line items module
The most complex module. Two-table model: invoices (header) +
invoice_items (lines). Auto-numbering via tenant_settings.invoiceCounter,
auto-totals on item changes.

Schema/validation:
- lib/validation/invoices.ts: invoiceSchema (header) + invoiceItemSchema
  (line). Both coerce comma decimals.
- lib/appwrite/invoice-actions.ts:
  * createInvoiceAction — fetches tenant_settings, increments
    invoiceCounter, formats number as '{prefix}-{year}-{0000}',
    persists totals as 0/0/0 (recomputed when items added).
  * updateInvoiceAction / deleteInvoiceAction — header CRUD; delete
    cascades to remove all items first then header.
  * addInvoiceItemAction / updateInvoiceItemAction /
    deleteInvoiceItemAction — line CRUD. Each computes lineTotal
    (qty*unit + vat) and triggers recomputeTotals(invoiceId) which
    re-sums all items and updates the header subtotal/vatTotal/total.
  All audit-logged.

Queries:
- listInvoices, getInvoice (with tenant cross-check), listInvoiceItems.

UI:
- /invoices index: 4 stat cards (Toplam / Tahsil edildi / Bekleyen /
  Gecikmiş), table with overdue-aware due date coloring, status badges,
  number is a Link to detail.
- InvoiceFormSheet: customer + dates (default issue=today, due=+30d) +
  status + notes. After create, redirects to /invoices/[id] for adding
  items.
- /invoices/[id] detail: header strip with status, dates, customer name;
  print/edit/delete actions; items editor card; subtotal/VAT/total card;
  notes card.
- InvoiceItemsEditor: rows are clickable to edit, X button to delete.
  ItemFormSheet for add/edit (description + qty + unitPrice + VAT %).

Print is just window.print() for now — relies on browser dialog. Detail
page deliberately uses tabular-nums for amounts.
2026-04-30 06:09:24 +03:00
kovakmedya 98ab73235f feat(finance): income/expense/debt/receivable tracking + summary
Multi-tenant cash flow tracker. All amounts in TRY, decimals preserved.

Schema/validation:
- lib/validation/finance.ts: financeEntrySchema with type enum, positive
  amount, date required, optional customer/invoice link, optional payment
  method.
- lib/appwrite/finance-actions.ts: create/update/delete with audit; date
  HTML input normalized to ISO before write.
- lib/appwrite/finance-queries.ts: listFinanceEntries ordered by date desc.

UI:
- /finance server page passes entries + customers to FinanceClient.
- 5 stat cards: Gelir / Gider / Net (income-expense, color-coded by sign)
  / Alacaklar / Borçlar.
- Type filter dropdown (Tümü/Gelir/Gider/Alacaklar/Borçlar) + global
  search (description/customer/amount).
- 4 quick-add buttons let users start a new entry pre-filled with the
  desired type. Single FinanceFormSheet handles all 4 types via a Select.
- Table: type badge (color-coded), signed amount (+ for income/receivable,
  − for expense/debt), date, customer, payment method label, description
  preview. Row dropdown: Edit / Delete.
- Inline destructive Sil button in Sheet footer when editing.
2026-04-30 06:04:46 +03:00
kovakmedya b4c1073d91 feat(calendar): month-view calendar bound to calendar_events
Replaces the template's static-data calendar with a multi-tenant calendar
backed by Appwrite calendar_events.

Schema/validation:
- lib/validation/calendar.ts (calendarEventSchema with cross-field check
  end >= start)
- lib/appwrite/calendar-actions.ts: createCalendarEventAction,
  updateCalendarEventAction, deleteCalendarEventAction. Date inputs
  (HTML datetime-local 'YYYY-MM-DDTHH:mm', date 'YYYY-MM-DD') are
  normalized to ISO 8601 before write.
- lib/appwrite/calendar-queries.ts: listCalendarEvents with optional
  start/end range queries.

UI:
- /calendar server page: pulls events + customers, hands to CalendarClient.
- CalendarClient: month grid (6 rows × 7 cols), Monday-first, today badge,
  prev/next/Bugün nav. Multi-day events show on every day in their range.
  Each day cell shows up to 3 event chips with start time prefix; '+N
  daha' for overflow. Hover reveals a + button to add an event on that day.
- EventFormSheet: title, all-day switch (toggles input type between
  date and datetime-local), start/end with validation, customer FK,
  color preset (blue/green/amber/red/violet/slate). Sentinel '__none__'
  for nullable Selects. When editing, footer shows a destructive 'Sil'
  ghost button on the left that triggers the parent's confirm dialog.

Color tokens centralized in COLOR_BG map; falls back to primary tint.

Removed all template calendar files (calendars.tsx, calendar-main, etc.)
since the data model didn't match.
2026-04-30 06:01:42 +03:00
kovakmedya 671195fb7d feat(tasks): Kanban board with drag-and-drop (dnd-kit)
Replaces the template's /tasks demo (deleted) with a real multi-tenant
Kanban board.

Schema/validation:
- lib/validation/tasks.ts (taskSchema with status/priority enums + dueDate
  optional + assignee/customer optional)
- lib/appwrite/task-actions.ts: createTaskAction, updateTaskAction,
  deleteTaskAction, moveTaskAction (used by drag-drop). All audit-logged;
  moveTaskAction only audits when status actually changes.
- lib/appwrite/task-queries.ts: listTasks ordered by 'order' asc.

UI:
- /tasks server page assembles { tasks, customers, teamMembers } and
  passes to TasksBoard. Removed the template's data-table demo files.
- TasksBoard (client): 4 droppable columns. Columns use @dnd-kit/core
  useDroppable; cards inside each column are SortableContext+useSortable
  for intra-column ordering. closestCorners collision detection.
- Drag-end computes new 'order' as midpoint between adjacent tasks
  (no full reindex), updates UI optimistically, then persists via
  moveTaskAction. Rolls back on server error with toast.
- TaskCard: priority badge (color-coded), due-date badge (red if
  overdue), assignee badge, customer subtitle, dropdown (Edit/Delete)
  on hover.
- TaskFormSheet: title/description/status/priority/dueDate/assignee/
  customer. Uses sentinel '__none__' for nullable Selects (Radix Select
  forbids empty string values), stripped before submit.
- DragOverlay shows the dragged card rotated 3deg with shadow.
2026-04-30 05:57:35 +03:00
kovakmedya 113988273f feat(software): catalog + customer assignments (M2M)
Software catalog with per-customer assignments via the customer_software
join table. Two tabs in one /software page:

Catalog tab:
- Software CRUD: name, version, description, defaultFee (TRY).
- Deleting a software cascades and removes all its assignments first
  (best-effort loop, then the catalog row), all wrapped in audit logs.

Assignments tab:
- M2M between customer and software with own fee (overrides defaultFee),
  billingPeriod (monthly default), startDate/endDate, notes.
- Form auto-fills fee from selected software's defaultFee.
- Both Sheet forms localized; date inputs round-tripped via toIsoDate
  (Appwrite expects ISO 8601 with TZ; HTML date input gives YYYY-MM-DD).
- Delete dialogs differentiated for catalog ('siliniyor') vs assignment
  ('kaldırılıyor').

New files:
- lib/validation/software.ts (softwareSchema + customerSoftwareSchema)
- lib/appwrite/software-actions.ts (6 server actions)
- lib/appwrite/software-queries.ts (listSoftware, listAssignments)
- lib/appwrite/software-types.ts (form state)
- /software route with SoftwareClient (Tabs), SoftwareFormSheet,
  AssignmentFormSheet, inline delete dialogs.

Empty states surface the right next-step CTA: 'önce müşteri ekleyin', or
'önce yazılım ekleyin', as appropriate.
2026-04-30 05:50:33 +03:00
kovakmedya a15a1c1c1a feat(services): customer-linked services CRUD module
Same pattern as customers. Each service belongs to one customer (FK), has
unit price (TRY), billing period (monthly/yearly/onetime), and recurring
flag.

- lib/validation/services.ts: Zod schema with TRY price coercion (accepts
  comma decimal), enum billing period, recurring boolean coercion.
- lib/appwrite/service-actions.ts: createServiceAction, updateServiceAction,
  deleteServiceAction. Same tenant guard, audit log, team-scoped row perms.
- lib/appwrite/service-queries.ts: listServices, listServicesByCustomer.
- lib/format.ts: formatTRY (Intl.NumberFormat tr-TR), formatDate/DateTime,
  BILLING_PERIOD_LABEL mapping (Aylık/Yıllık/Tek seferlik).
- /services page (server): joins services with customer names via
  in-memory map.
- ServicesClient: TanStack table with global filter (name/customer/desc),
  Repeat icon next to recurring badges, formatted TRY column.
- ServiceFormSheet: customer dropdown (disabled if no customers exist),
  unit price + billing period + recurring switch.
- DeleteServiceDialog: same destructive confirmation pattern.

Empty state on /services CTA's user back to add a customer first if none
exist.
2026-04-30 05:46:55 +03:00
kovakmedya 94f2c92da1 feat(customers): full CRUD module — pattern for all other modules
Establishes the multi-tenant module pattern. Subsequent modules (services,
software, calendar, tasks, finance, invoices) will copy this structure.

Validation:
- lib/validation/customers.ts: Zod schema with Turkish messages, optional
  fields normalized to undefined.

Server actions (lib/appwrite/customer-actions.ts):
- createCustomerAction, updateCustomerAction, deleteCustomerAction
- All call requireTenant() guard, write team-scoped row permissions
  (read+update by team, delete by owner|admin), and emit audit log.
- Update/delete cross-check tenantId on the existing row before mutating
  (defense in depth even though row-level perms already enforce it).
- Field-level errors flattened from Zod for inline form display.

Server-side queries (lib/appwrite/customer-queries.ts):
- listCustomers(tenantId), getCustomer(tenantId, id) — admin SDK with
  Query.equal('tenantId',...) tenant scope.

UI:
- /customers page (server component): pulls active tenant context, lists
  customers, hands off to CustomersClient.
- CustomersClient: TanStack Table with global filter (name/email/phone/
  taxId), column sorting on name + createdAt, pagination (20/page),
  status badges, row actions (Edit/Delete dropdown), empty-state CTA.
- CustomerFormSheet: shadcn Sheet-based add/edit form with all fields,
  toast feedback (sonner), inline field errors. Reused for create + update
  by switching the action.
- DeleteCustomerDialog: confirmation modal with destructive button.

Infrastructure:
- Added sonner Toaster to root layout (richColors, closeButton).
- Updated metadata to 'İşletmem KovakCRM' and html lang='tr'.
- Renamed theme storage key to isletmem-ui-theme.
2026-04-30 05:44:00 +03:00