Commit Graph

65 Commits

Author SHA1 Message Date
kovakmedya dd001ed5f4 fix: pre-create user_preferences row in getUserPrefs to prevent loop
createRow inside a Server Action triggers Next.js router cache
invalidation → layout reruns → components remount → loop.
By creating the empty row from getUserPrefs (Server Component,
not Server Action), saveUserPrefsAction always hits updateRow
which doesn't cause cache invalidation.
2026-05-09 02:22:04 +03:00
kovakmedya ba296fb3e4 fix: save dark/light mode selection to DB 2026-05-08 18:13:25 +03:00
kovakmedya c370949671 fix: remove cross-field overwrite in theme selection
each prop now saves only its own field; ThemeTab's
cross-clearing calls correctly null the other field
instead of overwriting the just-saved value
2026-05-08 18:06:06 +03:00
kovakmedya 2b6877736f fix: surface saveUserPrefsAction errors, convert empty strings to null
- saveUserPrefsAction now returns {ok, error} instead of void
- empty string values converted to null before writing (Appwrite nullable attrs)
- getUserPrefs treats empty strings as absent (str() helper)
- ThemeCustomizer savePrefs wrapper shows toast on failure
- manually cleared corrupted colorTheme/tweakcnTheme empty strings from DB
2026-05-08 18:00:22 +03:00
kovakmedya 971d8b0a58 feat: store user theme prefs in DB instead of Appwrite account.getPrefs
db: create user_preferences table (isletmem) — userId unique index,
    theme/colorTheme/tweakcnTheme/radius/sidebar* columns

- user-prefs-actions.ts: getUserPrefs (server-side read, plain object),
  saveUserPrefsAction (upsert by userId, Permission.user for row security)
- schema.ts: TABLES.userPreferences added
- layout.tsx: replace account.getPrefs+JSON.parse hack with getUserPrefs()
- dashboard-shell, prefs-initializer, theme-customizer: import UserPrefs
  type and saveUserPrefsAction instead of old saveThemePrefsAction
- theme-prefs-actions.ts: deleted (no remaining references)

Reason: account.updatePrefs is shared across all apps in the same Appwrite
project (İşletmem + Emlak share project 69f27b51). A dedicated per-app
table gives proper isolation, typed schema, and no prototype-object issues.
2026-05-08 17:48:31 +03:00
kovakmedya 00c740de80 fix: remove Appwrite sync effects that caused mount-loop
PrefsInitializer calls setTheme + updateConfig on mount. ThemeCustomizer
was watching these with skip-first-mount effects — but after PrefsInitializer
changed the values, the effects fired and called saveThemePrefsAction
(server action). Next.js server actions revalidate the router cache,
triggering a soft remount, resetting applied.current in PrefsInitializer,
causing another save → infinite loop / flicker.

Fix: dark/light and sidebar config no longer call saveThemePrefsAction
from reactive effects. Sidebar saves localStorage only. Color theme /
tweakcn / radius still call saveThemePrefsAction from explicit click
handlers (no mount trigger, no loop).
2026-05-08 17:33:43 +03:00
kovakmedya c307865a44 feat: persist theme prefs in localStorage for reliable same-device persistence
- Add src/lib/local-theme-prefs.ts: read/write color theme, radius, sidebar
  config to localStorage key 'isletmem-theme-prefs'
- ThemeCustomizer: init state from localStorage (falls back to Appwrite prefs),
  save to localStorage on every color/radius/sidebar change in addition to Appwrite
- PrefsInitializer: merge localStorage (wins) with server Appwrite prefs on mount

Appwrite updatePrefs had a silent try/catch so failures were invisible;
dark/light was already in localStorage via ThemeProvider but color theme was not.
2026-05-08 14:55:29 +03:00
kovakmedya e2d09ab138 fix: getPrefs sonucunu plain object'e çevir (Server→Client prop hatası) 2026-05-07 22:32:44 +03:00
kovakmedya 997cc393af feat: tema ayarları Appwrite user prefs ile kalıcı hale getirildi
- saveThemePrefsAction: account.updatePrefs ile mevcut prefs merge edilir
- Dashboard layout'ta account.getPrefs ile prefs server-side yüklenir
- PrefsInitializer: mount'ta dark/light, renk teması, radius, sidebar config Appwrite'dan uygulanır
- ThemeCustomizer: renk/tweakcn/radius değişikliği anında Appwrite'a kaydedilir; dark/light toggle useEffect ile izlenir
- Sayfa yenileme ve farklı cihazda giriş sonrasında ayarlar korunur
2026-05-07 22:26:40 +03:00
kovakmedya 78f50755ed feat(attachments): XHR upload + drag&drop + ilerleme çubuğu
- POST /api/attachments route: istemci XHR ile doğrudan sunucuya yükler, gerçek zamanlı progress olur
- AttachmentsPanel yeniden yazıldı: drag & drop zone, per-dosya ilerleme çubuğu (%), eş zamanlı çoklu yükleme
- Appwrite bucket fileSecurity=true güncellendi (per-dosya tenant izolasyonu)
2026-05-07 20:34:28 +03:00
kovakmedya 1299cd10ce feat: fatura PDF, hizmet/yazılım atama dosya ekleri
- /print/invoices/[id] sayfası: A4 fatura yazdırma/PDF (AutoPrint + PrintActionBar)
- Fatura detayı header'ına PDF butonu eklendi (Yazdır yerine)
- Appwrite Storage: entity-attachments bucket (20MB, şifreli)
- Appwrite Tables: attachments collection (tenantId, entityType, entityId, fileId, name, size, mimeType)
- attachment-actions.ts: fetchAttachmentsAction, uploadAttachmentAction, deleteAttachmentAction
- AttachmentsPanel bileşeni: dosya yükleme/listeleme/silme, edit modunda görünür
- Hizmet ve yazılım atama form sheet'lerine AttachmentsPanel entegrasyonu
- /api/files/[attachmentId]: güvenli proxy indirme (tenant doğrulama + admin key ile Appwrite'a istek)
2026-05-07 20:22:17 +03:00
kovakmedya a0aec13b8c feat(calendar): görevleri takvimde göster
- Bitiş tarihi olan ve done olmayan görevler takvimde turuncu pill olarak gösteriliyor
- Göreve tıklamak /tasks sayfasına yönlendiriyor, etkinlik formu açmıyor
- COLOR_BG'ye 'task' rengi eklendi (orange)
2026-05-07 19:47:50 +03:00
kovakmedya 63531d30d9 feat: add taxOffice and website fields to customers (db: customers) 2026-05-07 19:40:34 +03:00
kovakmedya f00928af4f fix: replace KovakSoft/KovakCRM with Kovak Yazılım ve Medya Ltd. Şti. throughout 2026-05-07 19:35:27 +03:00
kovakmedya 43e1de1fbc feat: custom password reset flow with 8-char token + Appwrite Messaging 2026-05-07 19:23:47 +03:00
kovakmedya ba6692166d feat: onboarding'de diğer KovakSoft uygulamalarından workspace içe aktarma 2026-05-06 23:04:16 +03:00
kovakmedya f81b7bf139 fix: filter teams by app — getUserTeams and setActiveTenant now reject cross-app teams 2026-05-06 22:31:57 +03:00
kovakmedya b06bba1901 fix: set return_url alongside success_url for Polar subscription redirects 2026-05-05 00:12:43 +03:00
kovakmedya 62777b3f99 fix: use @polar-sh/sdk validateEvent for webhook verification 2026-05-04 18:58:33 +03:00
kovakmedya 3986094bad fix: convert polar_whs_ prefix to whsec_ for svix compatibility 2026-05-04 18:52:55 +03:00
kovakmedya afbb029c67 fix: use svix library for Polar webhook signature verification 2026-05-04 18:47:28 +03:00
kovakmedya 89830aa28f fix: support svix-* headers for Polar webhooks, extend timestamp window 2026-05-04 18:32:56 +03:00
kovakmedya 106c33d1b4 fix: strip polar_whs_ prefix from webhook secret before base64 decode 2026-05-04 18:26:30 +03:00
kovakmedya e8a766c60a feat: Polar.sh payment integration, replace Shopier store approach 2026-05-04 17:45:30 +03:00
kovakmedya f43818a51a fix: move initialLeadState out of use-server file into lead-types.ts 2026-05-02 22:40:05 +03:00
kovakmedya ea3d6f6045 feat: leads module — kanban board, aktiviteler, takvim entegrasyonu, müşteriye dönüştür 2026-05-01 03:26:31 +03:00
kovakmedya 9e1355a137 feat: services — preset names, currency, multi-assignee (MCP: assigneeIds+currency columns) 2026-05-01 02:52:09 +03:00
kovakmedya 591df8f82e fix: increase serverActions bodySizeLimit to 3mb for logo uploads 2026-05-01 02:31:02 +03:00
kovakmedya c9d818435f fix: move initialLogoState out of use-server file into logo-types.ts 2026-05-01 02:23:35 +03:00
kovakmedya 00a8351f66 feat: Shopier payment integration with 3DS callback + unified checkout action 2026-04-30 21:57:51 +03:00
kovakmedya 196036c0d8 feat: plan tier system, mock checkout, saved cards, tenant logo upload + mobile sheet fix
Plan & billing layer:
- New tables: subscription_payments, saved_cards (via Appwrite MCP)
- tenant_settings: plan/planStartedAt/planExpiresAt/lastPaymentId columns
- Free tier limits (50 customers / 100 finance entries / 5 software / 1 member)
  enforced via requirePlanCapacity gate in create actions
- PlanLimitDialog opens when limit hit; UsageBanner at 80% threshold
- /pricing rebuilt with Free + Pro tiers and Klinik/Ajans ecosystem teasers
- /settings/billing redesigned: compact plan summary, saved cards list,
  KVKK transparency block, payment history
- Usage stats moved to /pricing where they are decision-relevant

Mock checkout flow:
- 3D animated credit card with sync inputs and CVC flip
- Brand auto-detection (Visa / Mastercard / Amex / troy)
- Saved-card mode when previous cards exist; first card defaults to default
- 'Bu kartı kaydet' checkbox with explicit storage scope disclosure
- /settings/billing/checkout/[orderId] route

Saved cards:
- saved_cards bucket stores last4 + brand + expiry + holder only
- Default toggle, remove action, owner-only management
- Architecture ready for Shopier provider token swap-in

Tenant logo upload (first file upload feature):
- New Appwrite bucket: tenant-logos (max 2MB, image only, public read)
- uploadLogoAction with orphan cleanup, removeLogoAction
- LogoUploader UI: drag-drop, client-side preview, validation
- Sidebar shows logo when set, falls back to default icon

Mobile sheet fix:
- SheetContent uses h-dvh instead of h-full (dynamic viewport)
- SheetFooter pads pb-[max(1rem,env(safe-area-inset-bottom))]
- 13 form sheets switched py-4 → pt-4 to let safe-area apply

db: subscription_payments, saved_cards tables; tenant_settings plan columns;
    tenant-logos storage bucket
2026-04-30 21:36:01 +03:00
kovakmedya ab336b191f feat(team): proper member removal + self-leave flow
Member-management UX cleanup:

- Replaced window.confirm() with shadcn Dialog confirmation (matches
  every other destructive action in the app).
- Toast feedback on success/error for both removal and role updates —
  before, errors from the server (örn. 'Sahibi yalnızca başka bir sahip
  kaldırabilir') were swallowed.
- New 'Ayrıl' (leave) button on the current user's own row — previously
  there was no way for a member to leave a workspace except by being
  removed by an admin.

Server (lib/appwrite/team-actions.ts):
- New leaveWorkspaceAction:
  * Refuses if the caller is the only owner (would leave the workspace
    ownerless).
  * Calls teams.deleteMembership for the caller's own membership.
  * Clears account.prefs.activeTenant + isletmem-tenant cookie so the
    next request goes to fallback tenant or onboarding.
  * Audit-logged with self:true marker.
- removeMemberAction unchanged (already had owner-only-can-remove-owner
  + can't-remove-self guards).

UI:
- Each row's action cell now shows: 'Ayrıl' (self) / 'Çıkar' (others, if
  canManage and target isn't owner) / nothing (the rest).
- Removal dialog explains data isn't deleted, just access revoked.
- Leave dialog warns the user about losing access.
- Both dialogs gate close-on-outside-click while a request is in flight.
2026-04-30 08:41:52 +03:00
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 2549ce097c feat(ui): white-label theme customizer panel — Türkçe + sansürlü
Yan paneldeki görünüm özelleştirme widget'ı template branding'inden
arındırıldı:

- 'Customizer' → 'Görünüm'
- Tab adları: 'Theme'/'Layout' → 'Renk'/'Düzen'
- 'Shadcn UI Theme Presets' → 'Hazır temalar'
- 'Tweakcn Theme Presets' → 'Genişletilmiş temalar' (kaynak adı
  uygulamada görünmüyor)
- 'Random' → 'Rastgele', 'Choose ... Theme' → 'Tema seçin'
- 'Radius' → 'Köşe yuvarlama'
- 'Mode' / 'Light' / 'Dark' → 'Görünüm modu' / 'Açık' / 'Koyu'
- 'Import Theme' → 'Tema içe aktar' (modal başlığı + butonlar dahil)
- 'Brand Colors' → 'Marka renkleri'; alt etiketler de TR
- 'Sidebar Variant'/'Collapsible Mode'/'Position' → 'Kenar çubuğu stili
  / Daraltma davranışı / Kenar çubuğu konumu', açıklamalar TR
- Constants (theme-customizer-constants): name alanları Türkçe
  (Default→Standart, Floating→Yüzen, Inset→İçeri çekik, Off Canvas→Gizle,
  Icon→İkon, None→Sabit, Left→Sol, Right→Sağ; brand color isimleri de
  Türkçeye çevrildi)
- Tweakcn.com reklam kartı **tamamen kaldırıldı** (artık 3rd party link
  yok). Onun yerine import butonunun altına nötr bir ipucu yazısı:
  'tweakcn.com gibi araçlardan dışa aktardığınız JSON tema dosyasını
  yükleyebilirsiniz.' — yani aracı bilgi olarak veriyoruz, reklam değil.
- Reset/X butonlarına a11y label + tooltip eklendi.

Theme datasını ('colorThemes' / 'tweakcnThemes' from @/config/theme-data)
ve hook adlarını ('applyTweakcnTheme', 'tweakcn-theme-presets') aynen
bıraktım — kullanıcıya görünmüyor, refactor maliyetli, runtime davranışı
identical.
2026-04-30 08:03:00 +03:00
kovakmedya 37cf745ca1 feat(finance): /finance/reports — single-page financial overview
CFO-style summary that pulls everything together: cash position, P&L by
period, 12-month trend, top customers, expense breakdown, loans, cards,
outstanding invoices.

Aggregator (lib/appwrite/finance-report-queries.ts):
- getFinancialReport(tenantId, period). Period: month / quarter / year /
  all. Pulls all relevant tables in parallel (customers, invoices, finance
  entries, bank accounts, loans, installments, cards, statements).
- KPIs: period income, period expense, period net, current cash position
  (bank balances + receivables − loan remaining − card outstanding).
- Cash composition: separates the four pieces of net cash position with
  links to drill in.
- Trend: 12-month income/expense series.
- Top customers: paid invoice totals, period-bound.
- Expense breakdown: heuristic split using financeEntryId backrefs from
  loan_installments and credit_card_statements — lets us bucket auto-
  generated expenses (loan vs card vs manual) without needing a new
  category column.
- Active loans summary: top 8 by remaining balance.
- Card statements: pending + overdue, sorted by due date.
- Outstanding invoices: top 12 unpaid (overdue first).

Page (/finance/reports):
- Period selector (?period=month|quarter|year|all). Default 'month';
  default URL drops the param.
- KPI row → composition card → 12-month trend → top customers + expense
  breakdown → loans + cards tables → outstanding invoices.
- Trend chart loaded via next/dynamic with ssr: false (recharts is heavy
  and only runs client-side anyway).

Sidebar Finans submenu: added Rapor → /finance/reports.
2026-04-30 07:55:39 +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 c848531326 feat(search): wire ⌘K palette to all entities + quick actions
Replaces the static template list with a working multi-entity command
palette tied to the active workspace.

Server (lib/appwrite/search-actions.ts):
- globalSearchAction(query): runs after the user has typed >= 2 chars.
  Pulls up to 200 rows per entity for the active tenant via requireTenant
  and admin SDK, then in-memory filters on:
    customers: name, email, phone, taxId
    invoices: number, notes, customer name (via id->name map)
    tasks: title, description
    services: name, description
    software: name, version, description
    calendar events: title, description
    finance entries: description, customer name, amount string
- Returns at most 8 hits per group. Each hit has { title, subtitle, url,
  group } so the client doesn't need extra lookups. Turkish-aware
  toLocaleLowerCase('tr-TR').

Client (components/command-search.tsx):
- Rewritten. cmdk Command with shouldFilter=false (we provide filtered
  results from the server).
- 220ms debounce on input; spinner during fetch.
- Ordered groups: Müşteriler / Faturalar / Görevler / Takvim / Finans /
  Hizmetler / Yazılımlar — each with its own icon.
- Static groups always evaluated client-side from the typed query:
  * Sayfalar (8 nav items)
  * Hızlı aksiyonlar (5 — yeni müşteri / fatura / görev / etkinlik /
    finans girişi)
  * Ayarlar (3 — şirket bilgileri / ekip / profil)
- Empty-state message ('Sonuç bulunamadı') only shown when the query
  is non-trivial AND nothing matches anywhere.
- Footer hint row with ↵/↑↓/Esc/⌘K legend.
- Invoice hits navigate to /invoices/[id]; other entity hits go to the
  list page (no per-id detail routes for those yet).

Trigger button (SearchTrigger): localized to 'Hızlı ara...'.
2026-04-30 07:08:22 +03:00
kovakmedya 89d456fc76 feat(profile): /settings/account — name, email, password
Replaces template's mock account form with real Appwrite-backed actions.

Server actions (lib/appwrite/profile-actions.ts):
- updateNameAction: account.updateName via session SDK; revalidates layout
  so the new name shows in sidebar/header right away.
- updateEmailAction: account.updateEmail (requires current password as
  Appwrite confirmation). Maps user_email_already_exists to a friendly
  Turkish message.
- updatePasswordAction: account.updatePassword(new, old). Validates
  old != empty, new >= 8 chars, new === confirm. Maps
  user_password_recently_used / user_password_mismatch.
- All audit-logged with entityType user_name / user_email / user_password
  and tenantId of the user's currently-active workspace (or 'global' if
  none). Audit failures swallowed.

UI:
- /settings/account is now an async server page that pulls the user via
  getCurrentUser, renders an account info card ($id, registration date,
  email verification, MFA), then 3 separate small forms — one card each
  — for name, email, password. Each form clears its own state and gives
  toast feedback independently.
- Removed the template's react-hook-form-based mock page.

Side note: skipped email-verification flow + MFA setup for later.
2026-04-30 06:47:53 +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 02a02ba9e6 fix: footer rebrand + iç sayfada genel-bakış'a yanlış redirect
1) Footer

The template's 'Made with ❤ by ShadcnStore Team' is gone. Footer now
shows '© <year> İşletmem — bir KovakSoft ürünüdür.' on the left and
Kullanım şartları / Gizlilik links on the right. Compact one-line layout
on desktop, stacked on mobile.

2) Internal pages were redirecting users to /onboarding (and from there
   to /dashboard) even when they had a valid tenant.

Root cause: requireTenant() called getActiveTenantId() and threw
NO_TENANT when the active-tenant cookie/prefs were missing — even though
the user's actually a member of one or more teams. getActiveContext()
already handled this fallback for the layout, but the per-page
requireTenant() guard didn't, so /customers, /tasks, /invoices, etc. all
bounced through /onboarding back to /dashboard.

- requireTenant() now falls back to the user's first team via
  teams.list(), and writes the cookie so the next request is fast.

3) Side fix: acceptInviteAction now sets account.prefs.activeTenant + the
   ACTIVE_TENANT cookie after successful join, so a freshly invited
   member lands directly in the right workspace instead of relying on
   the team-list fallback.
2026-04-30 06:39:27 +03:00
kovakmedya 858c916d95 feat(tasks): personal-scope filter — mine / unassigned / mine+unassigned / all
Multi-tenant teams need a way to focus on their own work without seeing
the whole team's board. Added a server-side filter:

- /tasks?view=mine_or_unassigned (default): own assigned + unassigned
- /tasks?view=mine: only assigned to me
- /tasks?view=unassigned: claimable, unassigned tasks
- /tasks?view=all: full team board (managers / overview)

Implementation:
- Server page reads ?view= query, validates against allowed list,
  filters tasks before passing to the client. Also computes total
  counts (across all rows) for each filter so the dropdown can show
  '(N)' badges that don't change when the user switches views.
- TasksBoard top-bar gets a Select that updates the URL via
  router.push() (preserves Next's full SSR + revalidatePath flow).
- Default-filter URL drops the ?view= param to keep the canonical
  /tasks URL clean.

Card visual cues:
- Tasks assigned to current user get a primary-tinted ring + a 'Bana
  atanmış' badge (replaces the assignee name pill).
- Unassigned tasks get a dashed border + 'Atanmamış' badge so they
  visually invite ownership.

Dashboard:
- 'Açık görevler' metric is now 'Açık görevlerim' — sums only own +
  unassigned tasks. Subtext updates accordingly. Same scoping for
  urgent count. Managers can still see team-wide via /tasks?view=all.
2026-04-30 06:28:25 +03:00
kovakmedya f11cd099f6 feat(dashboard): real data — metrics, charts, top customers, recent transactions
Dashboard is no longer mock data. Single getDashboardData(tenantId) server
query computes everything in one pass.

New aggregator (lib/appwrite/dashboard-queries.ts):
- Pulls customers, invoices, finance_entries, tasks, services in parallel.
- Derives:
  * metrics: totalCustomers, activeCustomers, monthIncome,
    prevMonthIncome (for delta), outstanding (unpaid invoice total),
    overdueCount, openTasks, urgentTasks
  * monthlyIncome: 12-month income+expense series for area chart
  * topCustomers: 5 highest-grossing customers by paid invoice total
  * recentTransactions: 8 newest finance entries
  * topServices: 5 services by aggregate unit price (placeholder, will
    refine when we have invoice line analytics)
  * newCustomersMonthly: 6-month new customer count for bar chart

Components (dashboard/components/):
- Metrics: 4 cards with trend indicator on income (delta vs previous
  month), warning tone on overdue invoices and urgent tasks.
- IncomeChart: Recharts Area chart, dual income/expense series with
  gradient fills, Turkish month labels.
- TopCustomers: ranked list with progress bars relative to top earner.
- RecentTransactions: list with type badge, signed amount, link to
  /finance for full list.
- CustomerGrowth: BarChart of new customers per month (last 6).
- QuickActions: 4 buttons linking to /customers, /invoices, /calendar,
  /tasks (replaced template's New User/Add Product/etc).

Layout: 4 metric cards row, then income chart + top customers (2-col),
then recent transactions + customer growth (2-col).

Removed:
- src/app/(dashboard)/dashboard-2/ (was the demo page; same components
  re-exported into the real /dashboard from there. Now /dashboard owns
  its components.)
- 'Dashboard 2' entry from CommandSearch; replaced with our actual
  module list (Müşteriler / Hizmetler / Yazılımlarımız / Takvim /
  Görevler / Gelir-Gider / Faturalar).
2026-04-30 06:19:44 +03:00
kovakmedya 37777a71f9 feat(invoices): auto income entry on 'paid' status
Marking an invoice as paid now creates a finance_entry (type=income) for
the customer with amount = invoice.total, linked via invoiceId. Reverting
status removes the entry. Idempotent: re-saving while already paid keeps
the existing entry (resyncs amount if invoice total changed in the
meantime).

- syncPaymentEntry(tenantId, userId, invoice) helper:
  * status === 'paid': create entry if none exists; otherwise update
    amount to match current invoice.total.
  * status !== 'paid': delete any income entries linked to the invoice.
  * Best-effort — failures are swallowed so the invoice mutation always
    succeeds even if Appwrite hiccups on the finance write.
  * Each create/delete writes an audit row tagged auto: 'invoice_paid' /
    'invoice_unpaid' so we can trace later.
- updateInvoiceAction now calls syncPaymentEntry after persisting.
- recomputeTotals (run on every item add/update/delete) also re-syncs
  the linked entry's amount when the invoice is currently paid.
- deleteInvoiceAction now cascade-deletes any linked finance_entries in
  addition to items.
- /invoices and /invoices/[id] both revalidate /finance after writes.

UI:
- Invoice form shows a hint under the status select explaining the
  finance side effect.
- Finance table tags rows with a 'Faturadan' badge when invoiceId is
  set, so users can tell auto-generated entries apart from manual ones.
2026-04-30 06:14:31 +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 add2317717 fix(ui): add 'use client' to sonner Toaster and chart components
Both files use client-only hooks (useTheme in sonner; useState/useMemo/
useId in chart via React) but the shadcn CLI shipped them without the
'use client' directive. Mounting <Toaster /> in the root server layout
crashed in production with:
  Attempted to call useTheme() from the server but useTheme is on the
  client.

Same risk on chart.tsx once any dashboard page renders <Chart>.

Verified all of src/components/ui/*.tsx — only these two needed the fix.
Future shadcn additions: check 'head -1' for the directive before
importing into server components.
2026-04-30 05:52:08 +03:00