-
-
+
+
-
-
-
-
+
+
+
Bekleyen Eşleşmeler
+
—
diff --git a/src/app/(dashboard)/faqs/components/faq-list.tsx b/src/app/(dashboard)/faqs/components/faq-list.tsx
deleted file mode 100644
index dd97584..0000000
--- a/src/app/(dashboard)/faqs/components/faq-list.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
-import { cn } from "@/lib/utils"
-import { Search } from "lucide-react"
-
-interface FAQ {
- id: number
- question: string
- answer: string
- category: string
-}
-
-interface Category {
- name: string
- count: number
-}
-
-interface FAQListProps {
- faqs: FAQ[]
- categories: Category[]
-}
-
-export function FAQList({ faqs, categories }: FAQListProps) {
- const [selectedCategory, setSelectedCategory] = useState("All")
- const [searchQuery, setSearchQuery] = useState("")
-
- // Filter FAQs based on selected category and search query
- const filteredFaqs = faqs.filter(faq => {
- const matchesCategory = selectedCategory === "All" || faq.category === selectedCategory
- const matchesSearch = searchQuery === "" ||
- faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
- faq.answer.toLowerCase().includes(searchQuery.toLowerCase())
- return matchesCategory && matchesSearch
- })
-
- return (
-
- {/* Categories Sidebar */}
-
-
- Categories
-
-
- setSearchQuery(e.target.value)}
- />
-
-
-
- {categories.map((category) => (
- setSelectedCategory(category.name)}
- >
- {category.name}
-
- {category.name === "All" ? faqs.length : category.count}
-
-
- ))}
-
-
-
- {/* FAQs List */}
-
-
-
-
- {selectedCategory === "All" ? "All FAQs" : `${selectedCategory} FAQs`}
-
- ({filteredFaqs.length} {filteredFaqs.length === 1 ? 'question' : 'questions'})
-
-
-
-
-
- {filteredFaqs.length === 0 ? (
-
-
No FAQs found matching your search criteria.
-
- ) : (
-
- {filteredFaqs.map((item) => (
-
-
-
- {item.question}
-
- {item.category}
-
-
-
-
- {item.answer}
-
-
- ))}
-
- )}
-
-
-
-
-
- )
-}
diff --git a/src/app/(dashboard)/faqs/components/features-grid.tsx b/src/app/(dashboard)/faqs/components/features-grid.tsx
deleted file mode 100644
index 10b54ef..0000000
--- a/src/app/(dashboard)/faqs/components/features-grid.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Card, CardContent } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Button } from '@/components/ui/button'
-import { ArrowRight, Sparkles, Shield, Truck, Clock } from 'lucide-react'
-
-interface FeatureItem {
- id: number
- title: string
- description: string
- icon: string
-}
-
-interface FeaturesGridProps {
- features: FeatureItem[]
-}
-
-const iconMap = {
- Sparkles,
- Shield,
- Truck,
- Clock,
-}
-
-export function FeaturesGrid({ features }: FeaturesGridProps) {
- return (
-
- {features.map(feature => {
- const IconComponent = iconMap[feature.icon as keyof typeof iconMap]
- return (
-
-
-
-
-
-
- {feature.title}
- {feature.description}
-
-
- Learn more
-
-
-
-
-
- )
- })}
-
- )
-}
diff --git a/src/app/(dashboard)/faqs/data/categories.json b/src/app/(dashboard)/faqs/data/categories.json
deleted file mode 100644
index 7db5697..0000000
--- a/src/app/(dashboard)/faqs/data/categories.json
+++ /dev/null
@@ -1,10 +0,0 @@
-[
- { "name": "All", "count": 46 },
- { "name": "General", "count": 8 },
- { "name": "Account", "count": 6 },
- { "name": "Billing", "count": 8 },
- { "name": "Technical", "count": 9 },
- { "name": "Privacy", "count": 5 },
- { "name": "Security", "count": 4 },
- { "name": "Support", "count": 6 }
-]
diff --git a/src/app/(dashboard)/faqs/data/faqs.json b/src/app/(dashboard)/faqs/data/faqs.json
deleted file mode 100644
index 34f50f2..0000000
--- a/src/app/(dashboard)/faqs/data/faqs.json
+++ /dev/null
@@ -1,278 +0,0 @@
-[
- {
- "id": 1,
- "question": "What is ShadcnStore Admin?",
- "answer": "ShadcnStore Admin is a comprehensive admin dashboard template built with React, TypeScript, and shadcn/ui components. It provides a complete solution for managing your e-commerce store or business operations.",
- "category": "General"
- },
- {
- "id": 2,
- "question": "How do I get started?",
- "answer": "You can get started by signing up for an account, choosing a plan that fits your needs, and following our quick setup guide to configure your dashboard.",
- "category": "General"
- },
- {
- "id": 3,
- "question": "Do you offer a free trial?",
- "answer": "Yes, we offer a 14-day free trial for all new users. No credit card is required to start the trial, and you can explore all features during this period.",
- "category": "General"
- },
- {
- "id": 4,
- "question": "What browsers are supported?",
- "answer": "We support all modern browsers including Chrome, Firefox, Safari, and Edge. For the best experience, we recommend using the latest version of your preferred browser.",
- "category": "General"
- },
- {
- "id": 5,
- "question": "How do I contact support?",
- "answer": "You can contact our support team through the support page, by email at support@shadcnstore.com, or through the live chat feature available 24/7.",
- "category": "General"
- },
- {
- "id": 6,
- "question": "Is there a mobile app available?",
- "answer": "Currently, we offer a responsive web application that works great on mobile devices. A dedicated mobile app is planned for future release.",
- "category": "General"
- },
- {
- "id": 7,
- "question": "Can I customize the dashboard?",
- "answer": "Yes, the dashboard is highly customizable. You can modify themes, layouts, add custom components, and configure various settings to match your brand.",
- "category": "General"
- },
- {
- "id": 8,
- "question": "What integrations are available?",
- "answer": "We offer integrations with popular services like Stripe, PayPal, Shopify, WooCommerce, Google Analytics, and many more through our API.",
- "category": "General"
- },
- {
- "id": 9,
- "question": "How do I reset my password?",
- "answer": "You can reset your password by clicking on the 'Forgot Password' link on the login page. Enter your email address, and we'll send you instructions to reset your password.",
- "category": "Account"
- },
- {
- "id": 10,
- "question": "How do I change my email address?",
- "answer": "You can change your email address in your account settings under the 'User Settings' section. You'll need to verify the new email address before the change takes effect.",
- "category": "Account"
- },
- {
- "id": 11,
- "question": "Can I have multiple team members?",
- "answer": "Yes, depending on your plan, you can invite team members and assign different roles and permissions to manage your store collaboratively.",
- "category": "Account"
- },
- {
- "id": 12,
- "question": "How do I delete my account?",
- "answer": "To delete your account, go to your account settings and select 'Delete Account'. Please note that this action is irreversible and all data will be permanently removed.",
- "category": "Account"
- },
- {
- "id": 13,
- "question": "Can I change my username?",
- "answer": "Yes, you can change your username in the account settings. Keep in mind that some features might reference your old username temporarily.",
- "category": "Account"
- },
- {
- "id": 14,
- "question": "How do I enable two-factor authentication?",
- "answer": "You can enable two-factor authentication in your account security settings. We support both SMS and authenticator app methods for added security.",
- "category": "Account"
- },
- {
- "id": 15,
- "question": "What payment methods do you accept?",
- "answer": "We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for enterprise customers. All payments are processed securely.",
- "category": "Billing"
- },
- {
- "id": 16,
- "question": "How can I upgrade my plan?",
- "answer": "You can upgrade your plan at any time from your account settings. Go to 'Plans & Billing' and select the plan that best fits your needs. Changes take effect immediately.",
- "category": "Billing"
- },
- {
- "id": 17,
- "question": "Can I downgrade my plan?",
- "answer": "Yes, you can downgrade your plan at any time. The change will take effect at the start of your next billing cycle to ensure you don't lose access to premium features.",
- "category": "Billing"
- },
- {
- "id": 18,
- "question": "Do you offer refunds?",
- "answer": "We offer a 30-day money-back guarantee for all plans. If you're not satisfied, contact our support team for a full refund within 30 days of purchase.",
- "category": "Billing"
- },
- {
- "id": 19,
- "question": "How does billing work?",
- "answer": "Billing is processed monthly or annually depending on your chosen plan. You'll receive an invoice before each billing cycle, and payment is automatically charged to your selected method.",
- "category": "Billing"
- },
- {
- "id": 20,
- "question": "Can I change my billing cycle?",
- "answer": "Yes, you can switch between monthly and annual billing at any time. Annual billing offers significant savings compared to monthly billing.",
- "category": "Billing"
- },
- {
- "id": 21,
- "question": "What happens if payment fails?",
- "answer": "If a payment fails, we'll attempt to charge your card again after 3 days. You'll receive email notifications, and your account will remain active during this grace period.",
- "category": "Billing"
- },
- {
- "id": 22,
- "question": "How do I view my billing history?",
- "answer": "You can view your complete billing history in the 'Plans & Billing' section of your account settings. All invoices and receipts are available for download.",
- "category": "Billing"
- },
- {
- "id": 23,
- "question": "Can I export my data?",
- "answer": "Yes, you can export your data at any time from your account settings. We provide exports in multiple formats including CSV, JSON, and PDF for different data types.",
- "category": "Technical"
- },
- {
- "id": 24,
- "question": "What APIs do you provide?",
- "answer": "We provide comprehensive REST APIs for all major features including product management, order processing, customer data, and analytics. Full documentation is available.",
- "category": "Technical"
- },
- {
- "id": 25,
- "question": "How do I backup my data?",
- "answer": "We automatically backup all your data daily. You can also create manual backups anytime from your settings, and restore from any backup point within the last 30 days.",
- "category": "Technical"
- },
- {
- "id": 26,
- "question": "Is there a rate limit on API calls?",
- "answer": "Yes, API rate limits vary by plan. Basic plans have 1000 calls/hour, Professional plans have 10,000 calls/hour, and Enterprise plans have unlimited calls.",
- "category": "Technical"
- },
- {
- "id": 27,
- "question": "How do I set up webhooks?",
- "answer": "Webhooks can be configured in the 'Connections' section of your settings. You can set up webhooks for various events like new orders, payment confirmations, and inventory updates.",
- "category": "Technical"
- },
- {
- "id": 28,
- "question": "What about system maintenance?",
- "answer": "We perform maintenance during low-traffic hours (typically Sunday 2-4 AM UTC). You'll be notified at least 48 hours in advance of any scheduled maintenance.",
- "category": "Technical"
- },
- {
- "id": 29,
- "question": "How do I troubleshoot connection issues?",
- "answer": "First, check your internet connection and try refreshing the page. If issues persist, check our status page or contact support with specific error messages.",
- "category": "Technical"
- },
- {
- "id": 30,
- "question": "Can I use custom domains?",
- "answer": "Yes, Professional and Enterprise plans support custom domains. You can configure your custom domain in the 'Connections' section of your account settings.",
- "category": "Technical"
- },
- {
- "id": 31,
- "question": "What databases do you support?",
- "answer": "We support integration with MySQL, PostgreSQL, MongoDB, and other popular databases through our Database Sync feature available in higher-tier plans.",
- "category": "Technical"
- },
- {
- "id": 32,
- "question": "How do you handle my personal data?",
- "answer": "We follow strict data protection policies and comply with GDPR, CCPA, and other privacy regulations. Your personal data is never shared with third parties without your consent.",
- "category": "Privacy"
- },
- {
- "id": 33,
- "question": "Can I request my data?",
- "answer": "Yes, you can request a complete copy of your personal data at any time. We'll provide it in a machine-readable format within 30 days of your request.",
- "category": "Privacy"
- },
- {
- "id": 34,
- "question": "How long do you retain data?",
- "answer": "We retain your data as long as your account is active. After account deletion, personal data is removed within 30 days, though some anonymized analytics may be retained.",
- "category": "Privacy"
- },
- {
- "id": 35,
- "question": "Do you use cookies?",
- "answer": "Yes, we use essential cookies for functionality and optional cookies for analytics and personalization. You can manage your cookie preferences in your account settings.",
- "category": "Privacy"
- },
- {
- "id": 36,
- "question": "Is my data encrypted?",
- "answer": "Yes, all data is encrypted both in transit (using TLS 1.3) and at rest (using AES-256 encryption). We use industry-standard security practices to protect your information.",
- "category": "Privacy"
- },
- {
- "id": 37,
- "question": "How secure is my data?",
- "answer": "We implement bank-level security with end-to-end encryption, regular security audits, and compliance with SOC 2 Type II standards. Your data security is our top priority.",
- "category": "Security"
- },
- {
- "id": 38,
- "question": "Do you support SSO?",
- "answer": "Yes, Enterprise plans include Single Sign-On (SSO) support with popular providers like Google, Microsoft Azure AD, and Okta for seamless team access.",
- "category": "Security"
- },
- {
- "id": 39,
- "question": "What about password requirements?",
- "answer": "We require strong passwords with at least 8 characters, including uppercase, lowercase, numbers, and special characters. We also highly recommend enabling two-factor authentication.",
- "category": "Security"
- },
- {
- "id": 40,
- "question": "How do you handle security incidents?",
- "answer": "We have a comprehensive incident response plan. In case of any security issues, we immediately investigate, contain the issue, and notify affected users within 24 hours.",
- "category": "Security"
- },
- {
- "id": 41,
- "question": "What support channels are available?",
- "answer": "We offer email support, live chat, and phone support (for Enterprise customers). Our knowledge base and community forums are also available 24/7.",
- "category": "Support"
- },
- {
- "id": 42,
- "question": "What are your support hours?",
- "answer": "Email and chat support are available 24/7. Phone support for Enterprise customers is available Monday-Friday, 9 AM-6 PM in your local timezone.",
- "category": "Support"
- },
- {
- "id": 43,
- "question": "How quickly will I get a response?",
- "answer": "Response times vary by plan: Basic (24 hours), Professional (12 hours), Enterprise (2 hours). Critical issues are prioritized and responded to immediately.",
- "category": "Support"
- },
- {
- "id": 44,
- "question": "Do you offer training?",
- "answer": "Yes, we provide comprehensive onboarding for all plans, video tutorials, documentation, and personalized training sessions for Enterprise customers.",
- "category": "Support"
- },
- {
- "id": 45,
- "question": "Can you help with custom implementations?",
- "answer": "Enterprise customers get access to our professional services team for custom implementations, integrations, and consulting services.",
- "category": "Support"
- },
- {
- "id": 46,
- "question": "Is there a community forum?",
- "answer": "Yes, we have an active community forum where users share tips, ask questions, and get help from both our team and other community members.",
- "category": "Support"
- }
-]
diff --git a/src/app/(dashboard)/faqs/data/features.json b/src/app/(dashboard)/faqs/data/features.json
deleted file mode 100644
index 0176042..0000000
--- a/src/app/(dashboard)/faqs/data/features.json
+++ /dev/null
@@ -1,26 +0,0 @@
-[
- {
- "id": 1,
- "title": "Premium Quality",
- "description": "Handcrafted with premium materials and meticulous attention to detail.",
- "icon": "Sparkles"
- },
- {
- "id": 2,
- "title": "Secure Shopping",
- "description": "100% secure payment processing with end-to-end encryption.",
- "icon": "Shield"
- },
- {
- "id": 3,
- "title": "Fast Delivery",
- "description": "Free worldwide shipping and hassle-free returns within 30 days.",
- "icon": "Truck"
- },
- {
- "id": 4,
- "title": "24/7 Support",
- "description": "Round-the-clock customer support to assist you anytime.",
- "icon": "Clock"
- }
-]
diff --git a/src/app/(dashboard)/faqs/page.tsx b/src/app/(dashboard)/faqs/page.tsx
deleted file mode 100644
index 02e1179..0000000
--- a/src/app/(dashboard)/faqs/page.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { FAQList } from "./components/faq-list"
-import { FeaturesGrid } from "./components/features-grid"
-
-// Import data
-import categoriesData from "./data/categories.json"
-import faqsData from "./data/faqs.json"
-import featuresData from "./data/features.json"
-
-export default function FAQsPage() {
- return (
-
-
-
-
- )
-}
diff --git a/src/app/(dashboard)/finance/banks/components/bank-form-sheet.tsx b/src/app/(dashboard)/finance/banks/components/bank-form-sheet.tsx
deleted file mode 100644
index 28cec34..0000000
--- a/src/app/(dashboard)/finance/banks/components/bank-form-sheet.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-"use client";
-
-import { useActionState, useEffect } from "react";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import {
- createBankAccountAction,
- updateBankAccountAction,
-} from "@/lib/appwrite/bank-account-actions";
-import { initialBankAccountState } from "@/lib/appwrite/bank-account-types";
-import { ScopeToggle } from "@/components/finance/scope-toggle";
-
-import type { BankAccountRow } from "./types";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- account?: BankAccountRow | null;
-};
-
-export function BankFormSheet({ open, onOpenChange, account }: Props) {
- const isEdit = Boolean(account);
- const action = isEdit ? updateBankAccountAction : createBankAccountAction;
- const [state, formAction, isPending] = useActionState(action, initialBankAccountState);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Hesap güncellendi." : "Hesap eklendi.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- return (
-
-
-
- {isEdit ? "Hesabı düzenle" : "Yeni banka hesabı"}
-
- Açılış bakiyesi sonradan değiştirilirse bütün hareketler aynı kalır, sadece toplam
- kayar.
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/banks/components/banks-client.tsx b/src/app/(dashboard)/finance/banks/components/banks-client.tsx
deleted file mode 100644
index 963b0a4..0000000
--- a/src/app/(dashboard)/finance/banks/components/banks-client.tsx
+++ /dev/null
@@ -1,294 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import {
- Archive,
- ArchiveRestore,
- Building2,
- Loader2,
- MoreHorizontal,
- Pencil,
- Plus,
- Trash2,
-} from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import {
- archiveBankAccountAction,
- deleteBankAccountAction,
-} from "@/lib/appwrite/bank-account-actions";
-import { formatTRY } from "@/lib/format";
-import { ScopeBadge } from "@/components/finance/scope-toggle";
-import { cn } from "@/lib/utils";
-
-import { BankFormSheet } from "./bank-form-sheet";
-import type { BankAccountRow } from "./types";
-
-type Props = { accounts: BankAccountRow[] };
-
-export function BanksClient({ accounts }: Props) {
- const [formOpen, setFormOpen] = useState(false);
- const [editing, setEditing] = useState
(null);
- const [deleting, setDeleting] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const active = accounts.filter((a) => !a.archived);
- const archived = accounts.filter((a) => a.archived);
- const totalBalance = active.reduce((s, a) => s + a.balance, 0);
-
- const toggleArchive = (acc: BankAccountRow) => {
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", acc.id);
- const result = await archiveBankAccountAction(fd);
- if (result.ok) {
- toast.success(acc.archived ? "Hesap geri açıldı." : "Hesap arşivlendi.");
- } else {
- toast.error(result.error ?? "İşlem başarısız.");
- }
- });
- };
-
- const handleDelete = () => {
- if (!deleting) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deleting.id);
- const result = await deleteBankAccountAction(fd);
- if (result.ok) {
- toast.success("Hesap silindi.");
- setDeleting(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- return (
-
-
-
-
- Toplam bakiye (aktif hesaplar)
- = 0 ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400",
- )}
- >
- {formatTRY(totalBalance)}
-
-
-
-
{
- setEditing(null);
- setFormOpen(true);
- }}
- >
-
- Yeni hesap
-
-
-
- {active.length === 0 && archived.length === 0 ? (
-
-
-
- Henüz banka hesabı eklenmemiş.
- {
- setEditing(null);
- setFormOpen(true);
- }}
- >
-
- İlk hesabı ekle
-
-
-
- ) : (
- <>
-
- {active.map((a) => (
-
{
- setEditing(a);
- setFormOpen(true);
- }}
- onArchiveToggle={() => toggleArchive(a)}
- onDelete={() => setDeleting(a)}
- busy={busy}
- />
- ))}
-
-
- {archived.length > 0 && (
-
-
- Arşivlenmiş hesaplar ({archived.length})
-
-
- {archived.map((a) => (
-
{
- setEditing(a);
- setFormOpen(true);
- }}
- onArchiveToggle={() => toggleArchive(a)}
- onDelete={() => setDeleting(a)}
- busy={busy}
- />
- ))}
-
-
- )}
- >
- )}
-
-
{
- setFormOpen(v);
- if (!v) setEditing(null);
- }}
- account={editing}
- />
-
- !v && setDeleting(null)}>
-
-
- Hesabı sil
-
- {deleting?.bankName} — {deleting?.accountName} kalıcı olarak silinecek.
- Bağlı finans hareketi varsa silme reddedilir; o durumda arşivlemeyi tercih edin.
-
-
-
- setDeleting(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
-
- );
-}
-
-function AccountCard({
- account,
- onEdit,
- onArchiveToggle,
- onDelete,
- busy,
-}: {
- account: BankAccountRow;
- onEdit: () => void;
- onArchiveToggle: () => void;
- onDelete: () => void;
- busy: boolean;
-}) {
- const positive = account.balance >= 0;
- return (
-
-
-
-
-
-
-
{account.bankName}
- {account.archived && (
-
- Arşivli
-
- )}
-
-
-
{account.accountName}
- {account.iban && (
-
- {account.iban.replace(/(.{4})/g, "$1 ").trim()}
-
- )}
-
-
-
-
-
-
-
-
-
-
- Düzenle
-
-
- {account.archived ? (
- <>
-
- Arşivden çıkar
- >
- ) : (
- <>
-
- Arşivle
- >
- )}
-
-
-
-
- Sil
-
-
-
-
-
-
-
Güncel bakiye
-
- {formatTRY(account.balance)}
-
- {account.balance !== account.openingBalance && (
-
- Açılış: {formatTRY(account.openingBalance)}
-
- )}
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/banks/components/types.ts b/src/app/(dashboard)/finance/banks/components/types.ts
deleted file mode 100644
index 0495794..0000000
--- a/src/app/(dashboard)/finance/banks/components/types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export type BankAccountRow = {
- id: string;
- bankName: string;
- accountName: string;
- iban: string;
- openingBalance: number;
- notes: string;
- archived: boolean;
- balance: number;
- scope: "company" | "personal";
-};
diff --git a/src/app/(dashboard)/finance/banks/page.tsx b/src/app/(dashboard)/finance/banks/page.tsx
deleted file mode 100644
index d1c4243..0000000
--- a/src/app/(dashboard)/finance/banks/page.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import {
- getBankAccountBalances,
- listBankAccounts,
-} from "@/lib/appwrite/bank-account-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { BanksClient } from "./components/banks-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Banka hesapları",
-};
-
-export default async function BanksPage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const [accounts, balances] = await Promise.all([
- listBankAccounts(ctx.tenantId, ctx.user.id),
- getBankAccountBalances(ctx.tenantId, ctx.user.id),
- ]);
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Banka hesapları
-
- İşletmenize ait banka hesaplarını ve güncel bakiyelerini takip edin.
-
-
-
-
({
- id: a.$id,
- bankName: a.bankName,
- accountName: a.accountName,
- iban: a.iban ?? "",
- openingBalance: a.openingBalance ?? 0,
- notes: a.notes ?? "",
- archived: Boolean(a.archived),
- balance: balances.get(a.$id) ?? a.openingBalance ?? 0,
- scope: (a.scope ?? "company") as "company" | "personal",
- }))}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/finance/cards/components/card-form-sheet.tsx b/src/app/(dashboard)/finance/cards/components/card-form-sheet.tsx
deleted file mode 100644
index 86d6385..0000000
--- a/src/app/(dashboard)/finance/cards/components/card-form-sheet.tsx
+++ /dev/null
@@ -1,228 +0,0 @@
-"use client";
-
-import { useActionState, useEffect } from "react";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import {
- createCreditCardAction,
- updateCreditCardAction,
-} from "@/lib/appwrite/credit-card-actions";
-import { initialCreditCardState } from "@/lib/appwrite/credit-card-types";
-import { ScopeToggle } from "@/components/finance/scope-toggle";
-
-import type { BankAccountOption, CreditCardRow } from "./types";
-
-const NONE = "__none__";
-
-export function CardFormSheet({
- open,
- onOpenChange,
- card,
- bankAccounts,
-}: {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- card?: CreditCardRow | null;
- bankAccounts: BankAccountOption[];
-}) {
- const isEdit = Boolean(card);
- const action = isEdit ? updateCreditCardAction : createCreditCardAction;
- const [state, formAction, isPending] = useActionState(action, initialCreditCardState);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Kart güncellendi." : "Kart eklendi.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- return (
-
-
-
- {isEdit ? "Kartı düzenle" : "Yeni kredi kartı"}
-
- Hesap kesim ve son ödeme günleri her ay otomatik kullanılır. Ekstreler kart başına manuel girilir.
-
-
-
- {
- if (fd.get("bankAccountId") === NONE) fd.set("bankAccountId", "");
- formAction(fd);
- }}
- className="flex flex-1 flex-col"
- >
- {isEdit && card && }
-
-
-
-
-
-
-
-
-
-
-
-
Bağlı hesap
-
-
-
-
-
- Yok
- {bankAccounts.map((b) => (
-
- {b.label}
-
- ))}
-
-
-
- Ekstre ödemeleri seçilen hesaba expense olarak yazılır.
-
-
-
-
- Notlar
-
-
-
-
-
-
- onOpenChange(false)} disabled={isPending}>
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Kaydet"}
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/cards/components/cards-client.tsx b/src/app/(dashboard)/finance/cards/components/cards-client.tsx
deleted file mode 100644
index b8bf653..0000000
--- a/src/app/(dashboard)/finance/cards/components/cards-client.tsx
+++ /dev/null
@@ -1,522 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import {
- Archive,
- ArchiveRestore,
- Check,
- CreditCard as CreditCardIcon,
- Loader2,
- MoreHorizontal,
- Pencil,
- Plus,
- Trash2,
-} from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Input } from "@/components/ui/input";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- archiveCreditCardAction,
- deleteCreditCardAction,
- deleteStatementAction,
- payStatementAction,
-} from "@/lib/appwrite/credit-card-actions";
-import { formatDate, formatTRY } from "@/lib/format";
-import { cn } from "@/lib/utils";
-
-import { CardFormSheet } from "./card-form-sheet";
-import { StatementFormSheet } from "./statement-form-sheet";
-import {
- type BankAccountOption,
- type CreditCardRow,
- STATEMENT_STATUS_COLOR,
- STATEMENT_STATUS_LABEL,
- type StatementRow,
-} from "./types";
-
-type Props = {
- cards: CreditCardRow[];
- statements: StatementRow[];
- bankAccounts: BankAccountOption[];
-};
-
-export function CardsClient({ cards, statements, bankAccounts }: Props) {
- const [cardFormOpen, setCardFormOpen] = useState(false);
- const [editingCard, setEditingCard] = useState(null);
- const [deletingCard, setDeletingCard] = useState(null);
- const [stmtFormOpen, setStmtFormOpen] = useState(false);
- const [stmtCard, setStmtCard] = useState(null);
- const [payDialog, setPayDialog] = useState(null);
- const [payAmount, setPayAmount] = useState("");
- const [deletingStmt, setDeletingStmt] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const active = cards.filter((c) => !c.archived);
- const archived = cards.filter((c) => c.archived);
-
- const stmtsByCard = new Map();
- for (const s of statements) {
- const arr = stmtsByCard.get(s.cardId) ?? [];
- arr.push(s);
- stmtsByCard.set(s.cardId, arr);
- }
-
- const totalOutstanding = statements
- .filter((s) => s.status !== "paid")
- .reduce((sum, s) => sum + (s.totalDebt - s.paidAmount), 0);
-
- const overdueCount = statements.filter((s) => s.status === "overdue").length;
-
- const toggleArchive = (c: CreditCardRow) => {
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", c.id);
- const r = await archiveCreditCardAction(fd);
- if (r.ok) toast.success(c.archived ? "Kart geri açıldı." : "Kart arşivlendi.");
- else toast.error(r.error ?? "İşlem başarısız.");
- });
- };
-
- const handleDeleteCard = () => {
- if (!deletingCard) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deletingCard.id);
- const r = await deleteCreditCardAction(fd);
- if (r.ok) {
- toast.success("Kart silindi.");
- setDeletingCard(null);
- } else {
- toast.error(r.error ?? "Silme başarısız.");
- }
- });
- };
-
- const handlePay = () => {
- if (!payDialog) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", payDialog.id);
- if (payAmount.trim()) fd.set("amount", payAmount);
- const r = await payStatementAction(fd);
- if (r.ok) {
- toast.success("Ödeme kaydedildi.");
- setPayDialog(null);
- setPayAmount("");
- } else {
- toast.error(r.error ?? "Ödeme başarısız.");
- }
- });
- };
-
- const handleDeleteStmt = () => {
- if (!deletingStmt) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deletingStmt.id);
- const r = await deleteStatementAction(fd);
- if (r.ok) {
- toast.success("Ekstre silindi.");
- setDeletingStmt(null);
- } else {
- toast.error(r.error ?? "Silme başarısız.");
- }
- });
- };
-
- return (
-
-
-
-
- Aktif kart
- {active.length}
-
-
-
-
- Bekleyen toplam borç
-
- {formatTRY(totalOutstanding)}
-
-
-
-
-
- Vadesi geçmiş ekstre
- 0 && "text-red-600 dark:text-red-400",
- )}
- >
- {overdueCount}
-
-
-
-
-
-
-
{
- setStmtCard(null);
- setStmtFormOpen(true);
- }}
- disabled={cards.length === 0}
- >
-
- Yeni ekstre
-
-
{
- setEditingCard(null);
- setCardFormOpen(true);
- }}
- >
-
- Yeni kart
-
-
-
- {cards.length === 0 ? (
-
-
-
- Henüz kredi kartı eklenmemiş.
- setCardFormOpen(true)}>
-
- İlk kartı ekle
-
-
-
- ) : (
-
- {active.map((c) => {
- const items = stmtsByCard.get(c.id) ?? [];
- const totalDebt = items
- .filter((s) => s.status !== "paid")
- .reduce((sum, s) => sum + (s.totalDebt - s.paidAmount), 0);
- return (
-
-
-
-
-
-
-
{c.bankName}
- ·
- {c.cardName}
- {c.last4 && (
-
- **{c.last4}
-
- )}
-
-
- Limit {formatTRY(c.creditLimit)}
- Kesim: ayın {c.statementDay}'i
- Vade: ayın {c.dueDay}'i
- Aylık faiz: %{c.interestRate}
-
- {c.bankAccountLabel && (
-
- Hesap: {c.bankAccountLabel}
-
- )}
-
-
-
-
Bekleyen
-
{formatTRY(totalDebt)}
-
-
-
-
-
-
-
-
- {
- setStmtCard(c);
- setStmtFormOpen(true);
- }}
- >
-
- Ekstre ekle
-
-
- {
- setEditingCard(c);
- setCardFormOpen(true);
- }}
- >
-
- Düzenle
-
- toggleArchive(c)}>
-
- Arşivle
-
-
- setDeletingCard(c)}
- >
-
- Sil
-
-
-
-
-
-
- {items.length > 0 && (
-
-
-
-
- Dönem
- Son ödeme
- Toplam
- Asgari
- Ödenen
- Durum
- İşlem
-
-
-
- {items.map((s) => {
- const remaining = s.totalDebt - s.paidAmount;
- return (
-
- {s.period}
-
- {formatDate(s.dueDate)}
-
-
- {formatTRY(s.totalDebt)}
-
-
- {formatTRY(s.minimumPayment)}
-
-
- {formatTRY(s.paidAmount)}
-
-
-
- {STATEMENT_STATUS_LABEL[s.status]}
-
-
-
-
- {remaining > 0 && (
- {
- setPayDialog(s);
- setPayAmount(remaining.toFixed(2));
- }}
- >
-
- Öde
-
- )}
- setDeletingStmt(s)}
- >
-
-
-
-
-
- );
- })}
-
-
-
- )}
-
-
- );
- })}
-
- {archived.length > 0 && (
-
-
- Arşivlenmiş kartlar ({archived.length})
-
-
- {archived.map((c) => (
-
-
-
-
- {c.bankName} — {c.cardName}{" "}
- {c.last4 && (
-
- **{c.last4}
-
- )}
-
-
Arşivli
-
- toggleArchive(c)}>
-
- Geri aç
-
-
-
- ))}
-
-
- )}
-
- )}
-
-
{
- setCardFormOpen(v);
- if (!v) setEditingCard(null);
- }}
- card={editingCard}
- bankAccounts={bankAccounts}
- />
-
- {
- setStmtFormOpen(v);
- if (!v) setStmtCard(null);
- }}
- card={stmtCard}
- cards={cards}
- />
-
- !v && setDeletingCard(null)}>
-
-
- Kartı sil
-
-
- {deletingCard?.bankName} — {deletingCard?.cardName}
- {" "}
- ve tüm ekstreleri silinecek.
-
-
-
- setDeletingCard(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
-
- {
- if (!v) {
- setPayDialog(null);
- setPayAmount("");
- }
- }}
- >
-
-
- Ekstre ödemesi
-
- {payDialog && (
- <>
- {payDialog.period} dönemi — kalan{" "}
- {formatTRY(payDialog.totalDebt - payDialog.paidAmount)}.
-
- Tutarı boş bırakırsanız tamamı ödenir.
- >
- )}
-
-
-
- Ödenen tutar (₺)
- setPayAmount(e.target.value)}
- />
-
-
- setPayDialog(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Ödemeyi kaydet
-
-
-
-
-
- !v && setDeletingStmt(null)}>
-
-
- Ekstreyi sil
-
- {deletingStmt?.period} ekstresi silinecek. Bağlı gider kaydı varsa
- o da silinir.
-
-
-
- setDeletingStmt(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/cards/components/statement-form-sheet.tsx b/src/app/(dashboard)/finance/cards/components/statement-form-sheet.tsx
deleted file mode 100644
index 81f166f..0000000
--- a/src/app/(dashboard)/finance/cards/components/statement-form-sheet.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useMemo } from "react";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import { createStatementAction } from "@/lib/appwrite/credit-card-actions";
-import { initialCreditCardState } from "@/lib/appwrite/credit-card-types";
-
-import type { CreditCardRow } from "./types";
-
-function pad(n: number) {
- return String(n).padStart(2, "0");
-}
-
-function defaultDates(card?: CreditCardRow | null) {
- const now = new Date();
- const sd = card?.statementDay ?? 1;
- const dd = card?.dueDay ?? 10;
- const statement = new Date(now.getFullYear(), now.getMonth(), Math.min(sd, 28));
- const due = new Date(now.getFullYear(), now.getMonth(), Math.min(dd, 28));
- if (due.getTime() < statement.getTime()) due.setMonth(due.getMonth() + 1);
- const period = `${statement.getFullYear()}-${pad(statement.getMonth() + 1)}`;
- const ymd = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- return { period, statementDate: ymd(statement), dueDate: ymd(due) };
-}
-
-export function StatementFormSheet({
- open,
- onOpenChange,
- card,
- cards,
-}: {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- card?: CreditCardRow | null;
- cards: CreditCardRow[];
-}) {
- const [state, formAction, isPending] = useActionState(
- createStatementAction,
- initialCreditCardState,
- );
- const defaults = useMemo(() => defaultDates(card), [card]);
-
- useEffect(() => {
- if (state.ok) {
- toast.success("Ekstre kaydedildi.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- return (
-
-
-
- Yeni ekstre
-
- Banka ekstrenizdeki dönem, son ödeme tarihi, toplam borç ve asgari ödeme tutarını girin.
-
-
-
-
-
-
-
Kart *
-
-
-
-
-
- {cards.map((c) => (
-
- {c.bankName} — {c.cardName} {c.last4 ? `**${c.last4}` : ""}
-
- ))}
-
-
- {state.fieldErrors?.cardId && (
-
{state.fieldErrors.cardId}
- )}
-
-
-
-
-
-
-
Toplam borç (₺) *
-
- {state.fieldErrors?.totalDebt && (
-
{state.fieldErrors.totalDebt}
- )}
-
-
- Asgari ödeme (₺)
-
-
-
-
-
- Notlar
-
-
-
-
-
-
- onOpenChange(false)} disabled={isPending}>
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- Kaydet
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/cards/components/types.ts b/src/app/(dashboard)/finance/cards/components/types.ts
deleted file mode 100644
index 596a377..0000000
--- a/src/app/(dashboard)/finance/cards/components/types.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-export type CreditCardRow = {
- id: string;
- bankName: string;
- cardName: string;
- last4: string;
- creditLimit: number;
- statementDay: number;
- dueDay: number;
- interestRate: number;
- bankAccountId: string;
- bankAccountLabel: string;
- archived: boolean;
- notes: string;
- scope: "company" | "personal";
-};
-
-export type StatementRow = {
- id: string;
- cardId: string;
- period: string; // YYYY-MM
- statementDate: string;
- dueDate: string;
- totalDebt: number;
- minimumPayment: number;
- paidAmount: number;
- status: "pending" | "partial" | "paid" | "overdue";
- notes: string;
-};
-
-export type BankAccountOption = { id: string; label: string };
-
-export const STATEMENT_STATUS_LABEL: Record = {
- pending: "Bekliyor",
- partial: "Kısmi ödendi",
- paid: "Ödendi",
- overdue: "Gecikti",
-};
-
-export const STATEMENT_STATUS_COLOR: Record = {
- pending: "bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/30",
- partial: "bg-amber-500/15 text-amber-800 dark:text-amber-300 border-amber-500/30",
- paid: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-500/30",
- overdue: "bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/30",
-};
diff --git a/src/app/(dashboard)/finance/cards/page.tsx b/src/app/(dashboard)/finance/cards/page.tsx
deleted file mode 100644
index 8178c85..0000000
--- a/src/app/(dashboard)/finance/cards/page.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listBankAccounts } from "@/lib/appwrite/bank-account-queries";
-import {
- listCreditCards,
- listStatements,
-} from "@/lib/appwrite/credit-card-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { CardsClient } from "./components/cards-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Kredi kartları",
-};
-
-export default async function CardsPage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const [cards, statements, bankAccounts] = await Promise.all([
- listCreditCards(ctx.tenantId, ctx.user.id),
- listStatements(ctx.tenantId, ctx.user.id),
- listBankAccounts(ctx.tenantId, ctx.user.id),
- ]);
-
- const bankMap = new Map(
- bankAccounts.map((b) => [b.$id, `${b.bankName} — ${b.accountName}`]),
- );
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Kredi kartları
-
- Kartlarınızı ve aylık ekstrelerinizi takip edin. Ekstre ödendiğinde otomatik gider kaydı oluşur.
-
-
-
-
({
- id: c.$id,
- bankName: c.bankName,
- cardName: c.cardName,
- last4: c.last4 ?? "",
- creditLimit: c.creditLimit ?? 0,
- statementDay: c.statementDay ?? 1,
- dueDay: c.dueDay ?? 10,
- interestRate: c.interestRate ?? 4.25,
- bankAccountId: c.bankAccountId ?? "",
- bankAccountLabel: c.bankAccountId ? bankMap.get(c.bankAccountId) ?? "" : "",
- archived: Boolean(c.archived),
- notes: c.notes ?? "",
- scope: (c.scope ?? "company") as "company" | "personal",
- }))}
- statements={statements.map((s) => ({
- id: s.$id,
- cardId: s.cardId,
- period: s.period,
- statementDate: s.statementDate,
- dueDate: s.dueDate,
- totalDebt: s.totalDebt,
- minimumPayment: s.minimumPayment ?? 0,
- paidAmount: s.paidAmount ?? 0,
- status: s.status ?? "pending",
- notes: s.notes ?? "",
- }))}
- bankAccounts={bankAccounts
- .filter((b) => !b.archived)
- .map((b) => ({
- id: b.$id,
- label: `${b.bankName} — ${b.accountName}`,
- }))}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/finance/components/finance-client.tsx b/src/app/(dashboard)/finance/components/finance-client.tsx
deleted file mode 100644
index cedaef5..0000000
--- a/src/app/(dashboard)/finance/components/finance-client.tsx
+++ /dev/null
@@ -1,436 +0,0 @@
-"use client";
-
-import { useMemo, useState, useTransition } from "react";
-import {
- type ColumnDef,
- flexRender,
- getCoreRowModel,
- getFilteredRowModel,
- getPaginationRowModel,
- getSortedRowModel,
- useReactTable,
- type SortingState,
-} from "@tanstack/react-table";
-import {
- ArrowDownCircle,
- ArrowUpCircle,
- CircleAlert,
- CircleDollarSign,
- Loader2,
- MoreHorizontal,
- Pencil,
- Plus,
- Search,
- Trash2,
- Wallet,
-} from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Input } from "@/components/ui/input";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { deleteFinanceEntryAction } from "@/lib/appwrite/finance-actions";
-import { formatDate, formatTRY } from "@/lib/format";
-import { cn } from "@/lib/utils";
-
-import { FinanceFormSheet } from "./finance-form-sheet";
-import {
- type BankAccountOption,
- type Customer,
- type FinanceRow,
- type FinanceType,
- PAYMENT_METHOD_LABEL,
- TYPE_COLOR,
- TYPE_LABEL,
-} from "./types";
-
-type Props = {
- entries: FinanceRow[];
- customers: Customer[];
- bankAccounts: BankAccountOption[];
-};
-
-function StatCard({
- label,
- amount,
- icon: Icon,
- tone,
-}: {
- label: string;
- amount: number;
- icon: typeof Wallet;
- tone: "income" | "expense" | "receivable" | "debt" | "net";
-}) {
- const toneClass = {
- income: "text-emerald-600 dark:text-emerald-400",
- expense: "text-red-600 dark:text-red-400",
- receivable: "text-blue-600 dark:text-blue-400",
- debt: "text-amber-600 dark:text-amber-400",
- net: amount >= 0 ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400",
- }[tone];
- return (
-
-
-
-
{label}
-
{formatTRY(amount)}
-
-
-
-
- );
-}
-
-export function FinanceClient({ entries, customers, bankAccounts }: Props) {
- const [tab, setTab] = useState("all");
- const [search, setSearch] = useState("");
- const [sorting, setSorting] = useState([]);
- const [formOpen, setFormOpen] = useState(false);
- const [editing, setEditing] = useState(null);
- const [defaultType, setDefaultType] = useState("income");
- const [deleting, setDeleting] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const stats = useMemo(() => {
- let income = 0,
- expense = 0,
- receivable = 0,
- debt = 0;
- for (const e of entries) {
- if (e.type === "income") income += e.amount;
- else if (e.type === "expense") expense += e.amount;
- else if (e.type === "receivable") receivable += e.amount;
- else if (e.type === "debt") debt += e.amount;
- }
- return { income, expense, receivable, debt, net: income - expense };
- }, [entries]);
-
- const filtered = useMemo(
- () => (tab === "all" ? entries : entries.filter((e) => e.type === tab)),
- [entries, tab],
- );
-
- const columns = useMemo[]>(
- () => [
- {
- accessorKey: "type",
- header: "Tür",
- cell: ({ row }) => (
-
- {TYPE_LABEL[row.original.type]}
-
- ),
- },
- {
- accessorKey: "amount",
- header: "Tutar",
- cell: ({ row }) => {
- const sign =
- row.original.type === "income" || row.original.type === "receivable" ? "+" : "−";
- return (
-
- {sign} {formatTRY(row.original.amount)}
-
- );
- },
- },
- {
- accessorKey: "date",
- header: "Tarih",
- cell: ({ row }) => (
- {formatDate(row.original.date)}
- ),
- },
- {
- accessorKey: "customerName",
- header: "Müşteri",
- cell: ({ row }) =>
- row.original.customerName ? (
- {row.original.customerName}
- ) : (
- —
- ),
- },
- {
- accessorKey: "paymentMethod",
- header: "Ödeme",
- cell: ({ row }) => (
-
- {PAYMENT_METHOD_LABEL[row.original.paymentMethod]}
-
- ),
- },
- {
- accessorKey: "description",
- header: "Açıklama",
- cell: ({ row }) => (
-
-
- {row.original.description || "—"}
-
- {row.original.invoiceId && (
-
- Faturadan
-
- )}
-
- ),
- },
- {
- id: "actions",
- cell: ({ row }) => (
-
-
-
-
-
-
-
-
- {
- setEditing(row.original);
- setFormOpen(true);
- }}
- >
-
- Düzenle
-
- setDeleting(row.original)}
- >
-
- Sil
-
-
-
-
- ),
- },
- ],
- [],
- );
-
- const table = useReactTable({
- data: filtered,
- columns,
- state: { globalFilter: search, sorting },
- onGlobalFilterChange: setSearch,
- onSortingChange: setSorting,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getSortedRowModel: getSortedRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- initialState: { pagination: { pageSize: 25 } },
- globalFilterFn: (row, _id, fv) => {
- const v = String(fv).toLowerCase();
- return [row.original.description, row.original.customerName, row.original.amount.toString()]
- .join(" ")
- .toLowerCase()
- .includes(v);
- },
- });
-
- const handleDelete = () => {
- if (!deleting) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deleting.id);
- const result = await deleteFinanceEntryAction(fd);
- if (result.ok) {
- toast.success("Kayıt silindi.");
- setDeleting(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- const openCreate = (type: FinanceType) => {
- setEditing(null);
- setDefaultType(type);
- setFormOpen(true);
- };
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
setTab(v as typeof tab)}>
-
-
-
-
- Tümü
- Gelir
- Gider
- Alacaklar
- Borçlar
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Açıklama, müşteri, tutar..."
- className="pl-9"
- />
-
-
-
-
openCreate("income")}>
-
- Gelir
-
-
openCreate("expense")}>
-
- Gider
-
-
openCreate("receivable")}>
-
- Alacak
-
-
openCreate("debt")}>
-
- Borç
-
-
-
-
-
-
- {table.getHeaderGroups().map((hg) => (
-
- {hg.headers.map((h) => (
-
- {h.isPlaceholder
- ? null
- : flexRender(h.column.columnDef.header, h.getContext())}
-
- ))}
-
- ))}
-
-
- {table.getRowModel().rows.length ? (
- table.getRowModel().rows.map((r) => (
-
- {r.getVisibleCells().map((c) => (
-
- {flexRender(c.column.columnDef.cell, c.getContext())}
-
- ))}
-
- ))
- ) : (
-
-
- Kayıt yok.
-
-
- )}
-
-
-
-
-
- {
- setFormOpen(v);
- if (!v) setEditing(null);
- }}
- entry={editing}
- defaultType={defaultType}
- customers={customers}
- bankAccounts={bankAccounts}
- onRequestDelete={(e) => {
- setFormOpen(false);
- setDeleting(e);
- }}
- />
-
- !v && setDeleting(null)}>
-
-
- Kaydı sil
-
- {deleting && (
- <>
- {TYPE_LABEL[deleting.type]} — {formatTRY(deleting.amount)} (
- {formatDate(deleting.date)}) silinecek.
- >
- )}
-
-
-
- setDeleting(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
- >
- );
-}
diff --git a/src/app/(dashboard)/finance/components/finance-form-sheet.tsx b/src/app/(dashboard)/finance/components/finance-form-sheet.tsx
deleted file mode 100644
index ed7d806..0000000
--- a/src/app/(dashboard)/finance/components/finance-form-sheet.tsx
+++ /dev/null
@@ -1,275 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState } from "react";
-import { Loader2, Save, Trash2 } from "lucide-react";
-import { toast } from "sonner";
-
-import { PlanLimitDialog } from "@/components/billing/plan-limit-dialog";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import {
- createFinanceEntryAction,
- updateFinanceEntryAction,
-} from "@/lib/appwrite/finance-actions";
-import { initialFinanceState } from "@/lib/appwrite/finance-types";
-import { ScopeToggle } from "@/components/finance/scope-toggle";
-
-import type { BankAccountOption, Customer, FinanceRow, FinanceType } from "./types";
-
-const NONE = "__none__";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- entry?: FinanceRow | null;
- defaultType?: FinanceType;
- customers: Customer[];
- bankAccounts: BankAccountOption[];
- onRequestDelete?: (entry: FinanceRow) => void;
-};
-
-function isoToDate(iso: string): string {
- if (!iso) return "";
- return iso.slice(0, 10);
-}
-
-export function FinanceFormSheet({
- open,
- onOpenChange,
- entry,
- defaultType = "income",
- customers,
- bankAccounts,
- onRequestDelete,
-}: Props) {
- const isEdit = Boolean(entry);
- const action = isEdit ? updateFinanceEntryAction : createFinanceEntryAction;
- const [state, formAction, isPending] = useActionState(action, initialFinanceState);
- const [planLimitOpen, setPlanLimitOpen] = useState(false);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Kayıt güncellendi." : "Kayıt eklendi.");
- onOpenChange(false);
- } else if (state.code === "PLAN_LIMIT_EXCEEDED") {
- setPlanLimitOpen(true);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- const today = new Date().toISOString().slice(0, 10);
-
- return (
-
-
-
- {isEdit ? "Kaydı düzenle" : "Yeni kayıt"}
-
- Gelir, gider, borç veya alacak girişi. Borç = ödeyeceğiniz, Alacak = tahsil edeceğiniz.
-
-
-
- {
- ["customerId", "paymentMethod", "bankAccountId"].forEach((k) => {
- if (fd.get(k) === NONE) fd.set(k, "");
- });
- formAction(fd);
- }}
- className="flex flex-1 flex-col"
- >
- {isEdit && entry && }
-
-
-
-
-
-
- Tür *
-
-
-
-
-
- Gelir
- Gider
- Alacak
- Borç
-
-
-
-
-
Tutar (₺) *
-
- {state.fieldErrors?.amount && (
-
{state.fieldErrors.amount}
- )}
-
-
-
-
-
- Tarih *
-
-
-
- Ödeme yöntemi
-
-
-
-
-
- Belirtilmemiş
- Nakit
- Havale / EFT
- Kart
- Çek
- Diğer
-
-
-
-
-
-
-
- Müşteri (opsiyonel)
-
-
-
-
-
- Yok
- {customers.map((c) => (
-
- {c.name}
-
- ))}
-
-
-
-
- Banka hesabı
-
-
-
-
-
- Yok
- {bankAccounts.map((b) => (
-
- {b.label}
-
- ))}
-
-
-
-
-
-
- Açıklama
-
-
-
-
-
-
-
- {isEdit && entry && onRequestDelete && (
- onRequestDelete(entry)}
- disabled={isPending}
- >
-
- Sil
-
- )}
-
-
- onOpenChange(false)}
- disabled={isPending}
- >
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Kaydet"}
- >
- )}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/components/types.ts b/src/app/(dashboard)/finance/components/types.ts
deleted file mode 100644
index b8dc669..0000000
--- a/src/app/(dashboard)/finance/components/types.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-export type FinanceType = "income" | "expense" | "debt" | "receivable";
-export type PaymentMethod = "cash" | "transfer" | "card" | "check" | "other" | "";
-
-export type FinanceRow = {
- id: string;
- type: FinanceType;
- amount: number;
- date: string;
- description: string;
- customerId: string;
- customerName: string;
- paymentMethod: PaymentMethod;
- invoiceId: string;
- bankAccountId: string;
- bankAccountLabel: string;
-};
-
-export type Customer = { id: string; name: string };
-export type BankAccountOption = { id: string; label: string };
-
-export const TYPE_LABEL: Record = {
- income: "Gelir",
- expense: "Gider",
- debt: "Borç",
- receivable: "Alacak",
-};
-
-export const TYPE_COLOR: Record = {
- income: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-500/30",
- expense: "bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/30",
- debt: "bg-amber-500/15 text-amber-800 dark:text-amber-300 border-amber-500/30",
- receivable: "bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/30",
-};
-
-export const PAYMENT_METHOD_LABEL: Record = {
- cash: "Nakit",
- transfer: "Havale / EFT",
- card: "Kart",
- check: "Çek",
- other: "Diğer",
- "": "—",
-};
diff --git a/src/app/(dashboard)/finance/loans/components/loan-form-sheet.tsx b/src/app/(dashboard)/finance/loans/components/loan-form-sheet.tsx
deleted file mode 100644
index a3e484a..0000000
--- a/src/app/(dashboard)/finance/loans/components/loan-form-sheet.tsx
+++ /dev/null
@@ -1,257 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState } from "react";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import { createLoanAction } from "@/lib/appwrite/loan-actions";
-import { initialLoanState } from "@/lib/appwrite/loan-types";
-import { formatTRY } from "@/lib/format";
-import { ScopeToggle } from "@/components/finance/scope-toggle";
-
-import type { BankAccountOption } from "./types";
-
-const NONE = "__none__";
-
-function computeMonthly(principal: number, ratePct: number, n: number): number {
- if (!principal || !n) return 0;
- const r = ratePct / 100;
- if (r === 0) return Number((principal / n).toFixed(2));
- const factor = Math.pow(1 + r, n);
- return Number(((principal * r * factor) / (factor - 1)).toFixed(2));
-}
-
-export function LoanFormSheet({
- open,
- onOpenChange,
- bankAccounts,
-}: {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- bankAccounts: BankAccountOption[];
-}) {
- const [state, formAction, isPending] = useActionState(createLoanAction, initialLoanState);
- const [principal, setPrincipal] = useState(0);
- const [rate, setRate] = useState(2.5);
- const [term, setTerm] = useState(24);
-
- useEffect(() => {
- if (state.ok) {
- toast.success("Kredi kaydedildi, taksitler oluşturuldu.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- const monthly = computeMonthly(principal, rate, term);
- const total = monthly * term;
- const today = new Date().toISOString().slice(0, 10);
-
- return (
-
-
-
- Yeni kredi
-
- Kaydedince {term || 0} adet taksit otomatik hesaplanır ve eklenir.
-
-
-
- {
- if (fd.get("bankAccountId") === NONE) fd.set("bankAccountId", "");
- formAction(fd);
- }}
- className="flex flex-1 flex-col"
- >
-
-
-
-
-
-
Banka *
-
- {state.fieldErrors?.bankName && (
-
{state.fieldErrors.bankName}
- )}
-
-
- Tür
-
-
-
-
-
- İhtiyaç
- Taşıt
- Konut
- Ticari
- KMH
- Diğer
-
-
-
-
-
-
- Kredi adı *
-
-
-
-
-
Bağlı hesap
-
-
-
-
-
- Yok
- {bankAccounts.map((b) => (
-
- {b.label}
-
- ))}
-
-
-
- Taksit ödemeleri seçilen hesaba expense olarak yazılır.
-
-
-
-
-
-
Anapara (₺) *
-
setPrincipal(Number(e.target.value) || 0)}
- />
- {state.fieldErrors?.principal && (
-
{state.fieldErrors.principal}
- )}
-
-
- Aylık faiz %
- setRate(Number(e.target.value) || 0)}
- />
-
-
- Vade (ay) *
- setTerm(Number(e.target.value) || 0)}
- />
-
-
-
-
-
-
-
- Aylık taksit
- {formatTRY(monthly)}
-
-
- Toplam ödeme
- {formatTRY(total)}
-
-
- Toplam faiz
- {formatTRY(Math.max(0, total - principal))}
-
-
-
-
- Notlar
-
-
-
-
-
-
- onOpenChange(false)} disabled={isPending}>
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Oluşturuluyor...
- >
- ) : (
- <>
-
- Krediyi kaydet
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/loans/components/loans-client.tsx b/src/app/(dashboard)/finance/loans/components/loans-client.tsx
deleted file mode 100644
index fe39c97..0000000
--- a/src/app/(dashboard)/finance/loans/components/loans-client.tsx
+++ /dev/null
@@ -1,357 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import {
- Banknote,
- Check,
- ChevronDown,
- ChevronUp,
- Loader2,
- Plus,
- RotateCcw,
- Trash2,
-} from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- deleteLoanAction,
- payInstallmentAction,
- unpayInstallmentAction,
-} from "@/lib/appwrite/loan-actions";
-import { formatDate, formatTRY } from "@/lib/format";
-import { cn } from "@/lib/utils";
-
-import { LoanFormSheet } from "./loan-form-sheet";
-import {
- type BankAccountOption,
- type InstallmentRow,
- LOAN_STATUS_LABEL,
- LOAN_TYPE_LABEL,
- type LoanRow,
-} from "./types";
-
-type Props = {
- loans: LoanRow[];
- installments: InstallmentRow[];
- bankAccounts: BankAccountOption[];
-};
-
-export function LoansClient({ loans, installments, bankAccounts }: Props) {
- const [formOpen, setFormOpen] = useState(false);
- const [expanded, setExpanded] = useState(null);
- const [deleting, setDeleting] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const totalPrincipal = loans
- .filter((l) => l.status === "active")
- .reduce((s, l) => s + l.principal, 0);
- const totalRemaining = loans
- .filter((l) => l.status === "active")
- .reduce((s, l) => s + (l.totalAmount - l.paidAmount), 0);
-
- const installmentsByLoan = new Map();
- for (const i of installments) {
- const arr = installmentsByLoan.get(i.loanId) ?? [];
- arr.push(i);
- installmentsByLoan.set(i.loanId, arr);
- }
-
- const togglePay = (inst: InstallmentRow) => {
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", inst.id);
- const result = inst.paid ? await unpayInstallmentAction(fd) : await payInstallmentAction(fd);
- if (result.ok) {
- toast.success(inst.paid ? "Taksit ödenmedi olarak işaretlendi." : "Taksit ödendi olarak işaretlendi.");
- } else {
- toast.error(result.error ?? "İşlem başarısız.");
- }
- });
- };
-
- const handleDelete = () => {
- if (!deleting) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deleting.id);
- const result = await deleteLoanAction(fd);
- if (result.ok) {
- toast.success("Kredi silindi.");
- setDeleting(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- return (
-
-
-
-
- Aktif kredi sayısı
-
- {loans.filter((l) => l.status === "active").length}
-
-
-
-
-
- Toplam çekilen
- {formatTRY(totalPrincipal)}
-
-
-
-
- Kalan ödeme
-
- {formatTRY(totalRemaining)}
-
-
-
-
-
-
-
setFormOpen(true)}>
-
- Yeni kredi
-
-
-
- {loans.length === 0 ? (
-
-
-
- Henüz kredi tanımlanmamış.
- setFormOpen(true)}>
-
- İlk krediyi ekle
-
-
-
- ) : (
-
- {loans.map((loan) => {
- const isOpen = expanded === loan.id;
- const items = installmentsByLoan.get(loan.id) ?? [];
- const progressPct =
- loan.totalAmount > 0 ? (loan.paidAmount / loan.totalAmount) * 100 : 0;
- return (
-
-
-
-
-
-
{loan.bankName}
- ·
- {loan.loanName}
-
- {LOAN_TYPE_LABEL[loan.loanType]}
-
-
- {LOAN_STATUS_LABEL[loan.status]}
-
-
- {loan.bankAccountLabel && (
-
- Hesap: {loan.bankAccountLabel}
-
- )}
-
-
- setExpanded(isOpen ? null : loan.id)}
- >
- {isOpen ? (
- <>
-
- Kapat
- >
- ) : (
- <>
-
- Taksitler ({items.length})
- >
- )}
-
- setDeleting(loan)}
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {loan.remainingCount === 0
- ? "Tüm taksitler ödendi"
- : `${loan.remainingCount} taksit kaldı`}
-
-
- {formatTRY(loan.paidAmount)} / {formatTRY(loan.totalAmount)}
-
-
-
-
-
- {isOpen && (
-
-
-
-
- #
- Vade
- Anapara
- Faiz
- Toplam
- Durum
-
-
-
- {items.map((it) => {
- const overdue =
- !it.paid && new Date(it.dueDate) < new Date();
- return (
-
- {it.installmentNo}
-
- {formatDate(it.dueDate)}
-
-
- {formatTRY(it.principalPart)}
-
-
- {formatTRY(it.interestPart)}
-
-
- {formatTRY(it.amount)}
-
-
- togglePay(it)}
- >
- {busy ? (
-
- ) : it.paid ? (
- <>
-
- Geri al
- >
- ) : (
- <>
-
- Ödendi
- >
- )}
-
-
-
- );
- })}
-
-
-
- )}
-
-
- );
- })}
-
- )}
-
-
-
-
!v && setDeleting(null)}>
-
-
- Krediyi sil
-
- {deleting?.bankName} — {deleting?.loanName} ve tüm taksitleri silinecek.
- Bu işlem geri alınamaz.
-
-
-
- setDeleting(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
-
- );
-}
-
-function Stat({ label, value }: { label: string; value: string }) {
- return (
-
- );
-}
diff --git a/src/app/(dashboard)/finance/loans/components/types.ts b/src/app/(dashboard)/finance/loans/components/types.ts
deleted file mode 100644
index cbca62d..0000000
--- a/src/app/(dashboard)/finance/loans/components/types.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-export type LoanRow = {
- id: string;
- bankName: string;
- loanName: string;
- loanType: "consumer" | "vehicle" | "housing" | "commercial" | "kmh" | "other";
- principal: number;
- interestRate: number;
- termMonths: number;
- monthlyPayment: number;
- startDate: string;
- paymentDay: number;
- status: "active" | "closed" | "defaulted";
- bankAccountId: string;
- bankAccountLabel: string;
- notes: string;
- totalAmount: number;
- paidAmount: number;
- remainingCount: number;
- nextDue: string | null;
- scope: "company" | "personal";
-};
-
-export type InstallmentRow = {
- id: string;
- loanId: string;
- installmentNo: number;
- dueDate: string;
- amount: number;
- principalPart: number;
- interestPart: number;
- paid: boolean;
- paidAt: string;
-};
-
-export type BankAccountOption = { id: string; label: string };
-
-export const LOAN_TYPE_LABEL: Record = {
- consumer: "İhtiyaç",
- vehicle: "Taşıt",
- housing: "Konut",
- commercial: "Ticari",
- kmh: "KMH",
- other: "Diğer",
-};
-
-export const LOAN_STATUS_LABEL: Record = {
- active: "Aktif",
- closed: "Kapalı",
- defaulted: "Temerrüt",
-};
diff --git a/src/app/(dashboard)/finance/loans/page.tsx b/src/app/(dashboard)/finance/loans/page.tsx
deleted file mode 100644
index b509475..0000000
--- a/src/app/(dashboard)/finance/loans/page.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listBankAccounts } from "@/lib/appwrite/bank-account-queries";
-import { listAllInstallments, listLoans } from "@/lib/appwrite/loan-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { LoansClient } from "./components/loans-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Krediler",
-};
-
-export default async function LoansPage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const [loans, installments, bankAccounts] = await Promise.all([
- listLoans(ctx.tenantId, ctx.user.id),
- listAllInstallments(ctx.tenantId, ctx.user.id),
- listBankAccounts(ctx.tenantId, ctx.user.id),
- ]);
-
- const bankMap = new Map(
- bankAccounts.map((b) => [b.$id, `${b.bankName} — ${b.accountName}`]),
- );
-
- // Aggregate installment metrics per loan
- const byLoan = new Map<
- string,
- { totalAmount: number; paidAmount: number; nextDue: string | null; remainingCount: number }
- >();
- for (const inst of installments) {
- const cur = byLoan.get(inst.loanId) ?? {
- totalAmount: 0,
- paidAmount: 0,
- nextDue: null,
- remainingCount: 0,
- };
- cur.totalAmount += inst.amount ?? 0;
- if (inst.paid) {
- cur.paidAmount += inst.amount ?? 0;
- } else {
- cur.remainingCount += 1;
- if (!cur.nextDue || new Date(inst.dueDate).getTime() < new Date(cur.nextDue).getTime()) {
- cur.nextDue = inst.dueDate;
- }
- }
- byLoan.set(inst.loanId, cur);
- }
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Krediler
-
- Banka kredilerinizi ve taksit planlarını takip edin. Taksit ödendiğinde otomatik gider
- kaydı oluşur.
-
-
-
-
{
- const m = byLoan.get(l.$id) ?? {
- totalAmount: 0,
- paidAmount: 0,
- nextDue: null,
- remainingCount: 0,
- };
- return {
- id: l.$id,
- bankName: l.bankName,
- loanName: l.loanName,
- loanType: l.loanType ?? "consumer",
- principal: l.principal,
- interestRate: l.interestRate,
- termMonths: l.termMonths,
- monthlyPayment: l.monthlyPayment ?? 0,
- startDate: l.startDate,
- paymentDay: l.paymentDay ?? 1,
- status: l.status ?? "active",
- bankAccountId: l.bankAccountId ?? "",
- bankAccountLabel: l.bankAccountId ? bankMap.get(l.bankAccountId) ?? "" : "",
- notes: l.notes ?? "",
- totalAmount: m.totalAmount,
- paidAmount: m.paidAmount,
- remainingCount: m.remainingCount,
- nextDue: m.nextDue,
- scope: (l.scope ?? "company") as "company" | "personal",
- };
- })}
- installments={installments.map((i) => ({
- id: i.$id,
- loanId: i.loanId,
- installmentNo: i.installmentNo,
- dueDate: i.dueDate,
- amount: i.amount,
- principalPart: i.principalPart ?? 0,
- interestPart: i.interestPart ?? 0,
- paid: Boolean(i.paid),
- paidAt: i.paidAt ?? "",
- }))}
- bankAccounts={bankAccounts
- .filter((b) => !b.archived)
- .map((b) => ({
- id: b.$id,
- label: `${b.bankName} — ${b.accountName}`,
- }))}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/finance/page.tsx b/src/app/(dashboard)/finance/page.tsx
deleted file mode 100644
index 172280f..0000000
--- a/src/app/(dashboard)/finance/page.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listBankAccounts } from "@/lib/appwrite/bank-account-queries";
-import { listCustomers } from "@/lib/appwrite/customer-queries";
-import { listFinanceEntries } from "@/lib/appwrite/finance-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { FinanceClient } from "./components/finance-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Gelir / Gider",
-};
-
-export default async function FinancePage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const [entries, customers, bankAccounts] = await Promise.all([
- listFinanceEntries(ctx.tenantId, ctx.user.id),
- listCustomers(ctx.tenantId),
- listBankAccounts(ctx.tenantId, ctx.user.id),
- ]);
-
- const customerMap = new Map(customers.map((c) => [c.$id, c.name]));
- const bankMap = new Map(
- bankAccounts.map((b) => [b.$id, `${b.bankName} — ${b.accountName}`]),
- );
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Gelir / Gider
-
- Nakit hareketleri, borç ve alacaklarınızı tek yerden takip edin.
-
-
-
-
({
- id: e.$id,
- type: e.type,
- amount: e.amount,
- date: e.date,
- description: e.description ?? "",
- customerId: e.customerId ?? "",
- customerName: e.customerId ? customerMap.get(e.customerId) ?? "" : "",
- paymentMethod: e.paymentMethod ?? "",
- invoiceId: e.invoiceId ?? "",
- bankAccountId: e.bankAccountId ?? "",
- bankAccountLabel: e.bankAccountId ? bankMap.get(e.bankAccountId) ?? "" : "",
- }))}
- customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
- bankAccounts={bankAccounts
- .filter((b) => !b.archived)
- .map((b) => ({
- id: b.$id,
- label: `${b.bankName} — ${b.accountName}`,
- }))}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/finance/reports/components/report-client.tsx b/src/app/(dashboard)/finance/reports/components/report-client.tsx
deleted file mode 100644
index 7c3f5ed..0000000
--- a/src/app/(dashboard)/finance/reports/components/report-client.tsx
+++ /dev/null
@@ -1,542 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import {
- AlertCircle,
- ArrowDownRight,
- ArrowUpRight,
- Banknote,
- Building2,
- CircleDollarSign,
- CreditCard,
- Crown,
- ExternalLink,
- Receipt,
- Wallet,
-} from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import type { FinancialReport, ReportPeriod } from "@/lib/appwrite/finance-report-queries";
-import { formatDate, formatTRY } from "@/lib/format";
-import { cn } from "@/lib/utils";
-
-const PERIOD_LABEL: Record = {
- month: "Bu ay",
- quarter: "Bu çeyrek",
- year: "Bu yıl",
- all: "Tüm zamanlar",
-};
-
-const STATUS_LABEL: Record<"pending" | "partial" | "overdue", string> = {
- pending: "Bekliyor",
- partial: "Kısmi",
- overdue: "Gecikti",
-};
-
-const STATUS_COLOR: Record<"pending" | "partial" | "overdue", string> = {
- pending: "bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/30",
- partial: "bg-amber-500/15 text-amber-800 dark:text-amber-300 border-amber-500/30",
- overdue: "bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/30",
-};
-
-export function ReportClient({ data }: { data: FinancialReport }) {
- const router = useRouter();
-
- const setPeriod = (p: ReportPeriod) => {
- const params = new URLSearchParams();
- if (p !== "month") params.set("period", p);
- router.push(`/finance/reports${params.size ? `?${params}` : ""}`);
- };
-
- return (
-
-
- setPeriod(v as ReportPeriod)}>
-
-
-
-
- {PERIOD_LABEL.month}
- {PERIOD_LABEL.quarter}
- {PERIOD_LABEL.year}
- {PERIOD_LABEL.all}
-
-
-
-
- {/* KPIs */}
-
- = 0 ? "positive" : "negative"}
- icon={CircleDollarSign}
- subtitle="Banka + alacaklar − borçlar"
- />
-
-
- = 0 ? "positive" : "negative"}
- icon={Wallet}
- />
-
-
- {/* Cash composition */}
-
-
- Nakit pozisyonu detayı
-
- Bugünkü gerçek nakit + tahsil edilebilir − ödenecek borçlar
-
-
-
-
-
-
-
-
- Net pozisyon
- = 0
- ? "text-emerald-600 dark:text-emerald-400"
- : "text-red-600 dark:text-red-400",
- )}
- >
- {formatTRY(data.kpi.cashPosition)}
-
-
-
-
-
- {/* Trend chart */}
-
-
- {/* Top customers + Expense breakdown */}
-
-
-
-
-
- En çok ciro yapan müşteriler
-
-
- {PERIOD_LABEL[data.period]} ödenmiş faturalara göre
-
-
-
- {data.topCustomers.length === 0 ? (
-
- Bu dönemde ödenmiş fatura yok.
-
- ) : (
-
- )}
-
-
-
-
-
- Gider dağılımı
- {PERIOD_LABEL[data.period]} kaynak bazında
-
-
-
-
-
- {data.kpi.expense === 0 && (
-
- Bu dönemde gider yok.
-
- )}
-
-
-
-
- {/* Loans + Cards summary */}
-
-
-
-
-
-
- Aktif krediler
-
- Kalan ödeme tutarına göre
-
-
- Tümü
-
-
-
- {data.loans.length === 0 ? (
-
- Aktif kredi yok.
-
- ) : (
-
-
-
- Kredi
- Aylık
- Kalan
- Sonraki
-
-
-
- {data.loans.map((l) => (
-
-
- {l.bankName}
- {l.loanName}
-
-
- {formatTRY(l.monthlyPayment)}
-
-
- {formatTRY(l.remaining)}
-
-
- {l.nextDue ? formatDate(l.nextDue) : "—"}
-
-
- ))}
-
-
- )}
-
-
-
-
-
-
-
-
- Kart ekstreleri
-
- Bekleyen ve gecikmiş ödemeler
-
-
- Tümü
-
-
-
- {data.cardStatements.length === 0 ? (
-
- Açık ekstre yok.
-
- ) : (
-
-
-
- Kart
- Vade
- Kalan
- Durum
-
-
-
- {data.cardStatements.map((s) => (
-
-
- {s.cardLabel}
-
- {s.period}
-
-
-
- {formatDate(s.dueDate)}
-
-
- {formatTRY(s.remaining)}
-
-
-
- {STATUS_LABEL[s.status]}
-
-
-
- ))}
-
-
- )}
-
-
-
-
- {/* Outstanding invoices */}
-
-
-
-
-
- Bekleyen faturalar
-
-
- Tahsil edilmesi gereken — vadesi geçmiş olanlar üstte
-
-
-
- Tümü
-
-
-
- {data.outstandingInvoices.length === 0 ? (
-
- Bekleyen fatura yok.
-
- ) : (
-
-
-
- Numara
- Müşteri
- Vade
- Tutar
-
-
-
- {data.outstandingInvoices.map((inv) => (
-
-
-
- {inv.number}
-
-
- {inv.customerName}
-
- {inv.overdue && }
- {formatDate(inv.dueDate)}
-
-
- {formatTRY(inv.total)}
-
-
- ))}
-
-
- )}
-
-
-
- );
-}
-
-function KpiCard({
- label,
- value,
- tone,
- icon: Icon,
- subtitle,
-}: {
- label: string;
- value: string;
- tone: "positive" | "negative" | "neutral";
- icon: typeof Wallet;
- subtitle?: string;
-}) {
- const cls = {
- positive: "text-emerald-600 dark:text-emerald-400",
- negative: "text-red-600 dark:text-red-400",
- neutral: "text-muted-foreground",
- }[tone];
- return (
-
-
-
-
{label}
-
{value}
- {subtitle &&
{subtitle}
}
-
-
-
-
- );
-}
-
-function CompositionRow({
- icon: Icon,
- label,
- sign,
- amount,
- href,
-}: {
- icon: typeof Building2;
- label: string;
- sign: "+" | "−";
- amount: number;
- href?: string;
-}) {
- const positive = sign === "+";
- const content = (
-
-
-
- {label}
-
-
- {sign} {formatTRY(amount)}
-
-
- );
- if (href) {
- return (
-
- {content}
-
- );
- }
- return content;
-}
-
-function ExpenseRow({
- label,
- amount,
- total,
- color,
-}: {
- label: string;
- amount: number;
- total: number;
- color: string;
-}) {
- const pct = total > 0 ? (amount / total) * 100 : 0;
- return (
-
-
- {label}
-
- {formatTRY(amount)}{" "}
- ({pct.toFixed(1)}%)
-
-
-
-
- );
-}
-
-// Pull TrendChart in via dynamic import only on client. Recharts is heavy.
-import dynamic from "next/dynamic";
-const TrendChartLazy = dynamic(
- () => import("./trend-chart").then((m) => ({ default: m.TrendChart })),
- { ssr: false },
-);
diff --git a/src/app/(dashboard)/finance/reports/components/trend-chart.tsx b/src/app/(dashboard)/finance/reports/components/trend-chart.tsx
deleted file mode 100644
index 5a07a42..0000000
--- a/src/app/(dashboard)/finance/reports/components/trend-chart.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-"use client";
-
-import {
- Area,
- AreaChart,
- CartesianGrid,
- Legend,
- ResponsiveContainer,
- Tooltip,
- XAxis,
- YAxis,
-} from "recharts";
-
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { formatTRY } from "@/lib/format";
-
-type Point = { month: string; income: number; expense: number; net: number };
-
-export function TrendChart({ data }: { data: Point[] }) {
- return (
-
-
- 12 aylık trend
- Gelir, gider ve net kâr
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- (v >= 1000 ? `${(v / 1000).toFixed(0)}k` : String(v))}
- />
- [
- formatTRY(Number(value) || 0),
- name === "income" ? "Gelir" : name === "expense" ? "Gider" : "Net",
- ]}
- />
- (v === "income" ? "Gelir" : v === "expense" ? "Gider" : "Net")}
- />
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/finance/reports/page.tsx b/src/app/(dashboard)/finance/reports/page.tsx
deleted file mode 100644
index cfe621a..0000000
--- a/src/app/(dashboard)/finance/reports/page.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import {
- getFinancialReport,
- type ReportPeriod,
-} from "@/lib/appwrite/finance-report-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { ReportClient } from "./components/report-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Finansal rapor",
-};
-
-const ALLOWED: ReportPeriod[] = ["month", "quarter", "year", "all"];
-
-export default async function ReportsPage({
- searchParams,
-}: {
- searchParams: Promise<{ period?: string }>;
-}) {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const sp = await searchParams;
- const period: ReportPeriod = (ALLOWED as string[]).includes(sp.period ?? "")
- ? (sp.period as ReportPeriod)
- : "month";
-
- const data = await getFinancialReport(ctx.tenantId, period);
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Finansal rapor
-
- İşletmenizin nakit pozisyonu, gelir/gider performansı ve borç yükünün tek bakışta özeti.
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/investors/page.tsx b/src/app/(dashboard)/investors/page.tsx
new file mode 100644
index 0000000..85a06e5
--- /dev/null
+++ b/src/app/(dashboard)/investors/page.tsx
@@ -0,0 +1,8 @@
+export default function Page() {
+ return (
+
+
investors
+
Yakında...
+
+ );
+}
diff --git a/src/app/(dashboard)/invoices/[id]/components/header-actions.tsx b/src/app/(dashboard)/invoices/[id]/components/header-actions.tsx
deleted file mode 100644
index 1693043..0000000
--- a/src/app/(dashboard)/invoices/[id]/components/header-actions.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import { useRouter } from "next/navigation";
-import { Loader2, Pencil, Printer, Trash2 } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { deleteInvoiceAction } from "@/lib/appwrite/invoice-actions";
-
-import { InvoiceFormSheet } from "../../components/invoice-form-sheet";
-import type { Customer, InvoiceRow } from "../../components/types";
-
-type Props = { invoice: InvoiceRow; customers: Customer[] };
-
-export function InvoiceHeaderActions({ invoice, customers }: Props) {
- const router = useRouter();
- const [editOpen, setEditOpen] = useState(false);
- const [deleting, setDeleting] = useState(false);
- const [busy, startTransition] = useTransition();
-
- const handleDelete = () => {
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", invoice.id);
- const result = await deleteInvoiceAction(fd);
- if (result.ok) {
- toast.success("Fatura silindi.");
- router.push("/invoices");
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- setDeleting(false);
- }
- });
- };
-
- return (
- <>
-
-
window.print()}>
-
- Yazdır
-
-
setEditOpen(true)}>
-
- Düzenle
-
-
setDeleting(true)}
- >
-
- Sil
-
-
-
-
-
-
-
-
- Faturayı sil
-
- {invoice.number} ve tüm kalemleri silinecek. Bu işlem geri alınamaz.
-
-
-
- setDeleting(false)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
- >
- );
-}
diff --git a/src/app/(dashboard)/invoices/[id]/components/items-editor.tsx b/src/app/(dashboard)/invoices/[id]/components/items-editor.tsx
deleted file mode 100644
index 809965f..0000000
--- a/src/app/(dashboard)/invoices/[id]/components/items-editor.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState, useTransition } from "react";
-import { Loader2, Plus, Save, Trash2 } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Sheet,
- SheetContent,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import {
- addInvoiceItemAction,
- deleteInvoiceItemAction,
- updateInvoiceItemAction,
-} from "@/lib/appwrite/invoice-actions";
-import { initialInvoiceState } from "@/lib/appwrite/invoice-types";
-import { formatTRY } from "@/lib/format";
-
-export type InvoiceItemRow = {
- id: string;
- description: string;
- quantity: number;
- unitPrice: number;
- vatRate: number;
- lineTotal: number;
-};
-
-type Props = { invoiceId: string; items: InvoiceItemRow[] };
-
-export function InvoiceItemsEditor({ invoiceId, items }: Props) {
- const [formOpen, setFormOpen] = useState(false);
- const [editing, setEditing] = useState(null);
- const [deleting, setDeleting] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const handleDelete = () => {
- if (!deleting) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deleting.id);
- const result = await deleteInvoiceItemAction(fd);
- if (result.ok) {
- toast.success("Kalem silindi.");
- setDeleting(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- return (
- <>
-
-
-
-
Kalemler ({items.length})
-
{
- setEditing(null);
- setFormOpen(true);
- }}
- >
-
- Kalem ekle
-
-
-
-
-
-
- Açıklama
- Miktar
- Birim fiyat
- KDV %
- Toplam
-
-
-
-
- {items.length ? (
- items.map((it) => (
- {
- setEditing(it);
- setFormOpen(true);
- }}
- >
- {it.description}
- {it.quantity}
-
- {formatTRY(it.unitPrice)}
-
- {it.vatRate}%
-
- {formatTRY(it.lineTotal)}
-
-
- {
- e.stopPropagation();
- setDeleting(it);
- }}
- >
-
-
-
-
- ))
- ) : (
-
-
-
- Henüz kalem eklenmemiş. Yukarıdan ekleyin.
-
-
-
- )}
-
-
-
-
-
- {
- setFormOpen(v);
- if (!v) setEditing(null);
- }}
- invoiceId={invoiceId}
- item={editing}
- />
-
- !v && setDeleting(null)}>
-
-
- Kalemi sil
-
- {deleting?.description} kalemini silmek istediğinize emin misiniz?
-
-
-
- setDeleting(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
- >
- );
-}
-
-function ItemFormSheet({
- open,
- onOpenChange,
- invoiceId,
- item,
-}: {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- invoiceId: string;
- item?: InvoiceItemRow | null;
-}) {
- const isEdit = Boolean(item);
- const action = isEdit ? updateInvoiceItemAction : addInvoiceItemAction;
- const [state, formAction, isPending] = useActionState(action, initialInvoiceState);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Kalem güncellendi." : "Kalem eklendi.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- return (
-
-
-
- {isEdit ? "Kalemi düzenle" : "Yeni kalem"}
-
-
-
- {isEdit && item && }
- {!isEdit && }
-
-
-
-
-
- onOpenChange(false)}
- disabled={isPending}
- >
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Ekle"}
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/invoices/[id]/page.tsx b/src/app/(dashboard)/invoices/[id]/page.tsx
deleted file mode 100644
index 9563e0c..0000000
--- a/src/app/(dashboard)/invoices/[id]/page.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import type { Metadata } from "next";
-import Link from "next/link";
-import { notFound, redirect } from "next/navigation";
-import { ArrowLeft } from "lucide-react";
-
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import { listCustomers } from "@/lib/appwrite/customer-queries";
-import { getInvoice, listInvoiceItems } from "@/lib/appwrite/invoice-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { formatDate, formatTRY } from "@/lib/format";
-import { cn } from "@/lib/utils";
-
-import { STATUS_COLOR, STATUS_LABEL } from "../components/types";
-import { InvoiceItemsEditor } from "./components/items-editor";
-import { InvoiceHeaderActions } from "./components/header-actions";
-
-export const metadata: Metadata = {
- title: "İşletmem — Fatura",
-};
-
-export default async function InvoiceDetailPage({
- params,
-}: {
- params: Promise<{ id: string }>;
-}) {
- const { id } = await params;
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const invoice = await getInvoice(ctx.tenantId, id);
- if (!invoice) notFound();
-
- const [items, customers] = await Promise.all([
- listInvoiceItems(ctx.tenantId, id),
- listCustomers(ctx.tenantId),
- ]);
-
- const customerName = customers.find((c) => c.$id === invoice.customerId)?.name ?? "—";
-
- return (
-
-
-
-
-
-
{customerName}
-
{invoice.number}
-
-
- {STATUS_LABEL[invoice.status ?? "draft"]}
-
-
- Düzenleme: {formatDate(invoice.issueDate)}
-
- Vade: {formatDate(invoice.dueDate)}
-
-
-
({ id: c.$id, name: c.name }))}
- />
-
-
-
({
- id: it.$id,
- description: it.description,
- quantity: it.quantity,
- unitPrice: it.unitPrice,
- vatRate: it.vatRate ?? 0,
- lineTotal: it.lineTotal,
- }))}
- />
-
-
-
-
- Ara toplam
- {formatTRY(invoice.subtotal ?? 0)}
-
-
- KDV
- {formatTRY(invoice.vatTotal ?? 0)}
-
-
-
- Genel toplam
- {formatTRY(invoice.total ?? 0)}
-
-
-
-
- {invoice.notes && (
-
-
- Notlar
- {invoice.notes}
-
-
- )}
-
- );
-}
diff --git a/src/app/(dashboard)/invoices/components/invoice-form-sheet.tsx b/src/app/(dashboard)/invoices/components/invoice-form-sheet.tsx
deleted file mode 100644
index 4007e48..0000000
--- a/src/app/(dashboard)/invoices/components/invoice-form-sheet.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-"use client";
-
-import { useActionState, useEffect } from "react";
-import { useRouter } from "next/navigation";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import {
- createInvoiceAction,
- updateInvoiceAction,
-} from "@/lib/appwrite/invoice-actions";
-import { initialInvoiceState } from "@/lib/appwrite/invoice-types";
-
-import type { Customer, InvoiceRow } from "./types";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- invoice?: InvoiceRow | null;
- customers: Customer[];
-};
-
-function isoToDate(iso: string): string {
- if (!iso) return "";
- return iso.slice(0, 10);
-}
-
-export function InvoiceFormSheet({ open, onOpenChange, invoice, customers }: Props) {
- const isEdit = Boolean(invoice);
- const action = isEdit ? updateInvoiceAction : createInvoiceAction;
- const [state, formAction, isPending] = useActionState(action, initialInvoiceState);
- const router = useRouter();
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Fatura güncellendi." : "Fatura oluşturuldu.");
- onOpenChange(false);
- if (!isEdit && state.invoiceId) {
- router.push(`/invoices/${state.invoiceId}`);
- }
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- const today = new Date().toISOString().slice(0, 10);
- const inThirty = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
- .toISOString()
- .slice(0, 10);
-
- return (
-
-
-
- {isEdit ? "Faturayı düzenle" : "Yeni fatura"}
-
- {isEdit
- ? "Fatura bilgilerini güncelleyin. Kalem eklemek için fatura detayına gidin."
- : "Faturayı oluşturun, ardından detay sayfasında kalemleri ekleyin. Numara otomatik üretilir."}
-
-
-
-
- {isEdit && invoice && }
-
-
-
-
Müşteri *
-
-
-
-
-
- {customers.map((c) => (
-
- {c.name}
-
- ))}
-
-
- {state.fieldErrors?.customerId && (
-
{state.fieldErrors.customerId}
- )}
-
-
-
-
-
-
Durum
-
-
-
-
-
- Taslak
- Gönderildi
- Ödendi
- Gecikmiş
- İptal
-
-
-
- “Ödendi” seçildiğinde finans modülüne otomatik gelir kaydı düşer.
- Durum geri alınırsa kayıt silinir.
-
-
-
-
- Notlar
-
-
-
-
-
-
- onOpenChange(false)}
- disabled={isPending}
- >
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Oluştur"}
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/invoices/components/invoices-client.tsx b/src/app/(dashboard)/invoices/components/invoices-client.tsx
deleted file mode 100644
index 8d2f1de..0000000
--- a/src/app/(dashboard)/invoices/components/invoices-client.tsx
+++ /dev/null
@@ -1,372 +0,0 @@
-"use client";
-
-import { useMemo, useState, useTransition } from "react";
-import Link from "next/link";
-import {
- type ColumnDef,
- flexRender,
- getCoreRowModel,
- getFilteredRowModel,
- getPaginationRowModel,
- getSortedRowModel,
- type SortingState,
- useReactTable,
-} from "@tanstack/react-table";
-import {
- ArrowUpRight,
- ChevronLeft,
- ChevronRight,
- ExternalLink,
- Loader2,
- MoreHorizontal,
- Plus,
- Receipt,
- Search,
- Trash2,
-} from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Input } from "@/components/ui/input";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { deleteInvoiceAction } from "@/lib/appwrite/invoice-actions";
-import { formatDate, formatTRY } from "@/lib/format";
-import { cn } from "@/lib/utils";
-
-import { InvoiceFormSheet } from "./invoice-form-sheet";
-import { type Customer, type InvoiceRow, STATUS_COLOR, STATUS_LABEL } from "./types";
-
-type Props = { invoices: InvoiceRow[]; customers: Customer[] };
-
-export function InvoicesClient({ invoices, customers }: Props) {
- const [search, setSearch] = useState("");
- const [sorting, setSorting] = useState([]);
- const [formOpen, setFormOpen] = useState(false);
- const [deleting, setDeleting] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const stats = useMemo(() => {
- let total = 0;
- let outstanding = 0;
- let paid = 0;
- let overdue = 0;
- for (const i of invoices) {
- total += i.total;
- if (i.status === "paid") paid += i.total;
- else if (i.status === "overdue") {
- outstanding += i.total;
- overdue += i.total;
- } else if (i.status === "sent" || i.status === "draft") outstanding += i.total;
- }
- return { total, outstanding, paid, overdue };
- }, [invoices]);
-
- const columns = useMemo[]>(
- () => [
- {
- accessorKey: "number",
- header: "Numara",
- cell: ({ row }) => (
-
- {row.original.number}
-
-
- ),
- },
- {
- accessorKey: "customerName",
- header: "Müşteri",
- cell: ({ row }) => row.original.customerName,
- },
- {
- accessorKey: "issueDate",
- header: "Tarih",
- cell: ({ row }) => (
- {formatDate(row.original.issueDate)}
- ),
- },
- {
- accessorKey: "dueDate",
- header: "Vade",
- cell: ({ row }) => {
- const overdue =
- row.original.status !== "paid" &&
- row.original.status !== "cancelled" &&
- new Date(row.original.dueDate) < new Date();
- return (
-
- {formatDate(row.original.dueDate)}
-
- );
- },
- },
- {
- accessorKey: "status",
- header: "Durum",
- cell: ({ row }) => (
-
- {STATUS_LABEL[row.original.status]}
-
- ),
- },
- {
- accessorKey: "total",
- header: "Toplam",
- cell: ({ row }) => (
- {formatTRY(row.original.total)}
- ),
- },
- {
- id: "actions",
- cell: ({ row }) => (
-
-
-
-
-
-
-
-
-
-
-
- Aç
-
-
- setDeleting(row.original)}
- >
-
- Sil
-
-
-
-
- ),
- },
- ],
- [],
- );
-
- const table = useReactTable({
- data: invoices,
- columns,
- state: { globalFilter: search, sorting },
- onGlobalFilterChange: setSearch,
- onSortingChange: setSorting,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getSortedRowModel: getSortedRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- initialState: { pagination: { pageSize: 25 } },
- globalFilterFn: (row, _id, fv) => {
- const v = String(fv).toLowerCase();
- return [row.original.number, row.original.customerName, row.original.notes]
- .join(" ")
- .toLowerCase()
- .includes(v);
- },
- });
-
- const handleDelete = () => {
- if (!deleting) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deleting.id);
- const result = await deleteInvoiceAction(fd);
- if (result.ok) {
- toast.success("Fatura silindi.");
- setDeleting(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- return (
- <>
-
-
-
- Toplam
- {formatTRY(stats.total)}
-
-
-
-
- Tahsil edildi
-
- {formatTRY(stats.paid)}
-
-
-
-
-
- Bekleyen
-
- {formatTRY(stats.outstanding)}
-
-
-
-
-
- Gecikmiş
-
- {formatTRY(stats.overdue)}
-
-
-
-
-
-
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Numara, müşteri, not..."
- className="pl-9"
- />
-
-
setFormOpen(true)} disabled={customers.length === 0}>
-
- Yeni fatura
-
-
-
-
-
- {table.getHeaderGroups().map((hg) => (
-
- {hg.headers.map((h) => (
-
- {h.isPlaceholder
- ? null
- : flexRender(h.column.columnDef.header, h.getContext())}
-
- ))}
-
- ))}
-
-
- {table.getRowModel().rows.length ? (
- table.getRowModel().rows.map((r) => (
-
- {r.getVisibleCells().map((c) => (
-
- {flexRender(c.column.columnDef.cell, c.getContext())}
-
- ))}
-
- ))
- ) : (
-
-
-
-
-
- {customers.length === 0
- ? "Önce müşteri ekleyin, sonra fatura kesebilirsiniz."
- : "Henüz fatura yok."}
-
-
-
-
- )}
-
-
-
-
-
- Toplam {table.getFilteredRowModel().rows.length} fatura
-
-
- table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- >
-
-
-
- Sayfa {table.getState().pagination.pageIndex + 1} /{" "}
- {Math.max(table.getPageCount(), 1)}
-
- table.nextPage()}
- disabled={!table.getCanNextPage()}
- >
-
-
-
-
-
-
-
-
-
- !v && setDeleting(null)}>
-
-
- Faturayı sil
-
- {deleting?.number} ve tüm kalemleri silinecek. Bu işlem geri alınamaz.
-
-
-
- setDeleting(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
- >
- );
-}
diff --git a/src/app/(dashboard)/invoices/components/types.ts b/src/app/(dashboard)/invoices/components/types.ts
deleted file mode 100644
index 524f2a7..0000000
--- a/src/app/(dashboard)/invoices/components/types.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-export type InvoiceStatus = "draft" | "sent" | "paid" | "overdue" | "cancelled";
-
-export type InvoiceRow = {
- id: string;
- number: string;
- customerId: string;
- customerName: string;
- issueDate: string;
- dueDate: string;
- status: InvoiceStatus;
- subtotal: number;
- vatTotal: number;
- total: number;
- notes: string;
-};
-
-export type Customer = { id: string; name: string };
-
-export const STATUS_LABEL: Record = {
- draft: "Taslak",
- sent: "Gönderildi",
- paid: "Ödendi",
- overdue: "Gecikmiş",
- cancelled: "İptal",
-};
-
-export const STATUS_COLOR: Record = {
- draft: "bg-slate-500/15 text-slate-700 dark:text-slate-300 border-slate-500/30",
- sent: "bg-blue-500/15 text-blue-700 dark:text-blue-300 border-blue-500/30",
- paid: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-500/30",
- overdue: "bg-red-500/15 text-red-700 dark:text-red-300 border-red-500/30",
- cancelled: "bg-muted text-muted-foreground border-muted-foreground/30",
-};
diff --git a/src/app/(dashboard)/invoices/page.tsx b/src/app/(dashboard)/invoices/page.tsx
deleted file mode 100644
index 6d0eaec..0000000
--- a/src/app/(dashboard)/invoices/page.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listCustomers } from "@/lib/appwrite/customer-queries";
-import { listInvoices } from "@/lib/appwrite/invoice-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { InvoicesClient } from "./components/invoices-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Faturalar",
-};
-
-export default async function InvoicesPage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const [invoices, customers] = await Promise.all([
- listInvoices(ctx.tenantId),
- listCustomers(ctx.tenantId),
- ]);
-
- const customerMap = new Map(customers.map((c) => [c.$id, c.name]));
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Faturalar
-
- Müşterilerinize fatura kesin, kalemleri yönetin, durumu takip edin.
-
-
-
-
({
- id: i.$id,
- number: i.number,
- customerId: i.customerId,
- customerName: customerMap.get(i.customerId) ?? "—",
- issueDate: i.issueDate,
- dueDate: i.dueDate,
- status: i.status ?? "draft",
- subtotal: i.subtotal ?? 0,
- vatTotal: i.vatTotal ?? 0,
- total: i.total ?? 0,
- notes: i.notes ?? "",
- }))}
- customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx
index 572292e..562815b 100644
--- a/src/app/(dashboard)/layout.tsx
+++ b/src/app/(dashboard)/layout.tsx
@@ -14,7 +14,7 @@ export default async function DashboardLayout({
const company = {
id: ctx.tenantId,
- name: ctx.settings?.companyName ?? "Çalışma alanı",
+ name: ctx.settings?.officeName ?? "Çalışma alanı",
logoUrl: getLogoUrl(ctx.settings?.logo) ?? null,
};
const user = {
diff --git a/src/app/(dashboard)/leads/components/lead-activities-fetcher.ts b/src/app/(dashboard)/leads/components/lead-activities-fetcher.ts
deleted file mode 100644
index 5a92a07..0000000
--- a/src/app/(dashboard)/leads/components/lead-activities-fetcher.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-"use server";
-
-import { listLeadActivities } from "@/lib/appwrite/lead-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import type { ActivityRow } from "./types";
-
-export async function listLeadActivitiesForClient(leadId: string): Promise {
- try {
- const ctx = await requireTenant();
- const rows = await listLeadActivities(ctx.tenantId, leadId);
- return rows.map((a) => ({
- id: a.$id,
- leadId: a.leadId,
- type: a.type,
- content: a.content,
- calendarEventId: a.calendarEventId ?? null,
- occurredAt: a.occurredAt ?? null,
- createdAt: a.$createdAt,
- createdByName: "",
- }));
- } catch {
- return [];
- }
-}
diff --git a/src/app/(dashboard)/leads/components/lead-card.tsx b/src/app/(dashboard)/leads/components/lead-card.tsx
deleted file mode 100644
index 55e496c..0000000
--- a/src/app/(dashboard)/leads/components/lead-card.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-"use client";
-
-import { useSortable } from "@dnd-kit/sortable";
-import { CSS } from "@dnd-kit/utilities";
-import { Calendar, GripVertical, MoreHorizontal, Pencil, Phone, Trash2, UserCircle } from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { cn } from "@/lib/utils";
-import { formatCurrency, formatDate } from "@/lib/format";
-
-import { LEAD_SOURCE_LABEL, LEAD_STATUS_CONFIG, type LeadRow } from "./types";
-
-type Props = {
- lead: LeadRow;
- onEdit: (lead: LeadRow) => void;
- onDetail: (lead: LeadRow) => void;
- onDelete: (lead: LeadRow) => void;
- isOverlay?: boolean;
-};
-
-export function LeadCard({ lead, onEdit, onDetail, onDelete, isOverlay }: Props) {
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
- id: lead.id,
- data: { type: "lead", status: lead.status },
- });
-
- const style = { transform: CSS.Transform.toString(transform), transition };
- const cfg = LEAD_STATUS_CONFIG[lead.status];
- const isOverdue = lead.nextFollowUpAt && new Date(lead.nextFollowUpAt) < new Date();
-
- return (
- onDetail(lead)}
- >
-
-
e.stopPropagation()}
- className="text-muted-foreground hover:text-foreground mt-0.5 cursor-grab touch-none active:cursor-grabbing"
- >
-
-
-
-
-
-
-
{lead.name}
- {lead.contactName && (
-
{lead.contactName}
- )}
-
-
-
- e.stopPropagation()}
- >
-
-
-
-
- { e.stopPropagation(); onDetail(lead); }}>
-
- Detay & Aktiviteler
-
- { e.stopPropagation(); onEdit(lead); }}>
-
- Düzenle
-
-
- { e.stopPropagation(); onDelete(lead); }}>
-
- Sil
-
-
-
-
-
-
-
- {LEAD_SOURCE_LABEL[lead.source]}
-
-
- {lead.estimatedValue != null && lead.estimatedValue > 0 && (
-
- {formatCurrency(lead.estimatedValue, lead.currency)}
-
- )}
-
-
-
- {lead.phone && (
-
-
- {lead.phone}
-
- )}
- {lead.nextFollowUpAt && (
-
-
- {formatDate(lead.nextFollowUpAt)}
- {isOverdue && " — gecikti"}
-
- )}
-
-
- {lead.assigneeName && (
-
-
- {lead.assigneeName}
-
- )}
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/leads/components/lead-detail-sheet.tsx b/src/app/(dashboard)/leads/components/lead-detail-sheet.tsx
deleted file mode 100644
index a683e43..0000000
--- a/src/app/(dashboard)/leads/components/lead-detail-sheet.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState, useTransition } from "react";
-import {
- Calendar, CheckCircle2, ChevronDown, Loader2, Mail, MessageSquarePlus,
- Phone, TrendingUp, UserCheck, X,
-} from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet, SheetContent, SheetHeader, SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import { cn } from "@/lib/utils";
-import { formatCurrency, formatDateTime } from "@/lib/format";
-import {
- addLeadActivityAction,
- scheduleFollowUpAction,
-} from "@/lib/appwrite/lead-activity-actions";
-import { convertLeadToCustomerAction } from "@/lib/appwrite/lead-actions";
-
-import {
- ACTIVITY_TYPE_CONFIG,
- LEAD_SOURCE_LABEL,
- LEAD_STATUS_CONFIG,
- type ActivityRow,
- type LeadActivityType,
- type LeadRow,
-} from "./types";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- lead: LeadRow | null;
- activities: ActivityRow[];
- onEdit: (lead: LeadRow) => void;
-};
-
-const ACTIVITY_TYPES: LeadActivityType[] = ["note", "call", "meeting", "email"];
-
-export function LeadDetailSheet({ open, onOpenChange, lead, activities, onEdit }: Props) {
- const [activityState, activityAction, activityPending] = useActionState(
- addLeadActivityAction, { ok: false },
- );
- const [followUpState, followUpAction, followUpPending] = useActionState(
- scheduleFollowUpAction, { ok: false },
- );
- const [convertBusy, startConvert] = useTransition();
- const [tab, setTab] = useState<"activities" | "followup">("activities");
- const [activityType, setActivityType] = useState("note");
-
- useEffect(() => {
- if (activityState.ok) toast.success("Aktivite kaydedildi.");
- else if (activityState.error) toast.error(activityState.error);
- }, [activityState]);
-
- useEffect(() => {
- if (followUpState.ok) { toast.success("Takip takvime eklendi."); setTab("activities"); }
- else if (followUpState.error) toast.error(followUpState.error);
- }, [followUpState]);
-
- const handleConvert = () => {
- if (!lead) return;
- startConvert(async () => {
- const fd = new FormData();
- fd.set("leadId", lead.id);
- const result = await convertLeadToCustomerAction(fd);
- if (result.ok) {
- toast.success("Müşteriye dönüştürüldü.");
- onOpenChange(false);
- } else {
- toast.error(result.error ?? "Dönüştürme başarısız.");
- }
- });
- };
-
- if (!lead) return null;
- const cfg = LEAD_STATUS_CONFIG[lead.status];
-
- return (
-
-
-
-
-
-
{lead.name}
- {lead.contactName && (
-
{lead.contactName}
- )}
-
-
- {cfg.label}
-
-
-
-
-
- {/* Info strip */}
-
- {lead.phone && (
-
- {lead.phone}
-
- )}
- {lead.email && (
-
- {lead.email}
-
- )}
- {lead.estimatedValue != null && lead.estimatedValue > 0 && (
-
-
- {formatCurrency(lead.estimatedValue, lead.currency)}
-
- )}
-
{LEAD_SOURCE_LABEL[lead.source]}
-
-
- {/* Tab bar */}
-
- {(["activities", "followup"] as const).map((t) => (
- setTab(t)}
- className={cn(
- "flex-1 px-4 py-2.5 text-sm font-medium transition-colors",
- tab === t
- ? "border-b-2 border-primary text-foreground"
- : "text-muted-foreground hover:text-foreground",
- )}
- >
- {t === "activities" ? "Aktiviteler" : "Takip Planla"}
-
- ))}
-
-
-
- {tab === "activities" && (
-
- {/* Add activity form */}
-
-
-
- setActivityType(v as LeadActivityType)}
- >
-
-
-
-
- {ACTIVITY_TYPES.map((t) => (
-
- {ACTIVITY_TYPE_CONFIG[t].icon} {ACTIVITY_TYPE_CONFIG[t].label}
-
- ))}
-
-
- ekle
-
-
-
-
- {activityPending
- ?
- : }
- Kaydet
-
-
-
-
- {/* Timeline */}
-
- {activities.length === 0 && (
-
Henüz aktivite yok.
- )}
- {activities.map((a) => {
- const acfg = ACTIVITY_TYPE_CONFIG[a.type];
- return (
-
-
- {acfg.icon}
-
-
-
- {acfg.label}
-
- {formatDateTime(a.occurredAt ?? a.createdAt)}
-
-
-
{a.content}
-
-
- );
- })}
-
-
- )}
-
- {tab === "followup" && (
-
-
-
- Takip tarihi & saati
-
-
-
- Not (isteğe bağlı)
-
-
-
- {followUpPending
- ? <> Planlanıyor…>
- : <> Takvime ekle>}
-
- {lead.nextFollowUpAt && (
-
- Mevcut takip: {formatDateTime(lead.nextFollowUpAt)}
-
- )}
-
- )}
-
-
- {/* Footer actions */}
-
-
onEdit(lead)} className="flex-1">
-
- Düzenle
-
- {lead.status !== "converted" && lead.status !== "lost" && (
-
- {convertBusy
- ?
- : }
- Müşteriye dönüştür
-
- )}
- {lead.status === "converted" && (
-
- Müşteriye dönüştürüldü
-
- )}
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/leads/components/lead-form-sheet.tsx b/src/app/(dashboard)/leads/components/lead-form-sheet.tsx
deleted file mode 100644
index 091202e..0000000
--- a/src/app/(dashboard)/leads/components/lead-form-sheet.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState } from "react";
-import { ChevronDown, Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import {
- Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import {
- createLeadAction,
- updateLeadAction,
-} from "@/lib/appwrite/lead-actions";
-import { initialLeadState } from "@/lib/appwrite/lead-types";
-
-import { LEAD_SOURCE_LABEL, LEAD_STATUS_CONFIG, type LeadRow, type MemberOption } from "./types";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- lead?: LeadRow | null;
- defaultStatus?: string;
- members: MemberOption[];
- onCreated?: (leadId: string) => void;
-};
-
-export function LeadFormSheet({ open, onOpenChange, lead, defaultStatus, members, onCreated }: Props) {
- const isEdit = Boolean(lead);
- const action = isEdit ? updateLeadAction : createLeadAction;
- const [state, formAction, isPending] = useActionState(action, initialLeadState);
- const [assigneeOpen, setAssigneeOpen] = useState(false);
- const [assigneeId, setAssigneeId] = useState(lead?.assigneeId ?? "");
-
- useEffect(() => {
- if (open) setAssigneeId(lead?.assigneeId ?? "");
- }, [open, lead]);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Aday güncellendi." : "Aday eklendi.");
- if (!isEdit && state.leadId) onCreated?.(state.leadId);
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- const selectedMember = members.find((m) => m.id === assigneeId);
-
- return (
-
-
-
- {isEdit ? "Adayı düzenle" : "Yeni müşteri adayı"}
- Müşteri adayının bilgilerini girin.
-
-
-
- {isEdit && lead && }
-
-
-
- {/* Ad / Şirket */}
-
-
Şirket / Lead adı *
-
- {state.fieldErrors?.name &&
{state.fieldErrors.name}
}
-
-
- {/* İlgili kişi */}
-
- İlgili kişi
-
-
-
- {/* Tel + Email */}
-
-
- Telefon
-
-
-
-
E-posta
-
- {state.fieldErrors?.email &&
{state.fieldErrors.email}
}
-
-
-
- {/* Kaynak + Durum */}
-
-
- Kaynak
-
-
-
- {(Object.entries(LEAD_SOURCE_LABEL) as [string, string][]).map(([v, l]) => (
- {l}
- ))}
-
-
-
-
- Durum
-
-
-
- {(Object.entries(LEAD_STATUS_CONFIG) as [string, { label: string }][]).map(([v, c]) => (
- {c.label}
- ))}
-
-
-
-
-
- {/* Tahmini değer + para birimi */}
-
-
- Tahmini değer
-
-
-
- Para birimi
-
-
-
- ₺ TRY
- $ USD
- € EUR
-
-
-
-
-
- {/* Sorumlu */}
- {members.length > 0 && (
-
-
Sorumlu
-
-
-
- {selectedMember ? (
- {selectedMember.name}
- ) : (
- Personel seçin
- )}
-
-
-
-
-
- { setAssigneeId(""); setAssigneeOpen(false); }} />
- Atanmamış
-
- {members.map((m) => (
-
- { setAssigneeId(m.id); setAssigneeOpen(false); }} />
-
-
- ))}
-
-
-
- )}
-
- {/* Notlar */}
-
- Notlar
-
-
-
-
-
-
- onOpenChange(false)} disabled={isPending}>Vazgeç
-
- {isPending ? <> Kaydediliyor...> : <> {isEdit ? "Güncelle" : "Kaydet"}>}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/leads/components/leads-board.tsx b/src/app/(dashboard)/leads/components/leads-board.tsx
deleted file mode 100644
index 2b23a24..0000000
--- a/src/app/(dashboard)/leads/components/leads-board.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-"use client";
-
-import { useMemo, useState, useTransition } from "react";
-import {
- DndContext,
- type DragEndEvent,
- DragOverlay,
- type DragStartEvent,
- PointerSensor,
- closestCorners,
- useDroppable,
- useSensor,
- useSensors,
-} from "@dnd-kit/core";
-import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
-import { Loader2, Plus, Trash2 } from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,
-} from "@/components/ui/dialog";
-import { cn } from "@/lib/utils";
-import { deleteLeadAction, moveLeadAction } from "@/lib/appwrite/lead-actions";
-
-import { LeadCard } from "./lead-card";
-import { LeadFormSheet } from "./lead-form-sheet";
-import { LeadDetailSheet } from "./lead-detail-sheet";
-import {
- COLUMNS,
- LEAD_STATUS_CONFIG,
- type ActivityRow,
- type LeadRow,
- type LeadStatus,
- type MemberOption,
-} from "./types";
-import { listLeadActivitiesForClient } from "./lead-activities-fetcher";
-
-type Props = {
- leads: LeadRow[];
- members: MemberOption[];
- currentUserId: string;
-};
-
-function Column({
- status,
- leads,
- onAdd,
- onEdit,
- onDetail,
- onDelete,
-}: {
- status: LeadStatus;
- leads: LeadRow[];
- onAdd: (status: LeadStatus) => void;
- onEdit: (lead: LeadRow) => void;
- onDetail: (lead: LeadRow) => void;
- onDelete: (lead: LeadRow) => void;
-}) {
- const { setNodeRef, isOver } = useDroppable({
- id: `col-${status}`,
- data: { type: "column", status },
- });
- const cfg = LEAD_STATUS_CONFIG[status];
- const totalValue = leads.reduce((s, l) => s + (l.estimatedValue ?? 0), 0);
-
- return (
-
-
-
-
{LEAD_STATUS_CONFIG[status].label}
- {leads.length}
-
-
onAdd(status)} aria-label="Yeni aday">
-
-
-
- {totalValue > 0 && (
-
-
- {new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY", maximumFractionDigits: 0 }).format(totalValue)}
-
-
- )}
-
-
l.id)} strategy={verticalListSortingStrategy}>
- {leads.map((lead) => (
-
- ))}
-
- {leads.length === 0 && (
-
Boş
- )}
-
-
- );
-}
-
-export function LeadsBoard({ leads: initialLeads, members, currentUserId: _uid }: Props) {
- const [leads, setLeads] = useState(initialLeads);
- const [activeId, setActiveId] = useState(null);
- const [formOpen, setFormOpen] = useState(false);
- const [formStatus, setFormStatus] = useState("cold");
- const [editing, setEditing] = useState(null);
- const [detailLead, setDetailLead] = useState(null);
- const [detailActivities, setDetailActivities] = useState([]);
- const [detailOpen, setDetailOpen] = useState(false);
- const [deleting, setDeleting] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
-
- const grouped = useMemo(() => {
- const map = Object.fromEntries(COLUMNS.map((c) => [c.key, [] as LeadRow[]])) as Record;
- for (const l of leads) map[l.status].push(l);
- return map;
- }, [leads]);
-
- const activeLead = useMemo(() => leads.find((l) => l.id === activeId) ?? null, [leads, activeId]);
-
- const onDragStart = (e: DragStartEvent) => setActiveId(String(e.active.id));
-
- const onDragEnd = (e: DragEndEvent) => {
- const { active, over } = e;
- setActiveId(null);
- if (!over) return;
-
- const overData = over.data.current as { type?: string; status?: LeadStatus } | undefined;
- let targetStatus: LeadStatus | undefined;
- if (overData?.type === "column") targetStatus = overData.status;
- else if (overData?.type === "lead") targetStatus = overData.status;
- if (!targetStatus) return;
-
- const src = leads.find((l) => l.id === active.id);
- if (!src || src.status === targetStatus) return;
-
- setLeads((prev) => prev.map((l) => l.id === src.id ? { ...l, status: targetStatus! } : l));
-
- startTransition(async () => {
- const result = await moveLeadAction(src.id, targetStatus!);
- if (!result.ok) {
- setLeads((prev) => prev.map((l) => l.id === src.id ? { ...l, status: src.status } : l));
- toast.error(result.error ?? "Taşıma başarısız.");
- }
- });
- };
-
- const openDetail = async (lead: LeadRow) => {
- setDetailLead(lead);
- setDetailActivities([]);
- setDetailOpen(true);
- const acts = await listLeadActivitiesForClient(lead.id);
- setDetailActivities(acts);
- };
-
- const handleDelete = () => {
- if (!deleting) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deleting.id);
- const result = await deleteLeadAction(fd);
- if (result.ok) {
- toast.success("Aday silindi.");
- setLeads((prev) => prev.filter((l) => l.id !== deleting.id));
- setDeleting(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- useMemo(() => setLeads(initialLeads), [initialLeads]);
-
- return (
- <>
-
-
- Toplam {leads.length} aday
- {leads.filter((l) => l.status === "converted").length > 0 && (
-
- · {leads.filter((l) => l.status === "converted").length} kazanıldı
-
- )}
-
-
{ setEditing(null); setFormStatus("cold"); setFormOpen(true); }}>
-
- Yeni aday
-
-
-
-
-
- {COLUMNS.map((col) => (
- { setEditing(null); setFormStatus(s); setFormOpen(true); }}
- onEdit={(l) => { setEditing(l); setFormOpen(true); }}
- onDetail={openDetail}
- onDelete={(l) => setDeleting(l)}
- />
- ))}
-
-
-
- {activeLead && (
- {}} onDetail={() => {}} onDelete={() => {}} isOverlay />
- )}
-
-
-
- { setFormOpen(v); if (!v) setEditing(null); }}
- lead={editing}
- defaultStatus={formStatus}
- members={members}
- onCreated={(id) => {
- // Detail sheet will reload on next open; no-op here
- }}
- />
-
- { setDetailOpen(false); setEditing(l); setFormOpen(true); }}
- />
-
- !v && setDeleting(null)}>
-
-
- Adayı sil
-
- {deleting?.name} adlı aday ve tüm aktiviteleri kalıcı olarak silinecek.
-
-
-
- setDeleting(null)} disabled={busy}>Vazgeç
-
- {busy ? : }
- Sil
-
-
-
-
- >
- );
-}
diff --git a/src/app/(dashboard)/leads/components/types.ts b/src/app/(dashboard)/leads/components/types.ts
deleted file mode 100644
index bf6e64a..0000000
--- a/src/app/(dashboard)/leads/components/types.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-import type { LeadActivityType, LeadSource, LeadStatus } from "@/lib/appwrite/schema";
-
-export type { LeadStatus, LeadSource, LeadActivityType };
-
-export type LeadRow = {
- id: string;
- name: string;
- contactName: string;
- email: string;
- phone: string;
- source: LeadSource;
- status: LeadStatus;
- estimatedValue: number | null;
- currency: string;
- notes: string;
- assigneeId: string;
- assigneeName: string;
- lastContactAt: string | null;
- nextFollowUpAt: string | null;
- calendarEventId: string | null;
- customerId: string | null;
- createdAt: string;
-};
-
-export type ActivityRow = {
- id: string;
- leadId: string;
- type: LeadActivityType;
- content: string;
- calendarEventId: string | null;
- occurredAt: string | null;
- createdAt: string;
- createdByName: string;
-};
-
-export type MemberOption = { id: string; name: string; email: string };
-
-export const LEAD_STATUS_CONFIG: Record<
- LeadStatus,
- { label: string; color: string; bg: string; border: string }
-> = {
- cold: { label: "Soğuk", color: "text-blue-600 dark:text-blue-400", bg: "bg-blue-500/10", border: "border-blue-500/30" },
- warm: { label: "Ilık", color: "text-orange-600 dark:text-orange-400", bg: "bg-orange-500/10", border: "border-orange-500/30" },
- hot: { label: "Sıcak", color: "text-red-600 dark:text-red-400", bg: "bg-red-500/10", border: "border-red-500/30" },
- converted: { label: "Kazanıldı", color: "text-green-600 dark:text-green-400", bg: "bg-green-500/10", border: "border-green-500/30" },
- lost: { label: "Kaybedildi",color: "text-muted-foreground", bg: "bg-muted/40", border: "border-border" },
-};
-
-export const LEAD_SOURCE_LABEL: Record = {
- website: "Website",
- social: "Sosyal medya",
- referral: "Referans",
- cold_call: "Soğuk arama",
- event: "Fuar / Etkinlik",
- other: "Diğer",
-};
-
-export const ACTIVITY_TYPE_CONFIG: Record = {
- note: { label: "Not", icon: "📝" },
- call: { label: "Arama", icon: "📞" },
- meeting: { label: "Toplantı", icon: "🤝" },
- email: { label: "E-posta", icon: "✉️" },
- status_change: { label: "Durum değişti", icon: "🔄" },
-};
-
-export const COLUMNS: { key: LeadStatus; title: string }[] = [
- { key: "cold", title: "Soğuk" },
- { key: "warm", title: "Ilık" },
- { key: "hot", title: "Sıcak" },
- { key: "converted", title: "Kazanıldı" },
- { key: "lost", title: "Kaybedildi" },
-];
diff --git a/src/app/(dashboard)/leads/page.tsx b/src/app/(dashboard)/leads/page.tsx
deleted file mode 100644
index 53ec0fa..0000000
--- a/src/app/(dashboard)/leads/page.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listLeads } from "@/lib/appwrite/lead-queries";
-import { createAdminClient } from "@/lib/appwrite/server";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-
-import { LeadsBoard } from "./components/leads-board";
-import type { LeadRow } from "./components/types";
-
-export const metadata: Metadata = {
- title: "İşletmem — Müşteri Adayları",
-};
-
-export default async function LeadsPage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const { teams } = createAdminClient();
- const [leads, membershipsResult] = await Promise.all([
- listLeads(ctx.tenantId),
- teams.listMemberships(ctx.tenantId).catch(() => ({ memberships: [] })),
- ]);
-
- const memberMap = new Map(
- membershipsResult.memberships
- .filter((m) => m.confirm)
- .map((m) => [m.userId, m.userName || m.userEmail]),
- );
-
- const members = membershipsResult.memberships
- .filter((m) => m.confirm)
- .map((m) => ({ id: m.userId, name: m.userName || m.userEmail, email: m.userEmail }));
-
- const leadRows: LeadRow[] = leads.map((l) => ({
- id: l.$id,
- name: l.name,
- contactName: l.contactName ?? "",
- email: l.email ?? "",
- phone: l.phone ?? "",
- source: l.source ?? "other",
- status: l.status ?? "cold",
- estimatedValue: l.estimatedValue ?? null,
- currency: l.currency ?? "TRY",
- notes: l.notes ?? "",
- assigneeId: l.assigneeId ?? "",
- assigneeName: l.assigneeId ? (memberMap.get(l.assigneeId) ?? "") : "",
- lastContactAt: l.lastContactAt ?? null,
- nextFollowUpAt: l.nextFollowUpAt ?? null,
- calendarEventId: l.calendarEventId ?? null,
- customerId: l.customerId ?? null,
- createdAt: l.$createdAt,
- }));
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Müşteri Adayları
-
- Müşteri adaylarını takip edin, ısıtın ve müşteriye dönüştürün.
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/mail/components/account-switcher.tsx b/src/app/(dashboard)/mail/components/account-switcher.tsx
deleted file mode 100644
index f146ea8..0000000
--- a/src/app/(dashboard)/mail/components/account-switcher.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-"use client"
-
-import * as React from "react"
-
-import { cn } from "@/lib/utils"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-
-interface AccountSwitcherProps {
- isCollapsed: boolean;
- accounts: {
- label: string;
- email: string;
- icon: React.ReactNode;
- }[];
-}
-
-export function AccountSwitcher({ isCollapsed, accounts }: AccountSwitcherProps) {
- const [selectedAccount, setSelectedAccount] = React.useState(
- accounts[0].email
- );
-
- return (
-
- span]:w-auto [&>svg]:hidden"
- )}
- aria-label="Select account"
- >
-
- {accounts.find((account) => account.email === selectedAccount)?.icon}
-
- {accounts.find((account) => account.email === selectedAccount)?.label}
-
-
-
-
- {accounts.map((account) => (
-
-
- {account.icon}
- {account.email}
-
-
- ))}
-
-
- );
-}
diff --git a/src/app/(dashboard)/mail/components/mail-display.tsx b/src/app/(dashboard)/mail/components/mail-display.tsx
deleted file mode 100644
index 62fc327..0000000
--- a/src/app/(dashboard)/mail/components/mail-display.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-"use client"
-
-import { addDays } from "date-fns";
-import { addHours } from "date-fns";
-import { format } from "date-fns";
-import { nextSaturday } from "date-fns";
-import {
- Archive,
- ArchiveX,
- Clock,
- Forward,
- MoreVertical,
- Reply,
- ReplyAll,
- Trash2,
-} from "lucide-react";
-
-import { DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { Button } from "@/components/ui/button";
-import { Calendar } from "@/components/ui/calendar";
-import { DropdownMenu, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
-import { Label } from "@/components/ui/label";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { Separator } from "@/components/ui/separator";
-import { Switch } from "@/components/ui/switch";
-import { Textarea } from "@/components/ui/textarea";
-import { type Mail } from "../data";
-import { useState } from "react";
-
-interface MailDisplayProps {
- mail: Mail | null;
-}
-
-export function MailDisplay({ mail }: MailDisplayProps) {
- const [selectedDate, setSelectedDate] = useState(new Date());
-
- return (
-
-
-
-
-
- Archive
-
-
-
- Move to junk
-
-
-
- Move to trash
-
-
-
-
-
-
- Snooze
-
-
-
-
-
Snooze until
-
-
- Later today{" "}
-
- {format(addHours(selectedDate, 4), "E, h:mm b")}
-
-
-
- Tomorrow
-
- {format(addDays(selectedDate, 1), "E, h:mm b")}
-
-
-
- This weekend
-
- {format(nextSaturday(selectedDate), "E, h:mm b")}
-
-
-
- Next week
-
- {format(addDays(selectedDate, 7), "E, h:mm b")}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Reply
-
-
-
- Reply all
-
-
-
- Forward
-
-
-
-
-
-
-
- More
-
-
-
- Mark as unread
- Star thread
- Add label
- Mute thread
-
-
-
-
- {mail ? (
-
-
-
-
-
-
- {mail.name
- .split(" ")
- .map((chunk) => chunk[0])
- .join("")}
-
-
-
-
{mail.name}
-
{mail.subject}
-
- Reply-To: {mail.email}
-
-
-
- {mail.date && (
-
- {format(new Date(mail.date), "PPpp")}
-
- )}
-
-
-
{mail.text}
-
-
-
-
-
-
-
- Mute this thread
-
- e.preventDefault()} size="sm" className="ml-auto cursor-pointer">
- Send
-
-
-
-
-
-
- ) : (
-
No message selected
- )}
-
- );
-}
diff --git a/src/app/(dashboard)/mail/components/mail-list.tsx b/src/app/(dashboard)/mail/components/mail-list.tsx
deleted file mode 100644
index 3ce9353..0000000
--- a/src/app/(dashboard)/mail/components/mail-list.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-"use client"
-
-import type { ComponentProps } from "react"
-import { formatDistanceToNow } from "date-fns"
-
-import { cn } from "@/lib/utils"
-import { Badge } from "@/components/ui/badge"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import type { Mail } from "../data"
-import { useMail } from "../use-mail"
-
-interface MailListProps {
- items: Mail[];
-}
-
-export function MailList({ items }: MailListProps) {
- const [mail, setMail] = useMail();
-
- return (
-
- {items.map((item) => (
-
- setMail({
- ...mail,
- selected: item.id,
- })
- }
- >
-
-
-
-
{item.name}
- {!item.read &&
}
-
-
- {formatDistanceToNow(new Date(item.date), {
- addSuffix: true,
- })}
-
-
-
{item.subject}
-
-
- {item.text.substring(0, 300)}
-
- {item.labels.length ? (
-
- {item.labels.map((label) => (
-
- {label}
-
- ))}
-
- ) : null}
-
- ))}
-
-
- );
-}
-
-function getBadgeVariantFromLabel(label: string): ComponentProps["variant"] {
- if (["work"].includes(label.toLowerCase())) {
- return "default";
- }
-
- if (["personal"].includes(label.toLowerCase())) {
- return "outline";
- }
-
- return "secondary";
-}
diff --git a/src/app/(dashboard)/mail/components/mail.tsx b/src/app/(dashboard)/mail/components/mail.tsx
deleted file mode 100644
index 11f076b..0000000
--- a/src/app/(dashboard)/mail/components/mail.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-"use client"
-
-import * as React from "react"
-import {
- AlertCircle,
- Archive,
- ArchiveX,
- File,
- Inbox,
- MessagesSquare,
- Search,
- Send,
- ShoppingCart,
- Trash2,
- Users2,
-} from "lucide-react"
-
-import { cn } from "@/lib/utils"
-import { Input } from "@/components/ui/input"
-import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
-import { Separator } from "@/components/ui/separator"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { TooltipProvider } from "@/components/ui/tooltip"
-import { AccountSwitcher } from "./account-switcher"
-import { MailDisplay } from "./mail-display"
-import { MailList } from "./mail-list"
-import { Nav } from "./nav"
-import { type Mail } from "../data"
-import { useMail } from "../use-mail"
-import { Button } from "@/components/ui/button"
-
-interface MailProps {
- accounts: {
- label: string;
- email: string;
- icon: React.ReactNode;
- }[];
- mails: Mail[];
- defaultLayout?: number[];
- defaultCollapsed?: boolean;
- navCollapsedSize: number;
-}
-
-export function Mail({
- accounts,
- mails,
- defaultLayout = [20, 32, 48],
- defaultCollapsed = false,
- navCollapsedSize,
-}: MailProps) {
- const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed);
- const [mail] = useMail();
-
- return (
-
- {
- document.cookie = `react-resizable-panels:layout:mail=${JSON.stringify(sizes)}`;
- }}
- className="h-full items-stretch rounded-lg border overflow-hidden"
- >
- {
- setIsCollapsed(true);
- document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(true)}`;
- }}
- onResize={() => {
- setIsCollapsed(false);
- document.cookie = `react-resizable-panels:collapsed=${JSON.stringify(false)}`;
- }}
- className={cn(isCollapsed && "w-full transition-all duration-300 ease-in-out")}
- >
-
-
-
-
- {isCollapsed ? "" : "Compose"}
-
-
-
-
-
-
-
-
-
-
-
-
-
Inbox
-
- All mail
- Unread
-
-
-
-
-
-
-
-
- !item.read)} />
-
-
-
-
-
- item.id === mail.selected) || null} />
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/mail/components/nav.tsx b/src/app/(dashboard)/mail/components/nav.tsx
deleted file mode 100644
index b5fc7e8..0000000
--- a/src/app/(dashboard)/mail/components/nav.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-"use client"
-
-import { type LucideIcon } from "lucide-react"
-
-import { cn } from "@/lib/utils"
-import { Badge } from "@/components/ui/badge"
-import { buttonVariants } from "@/components/ui/button"
-import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
-
-interface NavProps {
- isCollapsed: boolean;
- links: {
- title: string;
- label?: string;
- icon: LucideIcon;
- variant: "default" | "ghost";
- }[];
-}
-
-export function Nav({ links, isCollapsed }: NavProps) {
- return (
-
-
- {links.map((link, index) =>
- isCollapsed ? (
-
-
-
-
- {link.title}
-
-
-
- {link.title}
- {link.label && (
-
- {link.label}
-
- )}
-
-
- ) : (
-
-
- {link.title}
- {link.label && (
-
- {link.label}
-
- )}
-
- )
- )}
-
-
- );
-}
diff --git a/src/app/(dashboard)/mail/data.tsx b/src/app/(dashboard)/mail/data.tsx
deleted file mode 100644
index e191768..0000000
--- a/src/app/(dashboard)/mail/data.tsx
+++ /dev/null
@@ -1,300 +0,0 @@
-export const mails = [
- {
- id: "6c84fb90-12c4-11e1-840d-7b25c5ee775a",
- name: "William Smith",
- email: "williamsmith@example.com",
- subject: "Meeting Tomorrow",
- text: "Hi, let's have a meeting tomorrow to discuss the project. I've been reviewing the project details and have some ideas I'd like to share. It's crucial that we align on our next steps to ensure the project's success.\n\nPlease come prepared with any questions or insights you may have. Looking forward to our meeting!\n\nBest regards, William",
- date: "2023-10-22T09:00:00",
- read: true,
- labels: ["meeting", "work", "important"],
- },
- {
- id: "110e8400-e29b-11d4-a716-446655440000",
- name: "Alice Smith",
- email: "alicesmith@example.com",
- subject: "Re: Project Update",
- text: "Thank you for the project update. It looks great! I've gone through the report, and the progress is impressive. The team has done a fantastic job, and I appreciate the hard work everyone has put in.\n\nI have a few minor suggestions that I'll include in the attached document.\n\nLet's discuss these during our next meeting. Keep up the excellent work!\n\nBest regards, Alice",
- date: "2023-10-22T10:30:00",
- read: true,
- labels: ["work", "important"],
- },
- {
- id: "3e7c3f6d-bdf5-46ae-8d90-171300f27ae2",
- name: "Bob Johnson",
- email: "bobjohnson@example.com",
- subject: "Weekend Plans",
- text: "Any plans for the weekend? I was thinking of going hiking in the nearby mountains. It's been a while since we had some outdoor fun.\n\nIf you're interested, let me know, and we can plan the details. It'll be a great way to unwind and enjoy nature.\n\nLooking forward to your response!\n\nBest, Bob",
- date: "2023-04-10T11:45:00",
- read: true,
- labels: ["personal"],
- },
- {
- id: "61c35085-72d7-42b4-8d62-738f700d4b92",
- name: "Emily Davis",
- email: "emilydavis@example.com",
- subject: "Re: Question about Budget",
- text: "I have a question about the budget for the upcoming project. It seems like there's a discrepancy in the allocation of resources.\n\nI've reviewed the budget report and identified a few areas where we might be able to optimize our spending without compromising the project's quality.\n\nI've attached a detailed analysis for your reference. Let's discuss this further in our next meeting.\n\nThanks, Emily",
- date: "2023-03-25T13:15:00",
- read: false,
- labels: ["work", "budget"],
- },
- {
- id: "8f7b5db9-d935-4e42-8e05-1f1d0a3dfb97",
- name: "Michael Wilson",
- email: "michaelwilson@example.com",
- subject: "Important Announcement",
- text: "I have an important announcement to make during our team meeting. It pertains to a strategic shift in our approach to the upcoming product launch. We've received valuable feedback from our beta testers, and I believe it's time to make some adjustments to better meet our customers' needs.\n\nThis change is crucial to our success, and I look forward to discussing it with the team. Please be prepared to share your insights during the meeting.\n\nRegards, Michael",
- date: "2023-03-10T15:00:00",
- read: false,
- labels: ["meeting", "work", "important"],
- },
- {
- id: "1f0f2c02-e299-40de-9b1d-86ef9e42126b",
- name: "Sarah Brown",
- email: "sarahbrown@example.com",
- subject: "Re: Feedback on Proposal",
- text: "Thank you for your feedback on the proposal. It looks great! I'm pleased to hear that you found it promising. The team worked diligently to address all the key points you raised, and I believe we now have a strong foundation for the project.\n\nI've attached the revised proposal for your review.\n\nPlease let me know if you have any further comments or suggestions. Looking forward to your response.\n\nBest regards, Sarah",
- date: "2023-02-15T16:30:00",
- read: true,
- labels: ["work"],
- },
- {
- id: "17c0a96d-4415-42b1-8b4f-764efab57f66",
- name: "David Lee",
- email: "davidlee@example.com",
- subject: "New Project Idea",
- text: "I have an exciting new project idea to discuss with you. It involves expanding our services to target a niche market that has shown considerable growth in recent months.\n\nI've prepared a detailed proposal outlining the potential benefits and the strategy for execution.\n\nThis project has the potential to significantly impact our business positively. Let's set up a meeting to dive into the details and determine if it aligns with our current goals.\n\nBest regards, David",
- date: "2023-01-28T17:45:00",
- read: false,
- labels: ["meeting", "work", "important"],
- },
- {
- id: "2f0130cb-39fc-44c4-bb3c-0a4337edaaab",
- name: "Olivia Wilson",
- email: "oliviawilson@example.com",
- subject: "Vacation Plans",
- text: "Let's plan our vacation for next month. What do you think? I've been thinking of visiting a tropical paradise, and I've put together some destination options.\n\nI believe it's time for us to unwind and recharge. Please take a look at the options and let me know your preferences.\n\nWe can start making arrangements to ensure a smooth and enjoyable trip.\n\nExcited to hear your thoughts! Olivia",
- date: "2022-12-20T18:30:00",
- read: true,
- labels: ["personal"],
- },
- {
- id: "de305d54-75b4-431b-adb2-eb6b9e546014",
- name: "James Martin",
- email: "jamesmartin@example.com",
- subject: "Re: Conference Registration",
- text: "I've completed the registration for the conference next month. The event promises to be a great networking opportunity, and I'm looking forward to attending the various sessions and connecting with industry experts.\n\nI've also attached the conference schedule for your reference.\n\nIf there are any specific topics or sessions you'd like me to explore, please let me know. It's an exciting event, and I'll make the most of it.\n\nBest regards, James",
- date: "2022-11-30T19:15:00",
- read: true,
- labels: ["work", "conference"],
- },
- {
- id: "7dd90c63-00f6-40f3-bd87-5060a24e8ee7",
- name: "Sophia White",
- email: "sophiawhite@example.com",
- subject: "Team Dinner",
- text: "Let's have a team dinner next week to celebrate our success. We've achieved some significant milestones, and it's time to acknowledge our hard work and dedication.\n\nI've made reservations at a lovely restaurant, and I'm sure it'll be an enjoyable evening.\n\nPlease confirm your availability and any dietary preferences. Looking forward to a fun and memorable dinner with the team!\n\nBest, Sophia",
- date: "2022-11-05T20:30:00",
- read: false,
- labels: ["meeting", "work"],
- },
- {
- id: "99a88f78-3eb4-4d87-87b7-7b15a49a0a05",
- name: "Daniel Johnson",
- email: "danieljohnson@example.com",
- subject: "Feedback Request",
- text: "I'd like your feedback on the latest project deliverables. We've made significant progress, and I value your input to ensure we're on the right track.\n\nI've attached the deliverables for your review, and I'm particularly interested in any areas where you think we can further enhance the quality or efficiency.\n\nYour feedback is invaluable, and I appreciate your time and expertise. Let's work together to make this project a success.\n\nRegards, Daniel",
- date: "2022-10-22T09:30:00",
- read: false,
- labels: ["work"],
- },
- {
- id: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
- name: "Ava Taylor",
- email: "avataylor@example.com",
- subject: "Re: Meeting Agenda",
- text: "Here's the agenda for our meeting next week. I've included all the topics we need to cover, as well as time allocations for each.\n\nIf you have any additional items to discuss or any specific points to address, please let me know, and we can integrate them into the agenda.\n\nIt's essential that our meeting is productive and addresses all relevant matters.\n\nLooking forward to our meeting! Ava",
- date: "2022-10-10T10:45:00",
- read: true,
- labels: ["meeting", "work"],
- },
- {
- id: "c1a0ecb4-2540-49c5-86f8-21e5ce79e4e6",
- name: "William Anderson",
- email: "williamanderson@example.com",
- subject: "Product Launch Update",
- text: "The product launch is on track. I'll provide an update during our call. We've made substantial progress in the development and marketing of our new product.\n\nI'm excited to share the latest updates with you during our upcoming call. It's crucial that we coordinate our efforts to ensure a successful launch. Please come prepared with any questions or insights you may have.\n\nLet's make this product launch a resounding success!\n\nBest regards, William",
- date: "2022-09-20T12:00:00",
- read: false,
- labels: ["meeting", "work", "important"],
- },
- {
- id: "ba54eefd-4097-4949-99f2-2a9ae4d1a836",
- name: "Mia Harris",
- email: "miaharris@example.com",
- subject: "Re: Travel Itinerary",
- text: "I've received the travel itinerary. It looks great! Thank you for your prompt assistance in arranging the details. I've reviewed the schedule and the accommodations, and everything seems to be in order. I'm looking forward to the trip, and I'm confident it'll be a smooth and enjoyable experience.\n\nIf there are any specific activities or attractions you recommend at our destination, please feel free to share your suggestions.\n\nExcited for the trip! Mia",
- date: "2022-09-10T13:15:00",
- read: true,
- labels: ["personal", "travel"],
- },
- {
- id: "df09b6ed-28bd-4e0c-85a9-9320ec5179aa",
- name: "Ethan Clark",
- email: "ethanclark@example.com",
- subject: "Team Building Event",
- text: "Let's plan a team-building event for our department. Team cohesion and morale are vital to our success, and I believe a well-organized team-building event can be incredibly beneficial. I've done some research and have a few ideas for fun and engaging activities.\n\nPlease let me know your thoughts and availability. We want this event to be both enjoyable and productive.\n\nTogether, we'll strengthen our team and boost our performance.\n\nRegards, Ethan",
- date: "2022-08-25T15:30:00",
- read: false,
- labels: ["meeting", "work"],
- },
- {
- id: "d67c1842-7f8b-4b4b-9be1-1b3b1ab4611d",
- name: "Chloe Hall",
- email: "chloehall@example.com",
- subject: "Re: Budget Approval",
- text: "The budget has been approved. We can proceed with the project. I'm delighted to inform you that our budget proposal has received the green light from the finance department. This is a significant milestone, and it means we can move forward with the project as planned.\n\nI've attached the finalized budget for your reference. Let's ensure that we stay on track and deliver the project on time and within budget.\n\nIt's an exciting time for us! Chloe",
- date: "2022-08-10T16:45:00",
- read: true,
- labels: ["work", "budget"],
- },
- {
- id: "6c9a7f94-8329-4d70-95d3-51f68c186ae1",
- name: "Samuel Turner",
- email: "samuelturner@example.com",
- subject: "Weekend Hike",
- text: "Who's up for a weekend hike in the mountains? I've been craving some outdoor adventure, and a hike in the mountains sounds like the perfect escape. If you're up for the challenge, we can explore some scenic trails and enjoy the beauty of nature.\n\nI've done some research and have a few routes in mind.\n\nLet me know if you're interested, and we can plan the details.\n\nIt's sure to be a memorable experience! Samuel",
- date: "2022-07-28T17:30:00",
- read: false,
- labels: ["personal"],
- },
-]
-
-export type Mail = (typeof mails)[number]
-
-export const accounts = [
- {
- label: "Alicia Koch",
- email: "alicia@example.com",
- icon: (
-
- Gmail
-
-
- ),
- },
- {
- label: "Alicia Koch",
- email: "alicia2@example.com",
- icon: (
-
- Vercel
-
-
- ),
- },
- {
- label: "Alicia Koch",
- email: "alicia3@example.com",
- icon: (
-
- iCloud
-
-
- ),
- },
-]
-
-export type Account = (typeof accounts)[number]
-
-export const contacts = [
- {
- name: "Emma Johnson",
- email: "emma.johnson@example.com",
- },
- {
- name: "Liam Wilson",
- email: "liam.wilson@example.com",
- },
- {
- name: "Olivia Davis",
- email: "olivia.davis@example.com",
- },
- {
- name: "Noah Martinez",
- email: "noah.martinez@example.com",
- },
- {
- name: "Ava Taylor",
- email: "ava.taylor@example.com",
- },
- {
- name: "Lucas Brown",
- email: "lucas.brown@example.com",
- },
- {
- name: "Sophia Smith",
- email: "sophia.smith@example.com",
- },
- {
- name: "Ethan Wilson",
- email: "ethan.wilson@example.com",
- },
- {
- name: "Isabella Jackson",
- email: "isabella.jackson@example.com",
- },
- {
- name: "Mia Clark",
- email: "mia.clark@example.com",
- },
- {
- name: "Mason Lee",
- email: "mason.lee@example.com",
- },
- {
- name: "Layla Harris",
- email: "layla.harris@example.com",
- },
- {
- name: "William Anderson",
- email: "william.anderson@example.com",
- },
- {
- name: "Ella White",
- email: "ella.white@example.com",
- },
- {
- name: "James Thomas",
- email: "james.thomas@example.com",
- },
- {
- name: "Harper Lewis",
- email: "harper.lewis@example.com",
- },
- {
- name: "Benjamin Moore",
- email: "benjamin.moore@example.com",
- },
- {
- name: "Aria Hall",
- email: "aria.hall@example.com",
- },
- {
- name: "Henry Turner",
- email: "henry.turner@example.com",
- },
- {
- name: "Scarlett Adams",
- email: "scarlett.adams@example.com",
- },
-]
-
-export type Contact = (typeof contacts)[number]
diff --git a/src/app/(dashboard)/mail/index.tsx b/src/app/(dashboard)/mail/index.tsx
deleted file mode 100644
index a3615a0..0000000
--- a/src/app/(dashboard)/mail/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Mail } from "./components/mail";
-import { accounts, mails } from "./data";
-
-export default function MailPage() {
- return ;
-}
diff --git a/src/app/(dashboard)/mail/page.tsx b/src/app/(dashboard)/mail/page.tsx
deleted file mode 100644
index aefcb58..0000000
--- a/src/app/(dashboard)/mail/page.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Mail } from "./components/mail"
-import { accounts, mails } from "./data"
-
-export default function MailPage() {
- return (
-
- )
-}
diff --git a/src/app/(dashboard)/mail/use-mail.ts b/src/app/(dashboard)/mail/use-mail.ts
deleted file mode 100644
index 39a3975..0000000
--- a/src/app/(dashboard)/mail/use-mail.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { create } from "zustand";
-import type { Mail } from "./data";
-import { mails } from "./data";
-
-interface Config {
- selected: Mail["id"] | null;
-}
-
-const useMailStore = create<
- Config & { setState: (newState: Partial) => void }
->((set) => ({
- selected: mails[0].id,
- setState: (newState) => set((state) => ({ ...state, ...newState })),
-}));
-
-export function useMail(): [Config, (newState: Partial) => void] {
- const selected = useMailStore((state) => state.selected);
- const setState = useMailStore((state) => state.setState);
- return [{ selected }, setState];
-}
diff --git a/src/app/(dashboard)/presentations/page.tsx b/src/app/(dashboard)/presentations/page.tsx
new file mode 100644
index 0000000..b0ff874
--- /dev/null
+++ b/src/app/(dashboard)/presentations/page.tsx
@@ -0,0 +1,8 @@
+export default function Page() {
+ return (
+
+
presentations
+
Yakında...
+
+ );
+}
diff --git a/src/app/(dashboard)/pricing/components/faq-section.tsx b/src/app/(dashboard)/pricing/components/faq-section.tsx
deleted file mode 100644
index 91a4d8b..0000000
--- a/src/app/(dashboard)/pricing/components/faq-section.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
-
-interface FAQ {
- id: number
- question: string
- answer: string
-}
-
-interface FAQSectionProps {
- faqs: FAQ[]
-}
-
-export function FAQSection({ faqs }: FAQSectionProps) {
- return (
-
-
- Frequently Asked Questions
-
- Get answers to the most common questions about our pricing and plans
-
-
-
-
- {/* Left Column */}
-
-
- {faqs.slice(0, 3).map(item => (
-
- {item.question}
- {item.answer}
-
- ))}
-
-
-
- {/* Right Column */}
-
-
- {faqs.slice(3, 6).map(item => (
-
- {item.question}
- {item.answer}
-
- ))}
-
-
-
-
-
- )
-}
diff --git a/src/app/(dashboard)/pricing/components/features-grid.tsx b/src/app/(dashboard)/pricing/components/features-grid.tsx
deleted file mode 100644
index 9da5f03..0000000
--- a/src/app/(dashboard)/pricing/components/features-grid.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Rocket, Shield, Zap, Users, Headphones, Clock } from "lucide-react"
-
-// Icon mapping
-const iconMap = {
- Rocket,
- Shield,
- Zap,
- Users,
- Headphones,
- Clock,
-}
-
-interface Feature {
- id: number
- name: string
- description: string
- icon: string
-}
-
-interface FeaturesGridProps {
- features: Feature[]
-}
-
-export function FeaturesGrid({ features }: FeaturesGridProps) {
- return (
-
-
- All Plans Include
-
- Every plan comes with these essential features to help your team succeed
-
-
-
-
-
- {features.map(feature => {
- const IconComponent = iconMap[feature.icon as keyof typeof iconMap]
- return (
-
-
-
{feature.description}
-
- )
- })}
-
-
-
-
- )
-}
diff --git a/src/app/(dashboard)/pricing/data/faqs.json b/src/app/(dashboard)/pricing/data/faqs.json
deleted file mode 100644
index e37d4d3..0000000
--- a/src/app/(dashboard)/pricing/data/faqs.json
+++ /dev/null
@@ -1,32 +0,0 @@
-[
- {
- "id": 1,
- "question": "Can I change my plan anytime?",
- "answer": "Yes, you can upgrade or downgrade your plan at any time. Changes will be reflected in your next billing cycle, and you'll be charged or credited accordingly."
- },
- {
- "id": 2,
- "question": "Is there a free trial available?",
- "answer": "Yes, all plans come with a 14-day free trial. No credit card is required to start your trial, and you can explore all features during this period."
- },
- {
- "id": 3,
- "question": "What payment methods do you accept?",
- "answer": "We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for enterprise customers. All payments are processed securely."
- },
- {
- "id": 4,
- "question": "Do you offer discounts for annual plans?",
- "answer": "Yes, save 20% when you choose annual billing on any plan. You can switch to annual billing from your account settings at any time."
- },
- {
- "id": 5,
- "question": "What happens if I exceed my plan limits?",
- "answer": "If you exceed your plan limits, we'll notify you in advance. You can either upgrade your plan or purchase additional resources as needed."
- },
- {
- "id": 6,
- "question": "Can I cancel my subscription anytime?",
- "answer": "Yes, you can cancel your subscription at any time from your account settings. You'll continue to have access to all features until the end of your current billing period."
- }
-]
diff --git a/src/app/(dashboard)/pricing/data/features.json b/src/app/(dashboard)/pricing/data/features.json
deleted file mode 100644
index 740d3ac..0000000
--- a/src/app/(dashboard)/pricing/data/features.json
+++ /dev/null
@@ -1,38 +0,0 @@
-[
- {
- "id": 1,
- "name": "Fast Performance",
- "description": "Lightning-fast response times and optimized performance for all your business needs.",
- "icon": "Rocket"
- },
- {
- "id": 2,
- "name": "Enterprise Security",
- "description": "Bank-level security with end-to-end encryption and advanced threat protection.",
- "icon": "Shield"
- },
- {
- "id": 3,
- "name": "Instant Setup",
- "description": "Get up and running in minutes with our streamlined onboarding process.",
- "icon": "Zap"
- },
- {
- "id": 4,
- "name": "Team Collaboration",
- "description": "Seamless collaboration tools to keep your team connected and productive.",
- "icon": "Users"
- },
- {
- "id": 5,
- "name": "24/7 Support",
- "description": "Round-the-clock expert support whenever you need help or have questions.",
- "icon": "Headphones"
- },
- {
- "id": 6,
- "name": "Real-time Analytics",
- "description": "Monitor your business performance with real-time insights and detailed analytics.",
- "icon": "Clock"
- }
-]
diff --git a/src/app/(dashboard)/pricing/page.tsx b/src/app/(dashboard)/pricing/page.tsx
deleted file mode 100644
index ed72e13..0000000
--- a/src/app/(dashboard)/pricing/page.tsx
+++ /dev/null
@@ -1,295 +0,0 @@
-import { redirect } from "next/navigation";
-import { Building2, Check, Clock, Crown, Sparkles, Stethoscope } from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import { Progress } from "@/components/ui/progress";
-import { cn } from "@/lib/utils";
-import {
- RESOURCE_LABELS,
- getEffectivePlan,
- getPlanUsage,
- type PlanResource,
-} from "@/lib/appwrite/plan-limits";
-import {
- downgradeToFreeAction,
- startCheckoutAction,
-} from "@/lib/appwrite/subscription-actions";
-import { isShopierEnabled } from "@/lib/payments/shopier";
-import { PLAN_CATALOG } from "@/lib/appwrite/subscription-types";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-
-const trFmt = new Intl.NumberFormat("tr-TR", {
- style: "currency",
- currency: "TRY",
- maximumFractionDigits: 0,
-});
-
-type EcosystemTier = {
- id: "klinik" | "ajans";
- name: string;
- description: string;
- Icon: typeof Stethoscope;
- features: string[];
-};
-
-const ECOSYSTEM_TIERS: EcosystemTier[] = [
- {
- id: "klinik",
- name: "Kliniğim",
- description: "Hekim, klinik ve sağlık merkezleri için.",
- Icon: Stethoscope,
- features: [
- "Hasta kaydı + KVKK uyumlu dosyalama",
- "Randevu + hatırlatma",
- "Reçete ve tetkik takibi",
- "Klinik finans paneli",
- ],
- },
- {
- id: "ajans",
- name: "Ajansım",
- description: "Yaratıcı ajanslar ve danışmanlıklar için.",
- Icon: Building2,
- features: [
- "Proje + saat takibi",
- "Müşteri portalı",
- "Brief + onay akışı",
- "Ajans bazlı raporlama",
- ],
- },
-];
-
-export default async function PricingPage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const currentPlan = getEffectivePlan(ctx);
- const isPro = currentPlan === "pro";
- const canManage = ctx.role === "owner";
- const usage = await getPlanUsage(ctx);
- const shopierActive = isShopierEnabled();
-
- const resources: PlanResource[] = ["customers", "financeEntries", "software", "members"];
-
- const tiers = [
- {
- ...PLAN_CATALOG.free,
- isCurrent: !isPro,
- isPopular: false,
- },
- {
- ...PLAN_CATALOG.pro,
- isCurrent: isPro,
- isPopular: true,
- },
- ];
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Plan
-
- İşletmem'i ölçeğine göre kullan. Sektörel paketler (Kliniğim, Ajansım) yakında.
-
-
-
-
-
- Bu ayki kullanımın
-
- Mevcut planın sınırlarına ne kadar yaklaştığını gör.
-
-
-
- {resources.map((r) => {
- const u = usage.usage[r];
- const pct =
- u.limit === Number.POSITIVE_INFINITY
- ? 0
- : Math.min(100, Math.round((u.used / Math.max(1, u.limit)) * 100));
- const limitLabel =
- u.limit === Number.POSITIVE_INFINITY ? "∞" : String(u.limit);
- return (
-
-
- {RESOURCE_LABELS[r]}
-
- {u.used} / {limitLabel}
-
-
- {u.limit !== Number.POSITIVE_INFINITY && (
-
- )}
-
- );
- })}
-
-
-
-
-
-
İşletmem planları
-
- Tek para birimi: ₺ (TRY)
-
-
-
-
- {tiers.map((tier) => (
-
- {tier.isCurrent && (
-
-
-
- Mevcut plan
-
-
- )}
- {tier.isPopular && !tier.isCurrent && (
-
-
-
- Önerilen
-
-
- )}
-
- {tier.name}
- {tier.description}
-
-
-
- {trFmt.format(tier.price)}
- /ay
-
-
- {tier.features.map((feature) => (
-
- ))}
-
-
-
- {tier.isCurrent ? (
-
- Mevcut plan
-
- ) : !canManage ? (
-
- Sahip yetkisi gerekli
-
- ) : tier.id === "pro" ? (
-
-
-
-
- {shopierActive ? "Pro'ya geç" : "Pro'ya geç (Test)"}
-
-
- ) : (
-
-
- Ücretsiz'e dön
-
-
- )}
-
-
- ))}
-
-
-
-
-
-
Ekosistem paketleri
-
-
- Yakında
-
-
-
- Sektörel modüller İşletmem'in üzerine eklenecek. Aynı hesabınla farklı şirketleri tek
- panelden yöneteceksin.
-
-
-
- {ECOSYSTEM_TIERS.map((t) => (
-
-
-
- {t.name}
- {t.description}
-
-
-
- {t.features.map((feature) => (
-
- ))}
-
-
-
-
- Geliştirme aşamasında
-
-
-
- ))}
-
-
-
- {!shopierActive && (
-
-
-
- Test modu: Pro plan şu anda mock
- ödeme akışıyla çalışır. Shopier entegrasyonu aktif edilince gerçek tahsilat başlayacak.
-
-
-
- )}
-
- );
-}
diff --git a/src/app/(dashboard)/properties/page.tsx b/src/app/(dashboard)/properties/page.tsx
new file mode 100644
index 0000000..6207c5b
--- /dev/null
+++ b/src/app/(dashboard)/properties/page.tsx
@@ -0,0 +1,8 @@
+export default function Page() {
+ return (
+
+
properties
+
Yakında...
+
+ );
+}
diff --git a/src/app/(dashboard)/services/components/delete-service-dialog.tsx b/src/app/(dashboard)/services/components/delete-service-dialog.tsx
deleted file mode 100644
index 5ddc95e..0000000
--- a/src/app/(dashboard)/services/components/delete-service-dialog.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-"use client";
-
-import { useTransition } from "react";
-import { Loader2, Trash2 } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { deleteServiceAction } from "@/lib/appwrite/service-actions";
-
-export function DeleteServiceDialog({
- open,
- onOpenChange,
- id,
- name,
-}: {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- id: string | null;
- name: string;
-}) {
- const [isPending, startTransition] = useTransition();
-
- const handleDelete = () => {
- if (!id) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", id);
- const result = await deleteServiceAction(fd);
- if (result.ok) {
- toast.success("Hizmet silindi.");
- onOpenChange(false);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- return (
-
-
-
- Hizmeti sil
-
- {name} kalıcı olarak silinecek. Bu işlem geri alınamaz.
-
-
-
- onOpenChange(false)} disabled={isPending}>
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Siliniyor...
- >
- ) : (
- <>
-
- Sil
- >
- )}
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/services/components/service-form-sheet.tsx b/src/app/(dashboard)/services/components/service-form-sheet.tsx
deleted file mode 100644
index de5b963..0000000
--- a/src/app/(dashboard)/services/components/service-form-sheet.tsx
+++ /dev/null
@@ -1,350 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState } from "react";
-import { Check, ChevronDown, Loader2, Save, Users } from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Switch } from "@/components/ui/switch";
-import { Textarea } from "@/components/ui/textarea";
-import { cn } from "@/lib/utils";
-import {
- createServiceAction,
- updateServiceAction,
-} from "@/lib/appwrite/service-actions";
-import { initialServiceState } from "@/lib/appwrite/service-types";
-import type { CustomerOption, MemberOption, ServiceRow } from "./types";
-
-const PRESET_SERVICES = [
- "Web sitesi tasarımı",
- "Web sitesi bakımı",
- "SEO optimizasyonu",
- "Sosyal medya yönetimi",
- "Domain kayıt / yenileme",
- "Hosting hizmeti",
- "Kurumsal e-posta",
- "Grafik tasarım",
- "Logo tasarımı",
- "Google Ads yönetimi",
- "Meta Ads yönetimi",
- "Yazılım geliştirme",
- "Mobil uygulama",
- "Teknik destek",
- "Muhasebe danışmanlığı",
- "Eğitim / danışmanlık",
-];
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- service?: ServiceRow | null;
- customers: CustomerOption[];
- defaultCustomerId?: string;
- members: MemberOption[];
-};
-
-export function ServiceFormSheet({
- open,
- onOpenChange,
- service,
- customers,
- defaultCustomerId,
- members,
-}: Props) {
- const isEdit = Boolean(service);
- const action = isEdit ? updateServiceAction : createServiceAction;
- const [state, formAction, isPending] = useActionState(action, initialServiceState);
-
- const [name, setName] = useState(service?.name ?? "");
- const [assigneeIds, setAssigneeIds] = useState(service?.assigneeIds ?? []);
- const [assigneeOpen, setAssigneeOpen] = useState(false);
-
- // Reset local state when sheet opens with a different service
- useEffect(() => {
- if (open) {
- setName(service?.name ?? "");
- setAssigneeIds(service?.assigneeIds ?? []);
- }
- }, [open, service]);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Hizmet güncellendi." : "Hizmet eklendi.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- const toggleAssignee = (id: string) => {
- setAssigneeIds((prev) =>
- prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
- );
- };
-
- return (
-
-
-
- {isEdit ? "Hizmeti düzenle" : "Yeni hizmet"}
-
- {customers.length === 0
- ? "Hizmet eklemek için önce en az bir müşteri tanımlamalısınız."
- : "Müşteriye sunduğunuz hizmeti tanımlayın."}
-
-
-
-
- {isEdit && service && }
- {/* Assignee hidden inputs — one per selected member */}
- {assigneeIds.map((id) => (
-
- ))}
-
-
- {/* Müşteri */}
-
-
Müşteri *
-
-
-
-
-
- {customers.map((c) => (
-
- {c.name}
-
- ))}
-
-
- {state.fieldErrors?.customerId && (
-
{state.fieldErrors.customerId}
- )}
-
-
- {/* Hizmet adı + hazır şablonlar */}
-
-
Hizmet adı *
- {/* Preset chips */}
-
- {PRESET_SERVICES.map((preset) => (
- setName(preset)}
- className={cn(
- "rounded-full border px-2.5 py-0.5 text-xs transition-colors",
- name === preset
- ? "border-primary bg-primary text-primary-foreground"
- : "border-border bg-muted/40 text-muted-foreground hover:border-primary/50 hover:text-foreground",
- )}
- >
- {preset}
-
- ))}
-
-
setName(e.target.value)}
- placeholder="Hizmet adını yazın veya yukarıdan seçin"
- required
- />
- {state.fieldErrors?.name && (
-
{state.fieldErrors.name}
- )}
-
-
- {/* Açıklama */}
-
- Açıklama
-
-
-
- {/* Fiyat + Para birimi */}
-
-
-
Birim fiyat *
-
- {state.fieldErrors?.unitPrice && (
-
{state.fieldErrors.unitPrice}
- )}
-
-
- Para birimi
-
-
-
-
-
- ₺ TRY
- $ USD
- € EUR
-
-
-
-
-
- {/* Faturalama dönemi + Tekrarlayan */}
-
-
- Faturalama dönemi
-
-
-
-
-
- Tek seferlik
- Aylık
- Yıllık
-
-
-
-
-
-
- Tekrarlayan
-
-
-
-
-
-
- {/* Sorumlu personel */}
- {members.length > 0 && (
-
-
Sorumlu personel
-
-
-
-
- {assigneeIds.length === 0 ? (
-
- Personel seçin (isteğe bağlı)
-
- ) : (
- assigneeIds.map((id) => {
- const m = members.find((x) => x.id === id);
- return (
-
- {m?.name ?? id}
-
- );
- })
- )}
-
-
-
-
-
- {members.map((m) => {
- const checked = assigneeIds.includes(m.id);
- return (
-
- toggleAssignee(m.id)}
- />
-
- {checked && }
-
- );
- })}
-
-
-
- )}
-
-
-
-
- onOpenChange(false)}
- disabled={isPending}
- >
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Kaydet"}
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/services/components/services-client.tsx b/src/app/(dashboard)/services/components/services-client.tsx
deleted file mode 100644
index 4835297..0000000
--- a/src/app/(dashboard)/services/components/services-client.tsx
+++ /dev/null
@@ -1,333 +0,0 @@
-"use client";
-
-import { useMemo, useState } from "react";
-import {
- type ColumnDef,
- type SortingState,
- flexRender,
- getCoreRowModel,
- getFilteredRowModel,
- getPaginationRowModel,
- getSortedRowModel,
- useReactTable,
-} from "@tanstack/react-table";
-import {
- ArrowUpDown,
- Briefcase,
- ChevronLeft,
- ChevronRight,
- MoreHorizontal,
- Pencil,
- Plus,
- Repeat,
- Search,
- Trash2,
-} from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Input } from "@/components/ui/input";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { BILLING_PERIOD_LABEL, formatCurrency } from "@/lib/format";
-
-import { ServiceFormSheet } from "./service-form-sheet";
-import { DeleteServiceDialog } from "./delete-service-dialog";
-import type { CustomerOption, MemberOption, ServiceRow } from "./types";
-
-type Props = {
- services: ServiceRow[];
- customers: CustomerOption[];
- members: MemberOption[];
-};
-
-export function ServicesClient({ services, customers, members }: Props) {
- const memberMap = useMemo(() => new Map(members.map((m) => [m.id, m.name])), [members]);
- const [globalFilter, setGlobalFilter] = useState("");
- const [sorting, setSorting] = useState([]);
- const [formOpen, setFormOpen] = useState(false);
- const [editing, setEditing] = useState(null);
- const [deleting, setDeleting] = useState(null);
-
- const columns = useMemo[]>(
- () => [
- {
- accessorKey: "name",
- header: ({ column }) => (
- column.toggleSorting(column.getIsSorted() === "asc")}
- >
- Hizmet
-
-
- ),
- cell: ({ row }) => (
-
- {row.original.name}
- {row.original.description && (
-
- {row.original.description}
-
- )}
-
- ),
- },
- {
- accessorKey: "customerName",
- header: "Müşteri",
- cell: ({ row }) => {row.original.customerName} ,
- },
- {
- accessorKey: "unitPrice",
- header: ({ column }) => (
- column.toggleSorting(column.getIsSorted() === "asc")}
- >
- Fiyat
-
-
- ),
- cell: ({ row }) => (
-
- {formatCurrency(row.original.unitPrice, row.original.currency)}
-
- ),
- },
- {
- accessorKey: "billingPeriod",
- header: "Dönem",
- cell: ({ row }) => (
-
- {BILLING_PERIOD_LABEL[row.original.billingPeriod]}
- {row.original.recurring && (
-
- )}
-
- ),
- },
- {
- id: "assignees",
- header: "Personel",
- cell: ({ row }) => {
- const names = row.original.assigneeIds
- .map((id) => memberMap.get(id))
- .filter(Boolean) as string[];
- if (names.length === 0) return — ;
- return (
-
- {names.map((n) => (
-
- {n}
-
- ))}
-
- );
- },
- },
- {
- id: "actions",
- cell: ({ row }) => (
-
-
-
-
-
-
-
-
- {
- setEditing(row.original);
- setFormOpen(true);
- }}
- >
-
- Düzenle
-
- setDeleting(row.original)}
- >
-
- Sil
-
-
-
-
- ),
- },
- ],
- [],
- );
-
- const table = useReactTable({
- data: services,
- columns,
- state: { globalFilter, sorting },
- onGlobalFilterChange: setGlobalFilter,
- onSortingChange: setSorting,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getSortedRowModel: getSortedRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- initialState: { pagination: { pageSize: 20 } },
- globalFilterFn: (row, _id, filterValue) => {
- const v = String(filterValue).toLowerCase();
- const assigneeNames = row.original.assigneeIds.map((id) => memberMap.get(id) ?? "").join(" ");
- return [row.original.name, row.original.customerName, row.original.description, assigneeNames]
- .join(" ")
- .toLowerCase()
- .includes(v);
- },
- });
-
- return (
-
-
-
-
-
- setGlobalFilter(e.target.value)}
- placeholder="Hizmet adı, müşteri..."
- className="pl-9"
- />
-
-
{
- setEditing(null);
- setFormOpen(true);
- }}
- disabled={customers.length === 0}
- >
-
- Yeni hizmet
-
-
-
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => (
-
- {header.isPlaceholder
- ? null
- : flexRender(header.column.columnDef.header, header.getContext())}
-
- ))}
-
- ))}
-
-
- {table.getRowModel().rows.length ? (
- table.getRowModel().rows.map((row) => (
-
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
-
- ))}
-
- ))
- ) : (
-
-
-
-
-
- {customers.length === 0
- ? "Önce bir müşteri ekleyin, sonra hizmet tanımlayabilirsiniz."
- : "Henüz hizmet eklenmemiş."}
-
- {customers.length > 0 && (
-
{
- setEditing(null);
- setFormOpen(true);
- }}
- >
-
- İlk hizmeti ekle
-
- )}
-
-
-
- )}
-
-
-
-
-
- Toplam {table.getFilteredRowModel().rows.length} hizmet
-
-
- table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- >
-
-
-
- Sayfa {table.getState().pagination.pageIndex + 1} /{" "}
- {Math.max(table.getPageCount(), 1)}
-
- table.nextPage()}
- disabled={!table.getCanNextPage()}
- >
-
-
-
-
-
-
- {
- setFormOpen(v);
- if (!v) setEditing(null);
- }}
- service={editing}
- customers={customers}
- members={members}
- />
-
- !v && setDeleting(null)}
- id={deleting?.id ?? null}
- name={deleting?.name ?? ""}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/services/components/types.ts b/src/app/(dashboard)/services/components/types.ts
deleted file mode 100644
index 733eb02..0000000
--- a/src/app/(dashboard)/services/components/types.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export type ServiceRow = {
- id: string;
- customerId: string;
- customerName: string;
- name: string;
- description: string;
- unitPrice: number;
- currency: string;
- recurring: boolean;
- billingPeriod: "monthly" | "yearly" | "onetime";
- assigneeIds: string[];
- createdAt: string;
-};
-
-export type CustomerOption = { id: string; name: string };
-
-export type MemberOption = { id: string; name: string; email: string };
diff --git a/src/app/(dashboard)/services/page.tsx b/src/app/(dashboard)/services/page.tsx
deleted file mode 100644
index e69bca0..0000000
--- a/src/app/(dashboard)/services/page.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listCustomers } from "@/lib/appwrite/customer-queries";
-import { listServices } from "@/lib/appwrite/service-queries";
-import { createAdminClient } from "@/lib/appwrite/server";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { ServicesClient } from "./components/services-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Hizmetler",
-};
-
-export default async function ServicesPage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const { teams } = createAdminClient();
- const [services, customers, membershipsResult] = await Promise.all([
- listServices(ctx.tenantId),
- listCustomers(ctx.tenantId),
- teams.listMemberships(ctx.tenantId).catch(() => ({ memberships: [] })),
- ]);
-
- const customerMap = new Map(customers.map((c) => [c.$id, c.name]));
- const members = membershipsResult.memberships
- .filter((m) => m.confirm)
- .map((m) => ({ id: m.userId, name: m.userName || m.userEmail, email: m.userEmail }));
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Hizmetler
-
- Müşterilere sunduğunuz hizmetleri ve fiyatlarını yönetin.
-
-
-
-
({
- id: s.$id,
- customerId: s.customerId,
- customerName: customerMap.get(s.customerId) ?? "—",
- name: s.name,
- description: s.description ?? "",
- unitPrice: s.unitPrice,
- currency: s.currency ?? "TRY",
- recurring: Boolean(s.recurring),
- billingPeriod: s.billingPeriod ?? "onetime",
- assigneeIds: s.assigneeIds ?? [],
- createdAt: s.$createdAt,
- }))}
- customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
- members={members}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/settings/billing/checkout/[orderId]/components/credit-card-visual.tsx b/src/app/(dashboard)/settings/billing/checkout/[orderId]/components/credit-card-visual.tsx
deleted file mode 100644
index a4af0a1..0000000
--- a/src/app/(dashboard)/settings/billing/checkout/[orderId]/components/credit-card-visual.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-"use client";
-
-import { Cpu, Wifi } from "lucide-react";
-
-import { cn } from "@/lib/utils";
-
-type Brand = "visa" | "mastercard" | "amex" | "troy" | "unknown";
-
-function detectBrand(num: string): Brand {
- const n = num.replace(/\s/g, "");
- if (/^4/.test(n)) return "visa";
- if (/^(5[1-5]|2[2-7])/.test(n)) return "mastercard";
- if (/^3[47]/.test(n)) return "amex";
- if (/^(9792|65)/.test(n)) return "troy";
- return "unknown";
-}
-
-function BrandLogo({ brand }: { brand: Brand }) {
- const base = "text-white/95 font-black tracking-tight";
- if (brand === "visa") {
- return VISA ;
- }
- if (brand === "mastercard") {
- return (
-
- );
- }
- if (brand === "amex") {
- return AMEX ;
- }
- if (brand === "troy") {
- return troy ;
- }
- return (
-
- );
-}
-
-function maskedNumber(num: string): string {
- const digits = num.replace(/\D/g, "").slice(0, 16);
- const padded = digits.padEnd(16, "•");
- return padded.match(/.{1,4}/g)?.join(" ") ?? "";
-}
-
-type Props = {
- number: string;
- name: string;
- expiry: string;
- cvc: string;
- flipped: boolean;
-};
-
-export function CreditCardVisual({ number, name, expiry, cvc, flipped }: Props) {
- const brand = detectBrand(number);
- const display = maskedNumber(number);
- const cvcDisplay = cvc.padEnd(3, "•").slice(0, 4);
-
- return (
-
-
- {/* FRONT */}
-
-
-
-
-
-
-
-
- {display}
-
-
-
-
- Kart sahibi
-
-
- {name || "AD SOYAD"}
-
-
-
-
- Son kullanma
-
-
{expiry || "AA/YY"}
-
-
-
-
-
-
- {/* BACK */}
-
-
-
-
-
- {cvcDisplay}
-
-
- CVC
-
-
-
- Bu kart yalnızca İşletmem mock test akışı içindir. Gerçek bir banka kartı
- değildir, hiçbir tahsilat yapılmaz.
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/settings/billing/checkout/[orderId]/components/mock-payment-form.tsx b/src/app/(dashboard)/settings/billing/checkout/[orderId]/components/mock-payment-form.tsx
deleted file mode 100644
index e2c4091..0000000
--- a/src/app/(dashboard)/settings/billing/checkout/[orderId]/components/mock-payment-form.tsx
+++ /dev/null
@@ -1,379 +0,0 @@
-"use client";
-
-import { useState, useTransition } from "react";
-import Link from "next/link";
-import { ArrowLeft, CreditCard, Loader2, Lock, ShieldCheck, X } from "lucide-react";
-
-import { Button } from "@/components/ui/button";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { cn } from "@/lib/utils";
-import {
- cancelMockPaymentAction,
- confirmMockPaymentAction,
-} from "@/lib/appwrite/subscription-actions";
-import type { CardBrand, SavedCard } from "@/lib/appwrite/schema";
-
-import { CreditCardVisual } from "./credit-card-visual";
-
-const trFmt = new Intl.NumberFormat("tr-TR", {
- style: "currency",
- currency: "TRY",
- maximumFractionDigits: 0,
-});
-
-const BRAND_LABEL: Record = {
- visa: "Visa",
- mastercard: "Mastercard",
- amex: "Amex",
- troy: "troy",
- unknown: "Kart",
-};
-
-function formatNumber(v: string): string {
- return v
- .replace(/\D/g, "")
- .slice(0, 16)
- .replace(/(.{4})/g, "$1 ")
- .trim();
-}
-
-function formatExpiry(v: string): string {
- const digits = v.replace(/\D/g, "").slice(0, 4);
- if (digits.length < 3) return digits;
- return `${digits.slice(0, 2)}/${digits.slice(2)}`;
-}
-
-function formatCvc(v: string): string {
- return v.replace(/\D/g, "").slice(0, 4);
-}
-
-function detectBrand(num: string): CardBrand {
- const n = num.replace(/\s/g, "");
- if (/^4/.test(n)) return "visa";
- if (/^(5[1-5]|2[2-7])/.test(n)) return "mastercard";
- if (/^3[47]/.test(n)) return "amex";
- if (/^(9792|65)/.test(n)) return "troy";
- return "unknown";
-}
-
-type Props = {
- orderId: string;
- amount: number;
- planName: string;
- planPeriod: string;
- savedCards: SavedCard[];
-};
-
-export function MockPaymentForm({
- orderId,
- amount,
- planName,
- planPeriod,
- savedCards,
-}: Props) {
- const defaultSaved = savedCards.find((c) => c.isDefault) ?? savedCards[0] ?? null;
- const [mode, setMode] = useState<"saved" | "new">(defaultSaved ? "saved" : "new");
- const [selectedCardId, setSelectedCardId] = useState(defaultSaved?.$id ?? "");
-
- const [number, setNumber] = useState("");
- const [name, setName] = useState("");
- const [expiry, setExpiry] = useState("");
- const [cvc, setCvc] = useState("");
- const [flipped, setFlipped] = useState(false);
- const [saveCard, setSaveCard] = useState(true);
-
- const [confirming, startConfirm] = useTransition();
- const [cancelling, startCancel] = useTransition();
-
- const numberDigits = number.replace(/\s/g, "");
- const expiryDigits = expiry.replace(/\D/g, "");
-
- const newCardFilled =
- numberDigits.length === 16 &&
- name.trim().length >= 3 &&
- expiryDigits.length === 4 &&
- cvc.length >= 3;
-
- const filled = mode === "saved" ? Boolean(selectedCardId) : newCardFilled;
-
- const handleConfirm = () => {
- const fd = new FormData();
- fd.set("orderId", orderId);
-
- if (mode === "saved" && selectedCardId) {
- fd.set("savedCardId", selectedCardId);
- } else {
- const month = expiryDigits.slice(0, 2);
- const year = expiryDigits.slice(2, 4);
- fd.set("cardLast4", numberDigits.slice(-4));
- fd.set("cardExpiryMonth", month);
- fd.set("cardExpiryYear", `20${year}`);
- fd.set("cardBrand", detectBrand(numberDigits));
- fd.set("cardHolder", name.trim());
- fd.set("saveCard", saveCard ? "true" : "false");
- }
-
- startConfirm(() => confirmMockPaymentAction(fd));
- };
-
- const handleCancel = () => {
- const fd = new FormData();
- fd.set("orderId", orderId);
- startCancel(() => cancelMockPaymentAction(fd));
- };
-
- const busy = confirming || cancelling;
-
- // Visual sync — saved card preview when in saved mode
- const selected = savedCards.find((c) => c.$id === selectedCardId);
- const visualNumber =
- mode === "saved" && selected
- ? `${"•".repeat(12)} ${selected.last4}`
- : number;
- const visualName =
- mode === "saved" && selected ? selected.holderName ?? "" : name;
- const visualExpiry =
- mode === "saved" && selected
- ? `${String(selected.expiryMonth).padStart(2, "0")}/${String(selected.expiryYear).slice(2)}`
- : expiry;
- const visualCvc = mode === "saved" ? "" : cvc;
-
- return (
-
-
-
-
-
-
-
- Test modu — gerçek kart bilgisi gerekmez. Onayladığında plan{" "}
- {planPeriod} boyunca aktif
- olur, tahsilat yapılmaz.
-
-
-
-
-
-
-
- Ödenecek tutar
-
-
- {trFmt.format(amount)}
- {planName}
-
-
-
- {savedCards.length > 0 && (
-
- setMode("saved")}
- disabled={busy}
- >
- Kayıtlı kart
-
- setMode("new")}
- disabled={busy}
- >
- Yeni kart
-
-
- )}
-
- {mode === "saved" && savedCards.length > 0 ? (
-
- {savedCards.map((c) => (
-
- setSelectedCardId(c.$id)}
- disabled={busy}
- />
-
-
-
- {BRAND_LABEL[c.brand ?? "unknown"]} •••• {c.last4}
-
-
- {c.holderName ?? "İsimsiz"} · Son kullanma{" "}
- {String(c.expiryMonth).padStart(2, "0")}/{String(c.expiryYear).slice(2)}
-
-
- {c.isDefault && (
-
- Varsayılan
-
- )}
-
- ))}
-
- ) : (
-
-
- Kart numarası
- setFlipped(false)}
- onChange={(e) => setNumber(formatNumber(e.target.value))}
- className="font-mono tracking-wider"
- disabled={busy}
- />
-
-
-
- Kart üzerindeki ad
- setFlipped(false)}
- onChange={(e) => setName(e.target.value.toUpperCase())}
- className="uppercase"
- disabled={busy}
- />
-
-
-
-
-
- setSaveCard(Boolean(v))}
- disabled={busy}
- className="mt-0.5"
- />
-
-
Bu kartı kaydet
-
- Sonraki ödemelerde tek tıkla kullan. Kart numarasının yalnızca son 4 hanesi,
- markası ve son kullanma tarihi saklanır — ham numara hiçbir yerde tutulmaz.
-
-
-
-
- )}
-
-
-
- {confirming ? (
- <>
-
- Onaylanıyor...
- >
- ) : (
- <>
-
- Güvenli ödeme — {trFmt.format(amount)}
- >
- )}
-
-
- {cancelling ? (
-
- ) : (
-
- )}
- Vazgeç
-
-
-
- {!filled && mode === "new" && (
-
- Onay butonu etkin olması için tüm kart alanlarını doldurman gerekir. Test modu —
- herhangi bir 16 haneli numara çalışır.
-
- )}
-
-
-
- Plan & Faturalandırma'ya dön
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/settings/billing/checkout/[orderId]/page.tsx b/src/app/(dashboard)/settings/billing/checkout/[orderId]/page.tsx
deleted file mode 100644
index e7ff199..0000000
--- a/src/app/(dashboard)/settings/billing/checkout/[orderId]/page.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import Link from "next/link";
-import { redirect } from "next/navigation";
-import { ArrowLeft } from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { Card, CardContent } from "@/components/ui/card";
-import { listSavedCards } from "@/lib/appwrite/saved-card-queries";
-import { getPaymentByOrderId } from "@/lib/appwrite/subscription-queries";
-import { PLAN_CATALOG } from "@/lib/appwrite/subscription-types";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-
-import { MockPaymentForm } from "./components/mock-payment-form";
-
-export default async function MockCheckoutPage({
- params,
-}: {
- params: Promise<{ orderId: string }>;
-}) {
- const { orderId } = await params;
- const ctx = await requireTenant();
- if (ctx.role !== "owner") redirect("/settings/billing");
-
- const payment = await getPaymentByOrderId(ctx.tenantId, orderId);
- if (!payment) redirect("/settings/billing");
-
- if (payment.status === "success") redirect("/settings/billing?upgraded=1");
- if (payment.status === "failed") redirect("/settings/billing?cancelled=1");
-
- const plan = PLAN_CATALOG[payment.plan];
- const savedCards = await listSavedCards(ctx.tenantId);
-
- return (
-
-
-
-
- Plan & Faturalandırma
-
-
-
Ödemeyi tamamla
-
- Mock Test
-
-
-
- Sipariş No: {payment.orderId} · Aşağıdaki kart
- formunu doldur — alanlar gerçek zamanlı olarak karta yansır.
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/settings/billing/components/billing-history-card.tsx b/src/app/(dashboard)/settings/billing/components/billing-history-card.tsx
deleted file mode 100644
index 1eecc0e..0000000
--- a/src/app/(dashboard)/settings/billing/components/billing-history-card.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Separator } from "@/components/ui/separator"
-
-interface BillingHistoryItem {
- id: number
- month: string
- plan: string
- amount: string
- status: string
-}
-
-interface BillingHistoryCardProps {
- history: BillingHistoryItem[]
-}
-
-export function BillingHistoryCard({ history }: BillingHistoryCardProps) {
- return (
-
-
- Billing History
-
- View your past invoices and payments.
-
-
-
-
- {history.map((item, index) => (
-
-
-
-
{item.month}
-
{item.plan}
-
-
-
{item.amount}
-
{item.status}
-
-
- {index < history.length - 1 &&
}
-
- ))}
-
-
-
- )
-}
diff --git a/src/app/(dashboard)/settings/billing/components/current-plan-card.tsx b/src/app/(dashboard)/settings/billing/components/current-plan-card.tsx
deleted file mode 100644
index 4912627..0000000
--- a/src/app/(dashboard)/settings/billing/components/current-plan-card.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Progress } from "@/components/ui/progress"
-import { Crown, AlertTriangle } from "lucide-react"
-
-interface CurrentPlan {
- planName: string
- price: string
- nextBilling: string
- status: string
- daysUsed: number
- totalDays: number
- progressPercentage: number
- remainingDays: number
- needsAttention: boolean
- attentionMessage: string
-}
-
-interface CurrentPlanCardProps {
- plan: CurrentPlan
-}
-
-export function CurrentPlanCard({ plan }: CurrentPlanCardProps) {
- return (
-
-
- Current Plan
-
- You are currently on the {plan.planName}.
-
-
-
-
-
-
- {plan.planName}
- {plan.status}
-
-
-
{plan.price}
-
Next billing: {plan.nextBilling}
-
-
-
- {plan.needsAttention && (
-
-
-
-
-
-
We need your attention!
-
{plan.attentionMessage}
-
-
-
- {/* Progress Section */}
-
-
- Days
- {plan.daysUsed} of {plan.totalDays} Days
-
-
-
{plan.remainingDays} days remaining until your plan requires update
-
-
-
- )}
-
-
- )
-}
diff --git a/src/app/(dashboard)/settings/billing/data/billing-history.json b/src/app/(dashboard)/settings/billing/data/billing-history.json
deleted file mode 100644
index 459ec39..0000000
--- a/src/app/(dashboard)/settings/billing/data/billing-history.json
+++ /dev/null
@@ -1,23 +0,0 @@
-[
- {
- "id": 1,
- "month": "December 2024",
- "plan": "Professional Plan",
- "amount": "$79.00",
- "status": "Paid"
- },
- {
- "id": 2,
- "month": "November 2024",
- "plan": "Professional Plan",
- "amount": "$79.00",
- "status": "Paid"
- },
- {
- "id": 3,
- "month": "October 2024",
- "plan": "Professional Plan",
- "amount": "$79.00",
- "status": "Paid"
- }
-]
diff --git a/src/app/(dashboard)/settings/billing/data/current-plan.json b/src/app/(dashboard)/settings/billing/data/current-plan.json
deleted file mode 100644
index f6b2f5f..0000000
--- a/src/app/(dashboard)/settings/billing/data/current-plan.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "planName": "Professional Plan",
- "price": "$79/month",
- "nextBilling": "Aug 15, 2025",
- "status": "Current",
- "daysUsed": 18,
- "totalDays": 30,
- "progressPercentage": 60,
- "remainingDays": 12,
- "needsAttention": true,
- "attentionMessage": "Your plan requires update"
-}
diff --git a/src/app/(dashboard)/settings/billing/page.tsx b/src/app/(dashboard)/settings/billing/page.tsx
deleted file mode 100644
index 9837d8b..0000000
--- a/src/app/(dashboard)/settings/billing/page.tsx
+++ /dev/null
@@ -1,372 +0,0 @@
-import Link from "next/link";
-import {
- ArrowUpRight,
- CheckCircle2,
- CreditCard,
- Crown,
- Lock,
- ShieldCheck,
- Sparkles,
- Star,
- Trash2,
-} from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { cn } from "@/lib/utils";
-import {
- removeCardAction,
- setDefaultCardAction,
-} from "@/lib/appwrite/saved-card-actions";
-import { listSavedCards } from "@/lib/appwrite/saved-card-queries";
-import type { CardBrand } from "@/lib/appwrite/schema";
-import { listPaymentsForTenant } from "@/lib/appwrite/subscription-queries";
-import { PLAN_CATALOG } from "@/lib/appwrite/subscription-types";
-import { getEffectivePlan } from "@/lib/appwrite/plan-limits";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-
-const trFmt = new Intl.NumberFormat("tr-TR", {
- style: "currency",
- currency: "TRY",
- maximumFractionDigits: 0,
-});
-
-const dateFmt = new Intl.DateTimeFormat("tr-TR", {
- day: "2-digit",
- month: "short",
- year: "numeric",
-});
-
-const STATUS_LABEL: Record = {
- pending: "Bekliyor",
- success: "Başarılı",
- failed: "İptal",
- refunded: "İade",
-};
-
-const STATUS_VARIANT: Record = {
- pending: "secondary",
- success: "default",
- failed: "outline",
- refunded: "destructive",
-};
-
-const PROVIDER_LABEL: Record = {
- mock: "Mock (Test)",
- shopier: "Shopier",
-};
-
-const BRAND_LABEL: Record = {
- visa: "Visa",
- mastercard: "Mastercard",
- amex: "Amex",
- troy: "troy",
- unknown: "Kart",
-};
-
-export default async function BillingSettings({
- searchParams,
-}: {
- searchParams: Promise<{ upgraded?: string; cancelled?: string; downgraded?: string }>;
-}) {
- const sp = await searchParams;
- const ctx = await requireTenant();
- const plan = getEffectivePlan(ctx);
- const isPro = plan === "pro";
- const canManage = ctx.role === "owner";
-
- const [payments, savedCards] = await Promise.all([
- listPaymentsForTenant(ctx.tenantId, 10),
- listSavedCards(ctx.tenantId),
- ]);
-
- const expiresAt = ctx.settings?.planExpiresAt;
- const catalog = isPro ? PLAN_CATALOG.pro : PLAN_CATALOG.free;
-
- return (
-
-
-
- {ctx.settings?.companyName ?? "Çalışma alanı"}
-
-
Faturalandırma
-
- Kayıtlı ödeme yöntemlerini, faturalarını ve veri saklama bilgilerini buradan yönet.
-
-
-
- {sp.upgraded && (
-
-
-
- Pro plan aktif. Sınırsız kullanım açıldı.
-
-
- )}
- {sp.cancelled && (
-
-
- Ödeme iptal edildi. Plan değişmedi.
-
-
- )}
- {sp.downgraded && (
-
- Ücretsiz plana döndünüz.
-
- )}
-
-
-
-
-
-
- {isPro ? (
-
- ) : (
-
- )}
- {catalog.name} plan
-
-
- {isPro && expiresAt
- ? `Yenileme tarihi: ${dateFmt.format(new Date(expiresAt))}`
- : isPro
- ? "Sınırsız kullanım."
- : "Ücretsiz plan — sınırlar dolduğunda Pro'ya geç."}
-
-
-
-
- Planları gör
-
-
-
-
-
-
-
- {trFmt.format(catalog.price)}
- /ay
-
-
-
-
-
-
-
-
- Kayıtlı kartlar
-
- Sonraki ödemelerde kullanılacak ödeme yöntemleri.
-
-
- {canManage && !isPro && (
-
-
- Pro'ya geç
-
-
-
- )}
-
-
-
- {savedCards.length === 0 ? (
-
-
-
Henüz kayıtlı kart yok.
-
- Bir ödeme yaparken "Bu kartı kaydet" seçeneğini işaretle, kart bilgileri buraya
- eklenir.
-
-
- ) : (
-
- )}
-
-
-
-
-
-
-
- Kart bilgileriniz nasıl saklanır?
-
-
-
-
-
-
-
Ham kart numarası saklanmaz
-
- İşletmem sunucularında kart numaranızın yalnızca son 4 hanesi, markası, son
- kullanma tarihi ve kart sahibi adı tutulur. Tam kart numarası ve CVC asla
- kaydedilmez.
-
-
-
-
-
-
-
Mock test modu
-
- Şu an Pro plan mock ödeme akışıyla çalışır — gerçek tahsilat yapılmaz. Test
- amaçlı girilen kart numaralarına ait yalnızca son 4 hane görüntü amacıyla
- kaydedilir.
-
-
-
-
-
-
-
Shopier entegrasyonu sonrası
-
- Shopier (Türkiye'de PCI-DSS uyumlu, BDDK lisanslı ödeme hizmeti sağlayıcısı)
- devreye girdiğinde kart bilgileri Shopier'in altyapısında tokenize edilir.
- İşletmem yalnızca tokeni saklar; bir sonraki ödemede tokenle Shopier'e tekrar
- başvurulur. Token yalnızca o aboneliğe özeldir.
-
-
-
-
- KVKK Aydınlatma Metni ve Mesafeli Satış Sözleşmesi yakında bu sayfaya eklenecek.
-
-
-
-
-
-
- Ödeme geçmişi
- Son 10 işlem.
-
-
- {payments.length === 0 ? (
-
- Henüz ödeme kaydı yok.
-
- ) : (
-
-
-
- Tarih
- Sipariş No
- Plan
- Sağlayıcı
- Durum
- Tutar
-
-
-
- {payments.map((p) => (
-
-
- {dateFmt.format(new Date(p.$createdAt))}
-
- {p.orderId}
- {p.plan}
- {PROVIDER_LABEL[p.provider ?? "mock"]}
-
-
- {STATUS_LABEL[p.status ?? "pending"]}
-
- {p.status === "pending" && (
-
- Devam
-
- )}
-
-
- {trFmt.format(p.amount)}
-
-
- ))}
-
-
- )}
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/settings/members/page.tsx b/src/app/(dashboard)/settings/members/page.tsx
index 7573a08..0422054 100644
--- a/src/app/(dashboard)/settings/members/page.tsx
+++ b/src/app/(dashboard)/settings/members/page.tsx
@@ -65,7 +65,7 @@ export default async function MembersPage() {
return (
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
+
{ctx.settings?.officeName ?? "Çalışma alanı"}
Ekip üyeleri
Çalışma alanına üye davet edin, rolleri yönetin.
diff --git a/src/app/(dashboard)/settings/workspace/page.tsx b/src/app/(dashboard)/settings/workspace/page.tsx
index 787afb4..93d047c 100644
--- a/src/app/(dashboard)/settings/workspace/page.tsx
+++ b/src/app/(dashboard)/settings/workspace/page.tsx
@@ -7,7 +7,7 @@ import { LogoUploader } from "./components/logo-uploader";
import { WorkspaceSettingsForm } from "./components/workspace-form";
export const metadata: Metadata = {
- title: "İşletmem — Şirket bilgileri",
+ title: "KovakEmlak — Ofis bilgileri",
};
export default async function WorkspaceSettingsPage() {
@@ -19,14 +19,15 @@ export default async function WorkspaceSettingsPage() {
}
const canEdit = ctx.role === "owner" || ctx.role === "admin";
+ const officeName = ctx.settings?.officeName ?? "Ofis";
return (
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Şirket bilgileri
+
{officeName}
+
Ofis Bilgileri
- Faturalarda ve panel başlığında görünecek şirket bilgileri.
+ Panel başlığında görünecek ofis bilgileri.
{!canEdit && " Düzenlemek için yönetici yetkisine ihtiyacınız var."}
@@ -34,23 +35,20 @@ export default async function WorkspaceSettingsPage() {
diff --git a/src/app/(dashboard)/software/components/assignment-form-sheet.tsx b/src/app/(dashboard)/software/components/assignment-form-sheet.tsx
deleted file mode 100644
index b48809b..0000000
--- a/src/app/(dashboard)/software/components/assignment-form-sheet.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState } from "react";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import {
- createAssignmentAction,
- updateAssignmentAction,
-} from "@/lib/appwrite/software-actions";
-import { initialSoftwareState } from "@/lib/appwrite/software-types";
-import type { AssignmentRow, CustomerOption, SoftwareOption } from "./types";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- assignment?: AssignmentRow | null;
- customers: CustomerOption[];
- softwareOptions: SoftwareOption[];
-};
-
-function isoToInputDate(iso: string): string {
- if (!iso) return "";
- return iso.slice(0, 10);
-}
-
-export function AssignmentFormSheet({
- open,
- onOpenChange,
- assignment,
- customers,
- softwareOptions,
-}: Props) {
- const isEdit = Boolean(assignment);
- const action = isEdit ? updateAssignmentAction : createAssignmentAction;
- const [state, formAction, isPending] = useActionState(action, initialSoftwareState);
- const [selectedSoftware, setSelectedSoftware] = useState
(assignment?.softwareId ?? "");
-
- useEffect(() => {
- setSelectedSoftware(assignment?.softwareId ?? "");
- }, [assignment]);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Atama güncellendi." : "Atama oluşturuldu.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- const defaultFee =
- softwareOptions.find((s) => s.id === selectedSoftware)?.defaultFee ?? "";
-
- const blocked = customers.length === 0 || softwareOptions.length === 0;
-
- return (
-
-
-
-
- {isEdit ? "Atamayı düzenle" : "Müşteriye yazılım ata"}
-
-
- {blocked
- ? "Atama yapmak için en az bir müşteri ve bir yazılım gerekli."
- : "Yazılımı bir müşteriye atayın; varsayılan ücret özelleştirilebilir."}
-
-
-
-
- {isEdit && assignment && }
-
-
-
-
Müşteri *
-
-
-
-
-
- {customers.map((c) => (
-
- {c.name}
-
- ))}
-
-
- {state.fieldErrors?.customerId && (
-
{state.fieldErrors.customerId}
- )}
-
-
-
-
Yazılım *
-
-
-
-
-
- {softwareOptions.map((s) => (
-
- {s.name}
- {s.version ? ` v${s.version}` : ""}
-
- ))}
-
-
- {state.fieldErrors?.softwareId && (
-
{state.fieldErrors.softwareId}
- )}
-
-
-
-
- Bu müşteri için ücret (₺)
-
-
-
- Faturalama dönemi
-
-
-
-
-
- Aylık
- Yıllık
- Tek seferlik
-
-
-
-
-
-
-
-
- Notlar
-
-
-
-
-
-
- onOpenChange(false)}
- disabled={isPending}
- >
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Ata"}
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/software/components/software-client.tsx b/src/app/(dashboard)/software/components/software-client.tsx
deleted file mode 100644
index ffdae09..0000000
--- a/src/app/(dashboard)/software/components/software-client.tsx
+++ /dev/null
@@ -1,553 +0,0 @@
-"use client";
-
-import { useMemo, useState, useTransition } from "react";
-import {
- type ColumnDef,
- flexRender,
- getCoreRowModel,
- getFilteredRowModel,
- getPaginationRowModel,
- useReactTable,
-} from "@tanstack/react-table";
-import {
- Loader2,
- MoreHorizontal,
- Package,
- Pencil,
- Plus,
- Search,
- Trash2,
- Users,
-} from "lucide-react";
-import { toast } from "sonner";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Input } from "@/components/ui/input";
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import {
- deleteAssignmentAction,
- deleteSoftwareAction,
-} from "@/lib/appwrite/software-actions";
-import { BILLING_PERIOD_LABEL, formatDate, formatTRY } from "@/lib/format";
-
-import { AssignmentFormSheet } from "./assignment-form-sheet";
-import { SoftwareFormSheet } from "./software-form-sheet";
-import type {
- AssignmentRow,
- CustomerOption,
- SoftwareOption,
- SoftwareRow,
-} from "./types";
-
-type Props = {
- software: SoftwareRow[];
- assignments: AssignmentRow[];
- customers: CustomerOption[];
-};
-
-export function SoftwareClient({ software, assignments, customers }: Props) {
- const [tab, setTab] = useState<"catalog" | "assignments">("catalog");
- const [softwareSearch, setSoftwareSearch] = useState("");
- const [assignmentSearch, setAssignmentSearch] = useState("");
-
- const [softwareFormOpen, setSoftwareFormOpen] = useState(false);
- const [editingSoftware, setEditingSoftware] = useState(null);
- const [deletingSoftware, setDeletingSoftware] = useState(null);
-
- const [assignmentFormOpen, setAssignmentFormOpen] = useState(false);
- const [editingAssignment, setEditingAssignment] = useState(null);
- const [deletingAssignment, setDeletingAssignment] = useState(null);
-
- const [busy, startTransition] = useTransition();
-
- const softwareOptions: SoftwareOption[] = useMemo(
- () =>
- software.map((s) => ({
- id: s.id,
- name: s.name,
- version: s.version,
- defaultFee: s.defaultFee,
- })),
- [software],
- );
-
- const softwareCols = useMemo[]>(
- () => [
- {
- accessorKey: "name",
- header: "Ad",
- cell: ({ row }) => (
-
- {row.original.name}
- {row.original.description && (
-
- {row.original.description}
-
- )}
-
- ),
- },
- {
- accessorKey: "version",
- header: "Sürüm",
- cell: ({ row }) =>
- row.original.version ? (
- v{row.original.version}
- ) : (
- —
- ),
- },
- {
- accessorKey: "defaultFee",
- header: "Varsayılan ücret",
- cell: ({ row }) =>
- row.original.defaultFee !== null ? (
- {formatTRY(row.original.defaultFee)}
- ) : (
- —
- ),
- },
- {
- id: "actions",
- cell: ({ row }) => (
-
-
-
-
-
-
-
-
- {
- setEditingSoftware(row.original);
- setSoftwareFormOpen(true);
- }}
- >
-
- Düzenle
-
- setDeletingSoftware(row.original)}
- >
-
- Sil
-
-
-
-
- ),
- },
- ],
- [],
- );
-
- const assignmentCols = useMemo[]>(
- () => [
- {
- accessorKey: "softwareName",
- header: "Yazılım",
- cell: ({ row }) => (
-
- {row.original.softwareName}
- {row.original.softwareVersion && (
- v{row.original.softwareVersion}
- )}
-
- ),
- },
- {
- accessorKey: "customerName",
- header: "Müşteri",
- cell: ({ row }) => (
- {row.original.customerName}
- ),
- },
- {
- accessorKey: "fee",
- header: "Ücret",
- cell: ({ row }) => (
-
-
- {row.original.fee !== null ? formatTRY(row.original.fee) : "—"}
-
-
- {BILLING_PERIOD_LABEL[row.original.billingPeriod]}
-
-
- ),
- },
- {
- id: "dates",
- header: "Süre",
- cell: ({ row }) => (
-
- {formatDate(row.original.startDate)}
- {" → "}
- {row.original.endDate ? formatDate(row.original.endDate) : "süresiz"}
-
- ),
- },
- {
- id: "actions",
- cell: ({ row }) => (
-
-
-
-
-
-
-
-
- {
- setEditingAssignment(row.original);
- setAssignmentFormOpen(true);
- }}
- >
-
- Düzenle
-
- setDeletingAssignment(row.original)}
- >
-
- Kaldır
-
-
-
-
- ),
- },
- ],
- [],
- );
-
- const softwareTable = useReactTable({
- data: software,
- columns: softwareCols,
- state: { globalFilter: softwareSearch },
- onGlobalFilterChange: setSoftwareSearch,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- initialState: { pagination: { pageSize: 20 } },
- globalFilterFn: (row, _id, fv) => {
- const v = String(fv).toLowerCase();
- return [row.original.name, row.original.version, row.original.description]
- .join(" ")
- .toLowerCase()
- .includes(v);
- },
- });
-
- const assignmentTable = useReactTable({
- data: assignments,
- columns: assignmentCols,
- state: { globalFilter: assignmentSearch },
- onGlobalFilterChange: setAssignmentSearch,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- initialState: { pagination: { pageSize: 20 } },
- globalFilterFn: (row, _id, fv) => {
- const v = String(fv).toLowerCase();
- return [row.original.softwareName, row.original.customerName, row.original.notes]
- .join(" ")
- .toLowerCase()
- .includes(v);
- },
- });
-
- const deleteSoftware = () => {
- if (!deletingSoftware) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deletingSoftware.id);
- const result = await deleteSoftwareAction(fd);
- if (result.ok) {
- toast.success("Yazılım silindi.");
- setDeletingSoftware(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- const deleteAssignment = () => {
- if (!deletingAssignment) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deletingAssignment.id);
- const result = await deleteAssignmentAction(fd);
- if (result.ok) {
- toast.success("Atama kaldırıldı.");
- setDeletingAssignment(null);
- } else {
- toast.error(result.error ?? "İşlem başarısız.");
- }
- });
- };
-
- return (
-
-
- setTab(v as typeof tab)}>
-
-
-
-
- Katalog ({software.length})
-
-
-
- Atamalar ({assignments.length})
-
-
-
- {tab === "catalog" ? (
-
{
- setEditingSoftware(null);
- setSoftwareFormOpen(true);
- }}
- >
-
- Yeni yazılım
-
- ) : (
-
{
- setEditingAssignment(null);
- setAssignmentFormOpen(true);
- }}
- disabled={customers.length === 0 || software.length === 0}
- >
-
- Yeni atama
-
- )}
-
-
-
-
-
-
- setSoftwareSearch(e.target.value)}
- placeholder="Yazılım ara..."
- className="pl-9"
- />
-
-
-
-
- {softwareTable.getHeaderGroups().map((hg) => (
-
- {hg.headers.map((h) => (
-
- {h.isPlaceholder
- ? null
- : flexRender(h.column.columnDef.header, h.getContext())}
-
- ))}
-
- ))}
-
-
- {softwareTable.getRowModel().rows.length ? (
- softwareTable.getRowModel().rows.map((r) => (
-
- {r.getVisibleCells().map((c) => (
-
- {flexRender(c.column.columnDef.cell, c.getContext())}
-
- ))}
-
- ))
- ) : (
-
-
-
-
-
Henüz yazılım eklenmemiş.
-
{
- setEditingSoftware(null);
- setSoftwareFormOpen(true);
- }}
- >
-
- İlk yazılımı ekle
-
-
-
-
- )}
-
-
-
-
-
-
-
-
- setAssignmentSearch(e.target.value)}
- placeholder="Yazılım veya müşteri ara..."
- className="pl-9"
- />
-
-
-
-
- {assignmentTable.getHeaderGroups().map((hg) => (
-
- {hg.headers.map((h) => (
-
- {h.isPlaceholder
- ? null
- : flexRender(h.column.columnDef.header, h.getContext())}
-
- ))}
-
- ))}
-
-
- {assignmentTable.getRowModel().rows.length ? (
- assignmentTable.getRowModel().rows.map((r) => (
-
- {r.getVisibleCells().map((c) => (
-
- {flexRender(c.column.columnDef.cell, c.getContext())}
-
- ))}
-
- ))
- ) : (
-
-
-
-
-
- {customers.length === 0 || software.length === 0
- ? "Önce müşteri ve yazılım ekleyin, sonra atayabilirsiniz."
- : "Henüz atama yapılmamış."}
-
-
-
-
- )}
-
-
-
-
-
-
- {
- setSoftwareFormOpen(v);
- if (!v) setEditingSoftware(null);
- }}
- software={editingSoftware}
- />
-
- {
- setAssignmentFormOpen(v);
- if (!v) setEditingAssignment(null);
- }}
- assignment={editingAssignment}
- customers={customers}
- softwareOptions={softwareOptions}
- />
-
- !v && setDeletingSoftware(null)}
- >
-
-
- Yazılımı sil
-
- {deletingSoftware?.name} ve müşterilerle olan tüm atamaları silinecek.
-
-
-
- setDeletingSoftware(null)}
- disabled={busy}
- >
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
-
- !v && setDeletingAssignment(null)}
- >
-
-
- Atamayı kaldır
-
- {deletingAssignment?.softwareName} →{" "}
- {deletingAssignment?.customerName} ataması kaldırılacak.
-
-
-
- setDeletingAssignment(null)}
- disabled={busy}
- >
- Vazgeç
-
-
- {busy ? : }
- Kaldır
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/software/components/software-form-sheet.tsx b/src/app/(dashboard)/software/components/software-form-sheet.tsx
deleted file mode 100644
index 967da6e..0000000
--- a/src/app/(dashboard)/software/components/software-form-sheet.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-"use client";
-
-import { useActionState, useEffect, useState } from "react";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import { PlanLimitDialog } from "@/components/billing/plan-limit-dialog";
-import {
- createSoftwareAction,
- updateSoftwareAction,
-} from "@/lib/appwrite/software-actions";
-import { initialSoftwareState } from "@/lib/appwrite/software-types";
-import type { SoftwareRow } from "./types";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- software?: SoftwareRow | null;
-};
-
-export function SoftwareFormSheet({ open, onOpenChange, software }: Props) {
- const isEdit = Boolean(software);
- const action = isEdit ? updateSoftwareAction : createSoftwareAction;
- const [state, formAction, isPending] = useActionState(action, initialSoftwareState);
- const [planLimitOpen, setPlanLimitOpen] = useState(false);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Yazılım güncellendi." : "Yazılım eklendi.");
- onOpenChange(false);
- } else if (state.code === "PLAN_LIMIT_EXCEEDED") {
- setPlanLimitOpen(true);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- return (
-
-
-
- {isEdit ? "Yazılımı düzenle" : "Yeni yazılım"}
-
- Kataloğunuzdaki yazılım. Müşterilere ayrı ayrı ücretlerle atayabilirsiniz.
-
-
-
-
- {isEdit && software && }
-
-
-
-
Yazılım adı *
-
- {state.fieldErrors?.name && (
-
{state.fieldErrors.name}
- )}
-
-
-
-
-
- Açıklama
-
-
-
-
-
-
- onOpenChange(false)}
- disabled={isPending}
- >
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Kaydet"}
- >
- )}
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/software/components/types.ts b/src/app/(dashboard)/software/components/types.ts
deleted file mode 100644
index eaa0341..0000000
--- a/src/app/(dashboard)/software/components/types.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export type SoftwareRow = {
- id: string;
- name: string;
- version: string;
- description: string;
- defaultFee: number | null;
-};
-
-export type AssignmentRow = {
- id: string;
- customerId: string;
- customerName: string;
- softwareId: string;
- softwareName: string;
- softwareVersion: string;
- startDate: string;
- endDate: string;
- fee: number | null;
- billingPeriod: "monthly" | "yearly" | "onetime";
- notes: string;
-};
-
-export type SoftwareOption = { id: string; name: string; version: string; defaultFee: number | null };
-export type CustomerOption = { id: string; name: string };
diff --git a/src/app/(dashboard)/software/page.tsx b/src/app/(dashboard)/software/page.tsx
deleted file mode 100644
index 2483bde..0000000
--- a/src/app/(dashboard)/software/page.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listCustomers } from "@/lib/appwrite/customer-queries";
-import { listAssignments, listSoftware } from "@/lib/appwrite/software-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { SoftwareClient } from "./components/software-client";
-
-export const metadata: Metadata = {
- title: "İşletmem — Yazılımlarımız",
-};
-
-export default async function SoftwarePage() {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const [softwareList, customers, assignments] = await Promise.all([
- listSoftware(ctx.tenantId),
- listCustomers(ctx.tenantId),
- listAssignments(ctx.tenantId),
- ]);
-
- const softwareMap = new Map(softwareList.map((s) => [s.$id, s]));
- const customerMap = new Map(customers.map((c) => [c.$id, c.name]));
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Yazılımlarımız
-
- Yazılım kataloğunuzu yönetin ve müşterilere atayın.
-
-
-
-
({
- id: s.$id,
- name: s.name,
- version: s.version ?? "",
- description: s.description ?? "",
- defaultFee: s.defaultFee ?? null,
- }))}
- assignments={assignments.map((a) => ({
- id: a.$id,
- customerId: a.customerId,
- customerName: customerMap.get(a.customerId) ?? "—",
- softwareId: a.softwareId,
- softwareName: softwareMap.get(a.softwareId)?.name ?? "—",
- softwareVersion: softwareMap.get(a.softwareId)?.version ?? "",
- startDate: a.startDate ?? "",
- endDate: a.endDate ?? "",
- fee: a.fee ?? null,
- billingPeriod: a.billingPeriod ?? "monthly",
- notes: a.notes ?? "",
- }))}
- customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/tasks/components/task-card.tsx b/src/app/(dashboard)/tasks/components/task-card.tsx
deleted file mode 100644
index e78c04a..0000000
--- a/src/app/(dashboard)/tasks/components/task-card.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-"use client";
-
-import { useSortable } from "@dnd-kit/sortable";
-import { CSS } from "@dnd-kit/utilities";
-import { Calendar, GripVertical, MoreHorizontal, Pencil, Trash2, UserCircle } from "lucide-react";
-
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { cn } from "@/lib/utils";
-import { formatDate } from "@/lib/format";
-
-import { PRIORITY_COLOR, PRIORITY_LABEL, type TaskRow } from "./types";
-
-type Props = {
- task: TaskRow;
- onEdit: (task: TaskRow) => void;
- onDelete: (task: TaskRow) => void;
- isOverlay?: boolean;
- currentUserId?: string;
-};
-
-export function TaskCard({ task, onEdit, onDelete, isOverlay, currentUserId }: Props) {
- const assignedToMe = currentUserId && task.assigneeId === currentUserId;
- const unassigned = !task.assigneeId;
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
- useSortable({ id: task.id, data: { type: "task", status: task.status } });
-
- const style = {
- transform: CSS.Transform.toString(transform),
- transition,
- };
-
- const overdue =
- task.dueDate && task.status !== "done" && new Date(task.dueDate) < new Date();
-
- return (
-
-
-
-
-
-
-
-
{task.title}
-
-
-
-
-
-
-
- onEdit(task)}>
-
- Düzenle
-
- onDelete(task)}>
-
- Sil
-
-
-
-
-
- {task.description && (
-
{task.description}
- )}
-
- {task.customerName && (
-
{task.customerName}
- )}
-
-
-
- {PRIORITY_LABEL[task.priority]}
-
-
- {task.dueDate && (
-
-
- {formatDate(task.dueDate)}
-
- )}
-
- {assignedToMe ? (
-
-
- Bana atanmış
-
- ) : task.assigneeName ? (
-
-
- {task.assigneeName}
-
- ) : (
-
- Atanmamış
-
- )}
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/tasks/components/task-form-sheet.tsx b/src/app/(dashboard)/tasks/components/task-form-sheet.tsx
deleted file mode 100644
index 7ec6159..0000000
--- a/src/app/(dashboard)/tasks/components/task-form-sheet.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-"use client";
-
-import { useActionState, useEffect } from "react";
-import { Loader2, Save } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Sheet,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet";
-import { Textarea } from "@/components/ui/textarea";
-import { createTaskAction, updateTaskAction } from "@/lib/appwrite/task-actions";
-import { initialTaskState } from "@/lib/appwrite/task-types";
-import type { Customer, TaskRow, TaskStatus, TeamMember } from "./types";
-
-const NONE = "__none__";
-
-type Props = {
- open: boolean;
- onOpenChange: (v: boolean) => void;
- task?: TaskRow | null;
- defaultStatus?: TaskStatus;
- customers: Customer[];
- teamMembers: TeamMember[];
-};
-
-function isoToInputDate(iso: string): string {
- if (!iso) return "";
- return iso.slice(0, 10);
-}
-
-export function TaskFormSheet({
- open,
- onOpenChange,
- task,
- defaultStatus = "todo",
- customers,
- teamMembers,
-}: Props) {
- const isEdit = Boolean(task);
- const action = isEdit ? updateTaskAction : createTaskAction;
- const [state, formAction, isPending] = useActionState(action, initialTaskState);
-
- useEffect(() => {
- if (state.ok) {
- toast.success(isEdit ? "Görev güncellendi." : "Görev eklendi.");
- onOpenChange(false);
- } else if (state.error) {
- toast.error(state.error);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [state]);
-
- return (
-
-
-
- {isEdit ? "Görevi düzenle" : "Yeni görev"}
-
- Görev bilgilerini doldurun. Sonra Kanban'da sürükleyerek durumu değiştirebilirsiniz.
-
-
-
- {
- // Strip "__none__" sentinel before submit
- ["assigneeId", "customerId"].forEach((k) => {
- if (fd.get(k) === NONE) fd.set(k, "");
- });
- formAction(fd);
- }}
- className="flex flex-1 flex-col"
- >
- {isEdit && task && }
-
-
-
-
Başlık *
-
- {state.fieldErrors?.title && (
-
{state.fieldErrors.title}
- )}
-
-
-
- Açıklama
-
-
-
-
-
- Durum
-
-
-
-
-
- Beklemede
- Yapılacak
- Sürüyor
- Bitti
-
-
-
-
- Öncelik
-
-
-
-
-
- Düşük
- Orta
- Yüksek
- Acil
-
-
-
-
-
-
-
- Son tarih
-
-
-
- Atanan
-
-
-
-
-
- Kimse
- {teamMembers.map((m) => (
-
- {m.name}
-
- ))}
-
-
-
-
-
-
- Müşteri (opsiyonel)
-
-
-
-
-
- Yok
- {customers.map((c) => (
-
- {c.name}
-
- ))}
-
-
-
-
-
-
-
- onOpenChange(false)}
- disabled={isPending}
- >
- Vazgeç
-
-
- {isPending ? (
- <>
-
- Kaydediliyor...
- >
- ) : (
- <>
-
- {isEdit ? "Güncelle" : "Kaydet"}
- >
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/(dashboard)/tasks/components/tasks-board.tsx b/src/app/(dashboard)/tasks/components/tasks-board.tsx
deleted file mode 100644
index 40c14d5..0000000
--- a/src/app/(dashboard)/tasks/components/tasks-board.tsx
+++ /dev/null
@@ -1,376 +0,0 @@
-"use client";
-
-import { useMemo, useState } from "react";
-import { useRouter } from "next/navigation";
-import {
- DndContext,
- type DragEndEvent,
- DragOverlay,
- type DragStartEvent,
- PointerSensor,
- useDroppable,
- useSensor,
- useSensors,
- closestCorners,
-} from "@dnd-kit/core";
-import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
-import { Loader2, Plus, Trash2 } from "lucide-react";
-import { toast } from "sonner";
-
-import { Button } from "@/components/ui/button";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { deleteTaskAction, moveTaskAction } from "@/lib/appwrite/task-actions";
-import { cn } from "@/lib/utils";
-
-import { TaskCard } from "./task-card";
-import { TaskFormSheet } from "./task-form-sheet";
-import {
- COLUMNS,
- type Customer,
- FILTER_LABEL,
- type TaskFilter,
- type TaskRow,
- type TaskStatus,
- type TeamMember,
-} from "./types";
-import { useTransition } from "react";
-
-type FilterCounts = {
- total: number;
- mine: number;
- unassigned: number;
- mineOrUnassigned: number;
-};
-
-type Props = {
- tasks: TaskRow[];
- customers: Customer[];
- teamMembers: TeamMember[];
- currentUserId: string;
- filter: TaskFilter;
- filterCounts: FilterCounts;
-};
-
-function Column({
- status,
- title,
- tasks,
- onAdd,
- onEdit,
- onDelete,
- currentUserId,
-}: {
- status: TaskStatus;
- title: string;
- tasks: TaskRow[];
- onAdd: (status: TaskStatus) => void;
- onEdit: (task: TaskRow) => void;
- onDelete: (task: TaskRow) => void;
- currentUserId: string;
-}) {
- const { setNodeRef, isOver } = useDroppableColumn(status);
-
- return (
-
-
-
-
{title}
-
- {tasks.length}
-
-
-
onAdd(status)}
- aria-label="Yeni görev"
- >
-
-
-
-
-
-
t.id)}
- strategy={verticalListSortingStrategy}
- >
- {tasks.map((task) => (
-
- ))}
-
- {tasks.length === 0 && (
-
Boş
- )}
-
-
- );
-}
-
-function useDroppableColumn(status: TaskStatus) {
- return useDroppable({
- id: `col-${status}`,
- data: { type: "column", status },
- });
-}
-
-export function TasksBoard({
- tasks: initialTasks,
- customers,
- teamMembers,
- currentUserId,
- filter,
- filterCounts,
-}: Props) {
- const [tasks, setTasks] = useState(initialTasks);
- const [activeId, setActiveId] = useState(null);
- const [formOpen, setFormOpen] = useState(false);
- const [formStatus, setFormStatus] = useState("todo");
- const [editing, setEditing] = useState(null);
- const [deleting, setDeleting] = useState(null);
- const [busy, startTransition] = useTransition();
-
- const sensors = useSensors(
- useSensor(PointerSensor, {
- activationConstraint: { distance: 6 },
- }),
- );
-
- const grouped = useMemo(() => {
- const map: Record = {
- backlog: [],
- todo: [],
- in_progress: [],
- done: [],
- };
- for (const t of tasks) {
- map[t.status].push(t);
- }
- return map;
- }, [tasks]);
-
- const activeTask = useMemo(
- () => tasks.find((t) => t.id === activeId) ?? null,
- [tasks, activeId],
- );
-
- const onDragStart = (e: DragStartEvent) => {
- setActiveId(String(e.active.id));
- };
-
- const onDragEnd = (e: DragEndEvent) => {
- const { active, over } = e;
- setActiveId(null);
- if (!over || active.id === over.id) return;
-
- const activeData = active.data.current as { type?: string; status?: TaskStatus } | undefined;
- const overData = over.data.current as { type?: string; status?: TaskStatus } | undefined;
- if (activeData?.type !== "task") return;
-
- let targetStatus: TaskStatus | undefined;
- if (overData?.type === "column" && overData.status) {
- targetStatus = overData.status;
- } else if (overData?.type === "task" && overData.status) {
- targetStatus = overData.status;
- }
- if (!targetStatus) return;
-
- const sourceTask = tasks.find((t) => t.id === active.id);
- if (!sourceTask) return;
-
- // Compute new order: place after the over item, or end of column
- const targetTasks = tasks.filter(
- (t) => t.status === targetStatus && t.id !== active.id,
- );
- let newOrder: number;
- if (overData?.type === "task") {
- const overIndex = targetTasks.findIndex((t) => t.id === over.id);
- if (overIndex === -1) {
- newOrder = (targetTasks[targetTasks.length - 1]?.order ?? 0) + 1000;
- } else {
- const before = targetTasks[overIndex - 1]?.order ?? 0;
- const at = targetTasks[overIndex].order;
- newOrder = (before + at) / 2;
- }
- } else {
- newOrder = (targetTasks[targetTasks.length - 1]?.order ?? 0) + 1000;
- }
-
- // Optimistic update
- setTasks((prev) =>
- prev.map((t) => (t.id === sourceTask.id ? { ...t, status: targetStatus!, order: newOrder } : t)),
- );
-
- startTransition(async () => {
- const result = await moveTaskAction(sourceTask.id, targetStatus!, newOrder);
- if (!result.ok) {
- // Rollback
- setTasks((prev) =>
- prev.map((t) =>
- t.id === sourceTask.id ? { ...t, status: sourceTask.status, order: sourceTask.order } : t,
- ),
- );
- toast.error(result.error ?? "Görev taşınamadı.");
- }
- });
- };
-
- // Keep local state in sync when server data changes (e.g., after revalidate)
- useMemo(() => setTasks(initialTasks), [initialTasks]);
-
- const handleAdd = (status: TaskStatus) => {
- setEditing(null);
- setFormStatus(status);
- setFormOpen(true);
- };
-
- const handleDelete = () => {
- if (!deleting) return;
- startTransition(async () => {
- const fd = new FormData();
- fd.set("id", deleting.id);
- const result = await deleteTaskAction(fd);
- if (result.ok) {
- toast.success("Görev silindi.");
- setDeleting(null);
- } else {
- toast.error(result.error ?? "Silme başarısız.");
- }
- });
- };
-
- const router = useRouter();
- const setFilter = (value: TaskFilter) => {
- const params = new URLSearchParams();
- if (value !== "mine_or_unassigned") params.set("view", value);
- router.push(`/tasks${params.size ? `?${params}` : ""}`);
- };
-
- return (
- <>
-
-
- setFilter(v as TaskFilter)}>
-
-
-
-
-
- {FILTER_LABEL.mine_or_unassigned} ({filterCounts.mineOrUnassigned})
-
-
- {FILTER_LABEL.mine} ({filterCounts.mine})
-
-
- {FILTER_LABEL.unassigned} ({filterCounts.unassigned})
-
-
- {FILTER_LABEL.all} ({filterCounts.total})
-
-
-
-
-
-
handleAdd("todo")}>
-
- Yeni görev
-
-
-
-
-
- {COLUMNS.map((col) => (
- {
- setEditing(t);
- setFormOpen(true);
- }}
- onDelete={(t) => setDeleting(t)}
- currentUserId={currentUserId}
- />
- ))}
-
-
-
- {activeTask && (
- {}}
- onDelete={() => {}}
- isOverlay
- currentUserId={currentUserId}
- />
- )}
-
-
-
- {
- setFormOpen(v);
- if (!v) setEditing(null);
- }}
- task={editing}
- defaultStatus={formStatus}
- customers={customers}
- teamMembers={teamMembers}
- />
-
- !v && setDeleting(null)}>
-
-
- Görevi sil
-
- {deleting?.title} kalıcı olarak silinecek.
-
-
-
- setDeleting(null)} disabled={busy}>
- Vazgeç
-
-
- {busy ? : }
- Sil
-
-
-
-
- >
- );
-}
diff --git a/src/app/(dashboard)/tasks/components/types.ts b/src/app/(dashboard)/tasks/components/types.ts
deleted file mode 100644
index 45755e6..0000000
--- a/src/app/(dashboard)/tasks/components/types.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-export type TaskStatus = "backlog" | "todo" | "in_progress" | "done";
-export type TaskPriority = "low" | "medium" | "high" | "urgent";
-
-export type TaskRow = {
- id: string;
- title: string;
- description: string;
- status: TaskStatus;
- priority: TaskPriority;
- dueDate: string;
- assigneeId: string;
- assigneeName: string;
- customerId: string;
- customerName: string;
- order: number;
-};
-
-export type Customer = { id: string; name: string };
-export type TeamMember = { id: string; name: string };
-
-export type TaskFilter = "all" | "mine" | "unassigned" | "mine_or_unassigned";
-
-export const FILTER_LABEL: Record = {
- all: "Hepsi",
- mine: "Bana atanmış",
- unassigned: "Atanmamış",
- mine_or_unassigned: "Bana atanmış + Atanmamış",
-};
-
-export const COLUMNS: { key: TaskStatus; title: string }[] = [
- { key: "backlog", title: "Beklemede" },
- { key: "todo", title: "Yapılacak" },
- { key: "in_progress", title: "Sürüyor" },
- { key: "done", title: "Bitti" },
-];
-
-export const PRIORITY_LABEL: Record = {
- low: "Düşük",
- medium: "Orta",
- high: "Yüksek",
- urgent: "Acil",
-};
-
-export const PRIORITY_COLOR: Record = {
- low: "bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300",
- medium: "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
- high: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300",
- urgent: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-300",
-};
diff --git a/src/app/(dashboard)/tasks/page.tsx b/src/app/(dashboard)/tasks/page.tsx
deleted file mode 100644
index d616e8a..0000000
--- a/src/app/(dashboard)/tasks/page.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import type { Metadata } from "next";
-import { redirect } from "next/navigation";
-
-import { listCustomers } from "@/lib/appwrite/customer-queries";
-import { listTasks } from "@/lib/appwrite/task-queries";
-import { requireTenant } from "@/lib/appwrite/tenant-guard";
-import { createAdminClient } from "@/lib/appwrite/server";
-import { TasksBoard } from "./components/tasks-board";
-import type { TaskFilter } from "./components/types";
-
-export const metadata: Metadata = {
- title: "İşletmem — Görevler",
-};
-
-const ALLOWED_FILTERS: TaskFilter[] = ["all", "mine", "unassigned", "mine_or_unassigned"];
-
-export default async function TasksPage({
- searchParams,
-}: {
- searchParams: Promise<{ view?: string }>;
-}) {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- redirect("/onboarding");
- }
-
- const sp = await searchParams;
- const filter: TaskFilter =
- (ALLOWED_FILTERS as string[]).includes(sp.view ?? "")
- ? (sp.view as TaskFilter)
- : "mine_or_unassigned";
-
- const [allTasks, customers] = await Promise.all([
- listTasks(ctx.tenantId),
- listCustomers(ctx.tenantId),
- ]);
-
- const tasks = allTasks.filter((t) => {
- const assignee = t.assigneeId ?? "";
- if (filter === "mine") return assignee === ctx.user.id;
- if (filter === "unassigned") return !assignee;
- if (filter === "mine_or_unassigned")
- return !assignee || assignee === ctx.user.id;
- return true;
- });
-
- const filterCounts = {
- total: allTasks.length,
- mine: allTasks.filter((t) => t.assigneeId === ctx.user.id).length,
- unassigned: allTasks.filter((t) => !t.assigneeId).length,
- mineOrUnassigned: allTasks.filter(
- (t) => !t.assigneeId || t.assigneeId === ctx.user.id,
- ).length,
- };
-
- let teamMembers: { id: string; name: string }[] = [];
- try {
- const { teams } = createAdminClient();
- const memberships = await teams.listMemberships(ctx.tenantId);
- teamMembers = memberships.memberships.map((m) => ({
- id: m.userId,
- name: m.userName || m.userEmail,
- }));
- } catch {
- /* ignore */
- }
-
- const customerMap = new Map(customers.map((c) => [c.$id, c.name]));
- const memberMap = new Map(teamMembers.map((m) => [m.id, m.name]));
-
- return (
-
-
-
{ctx.settings?.companyName ?? "Çalışma alanı"}
-
Görevler
-
- Sürükle-bırak ile durumları değiştirin. Üstteki filtreden başkalarına atanmış
- görevleri de görebilirsiniz.
-
-
-
-
({
- id: t.$id,
- title: t.title,
- description: t.description ?? "",
- status: t.status ?? "todo",
- priority: t.priority ?? "medium",
- dueDate: t.dueDate ?? "",
- assigneeId: t.assigneeId ?? "",
- assigneeName: t.assigneeId ? memberMap.get(t.assigneeId) ?? "" : "",
- customerId: t.customerId ?? "",
- customerName: t.customerId ? customerMap.get(t.customerId) ?? "" : "",
- order: t.order ?? 0,
- }))}
- customers={customers.map((c) => ({ id: c.$id, name: c.name }))}
- teamMembers={teamMembers}
- currentUserId={ctx.user.id}
- filter={filter}
- filterCounts={filterCounts}
- />
-
- );
-}
diff --git a/src/app/(dashboard)/users/components/data-table.tsx b/src/app/(dashboard)/users/components/data-table.tsx
deleted file mode 100644
index 11eed0c..0000000
--- a/src/app/(dashboard)/users/components/data-table.tsx
+++ /dev/null
@@ -1,535 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import {
- type ColumnDef,
- type ColumnFiltersState,
- type SortingState,
- type VisibilityState,
- type Row,
- flexRender,
- getCoreRowModel,
- getFilteredRowModel,
- getPaginationRowModel,
- getSortedRowModel,
- useReactTable,
-} from "@tanstack/react-table"
-import {
- ChevronDown,
- EllipsisVertical,
- Eye,
- Pencil,
- Trash2,
- Download,
- Search,
-} from "lucide-react"
-
-import { Avatar, AvatarFallback } from "@/components/ui/avatar"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
- DropdownMenu,
- DropdownMenuCheckboxItem,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
-} from "@/components/ui/table"
-import { UserFormDialog } from "./user-form-dialog"
-
-interface User {
- id: number
- name: string
- email: string
- avatar: string
- role: string
- plan: string
- billing: string
- status: string
- joinedDate: string
- lastLogin: string
-}
-
-interface UserFormValues {
- name: string
- email: string
- role: string
- plan: string
- billing: string
- status: string
-}
-
-interface DataTableProps {
- users: User[]
- onDeleteUser: (id: number) => void
- onEditUser: (user: User) => void
- onAddUser: (userData: UserFormValues) => void
-}
-
-export function DataTable({ users, onDeleteUser, onEditUser, onAddUser }: DataTableProps) {
- const [sorting, setSorting] = useState([])
- const [columnFilters, setColumnFilters] = useState([])
- const [columnVisibility, setColumnVisibility] = useState({})
- const [rowSelection, setRowSelection] = useState({})
- const [globalFilter, setGlobalFilter] = useState("")
-
- const getStatusColor = (status: string) => {
- switch (status) {
- case "Active":
- return "text-green-600 bg-green-50 dark:text-green-400 dark:bg-green-900/20"
- case "Pending":
- return "text-orange-600 bg-orange-50 dark:text-orange-400 dark:bg-orange-900/20"
- case "Error":
- return "text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-900/20"
- case "Inactive":
- return "text-gray-600 bg-gray-50 dark:text-gray-400 dark:bg-gray-900/20"
- default:
- return "text-gray-600 bg-gray-50 dark:text-gray-400 dark:bg-gray-900/20"
- }
- }
-
- const getRoleColor = (role: string) => {
- switch (role) {
- case "Admin":
- return "text-red-600 bg-red-50 dark:text-red-400 dark:bg-red-900/20"
- case "Editor":
- return "text-blue-600 bg-blue-50 dark:text-blue-400 dark:bg-blue-900/20"
- case "Author":
- return "text-yellow-600 bg-yellow-50 dark:text-yellow-400 dark:bg-yellow-900/20"
- case "Maintainer":
- return "text-green-600 bg-green-50 dark:text-green-400 dark:bg-green-900/20"
- case "Subscriber":
- return "text-purple-600 bg-purple-50 dark:text-purple-400 dark:bg-purple-900/20"
- default:
- return "text-gray-600 bg-gray-50 dark:text-gray-400 dark:bg-gray-900/20"
- }
- }
-
- const exactFilter = (row: Row, columnId: string, value: string) => {
- return row.getValue(columnId) === value
- }
-
- const columns: ColumnDef[] = [
- {
- id: "select",
- header: ({ table }) => (
-
- table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- />
-
- ),
- cell: ({ row }) => (
-
- row.toggleSelected(!!value)}
- aria-label="Select row"
- />
-
- ),
- enableSorting: false,
- enableHiding: false,
- size: 50,
- },
- {
- accessorKey: "name",
- header: "User",
- cell: ({ row }) => {
- const user = row.original
- return (
-
-
-
- {user.avatar}
-
-
-
- {user.name}
- {user.email}
-
-
- )
- },
- },
- {
- accessorKey: "role",
- header: "Role",
- cell: ({ row }) => {
- const role = row.getValue("role") as string
- return (
-
- {role}
-
- )
- },
- filterFn: exactFilter,
- },
- {
- accessorKey: "plan",
- header: "Plan",
- cell: ({ row }) => {
- const plan = row.getValue("plan") as string
- return {plan}
- },
- filterFn: exactFilter,
- },
- {
- accessorKey: "billing",
- header: "Billing",
- cell: ({ row }) => {
- const billing = row.getValue("billing") as string
- return {billing}
- },
- },
- {
- accessorKey: "status",
- header: "Status",
- cell: ({ row }) => {
- const status = row.getValue("status") as string
- return (
-
- {status}
-
- )
- },
- filterFn: exactFilter,
- },
- {
- id: "actions",
- header: "Actions",
- cell: ({ row }) => {
- const user = row.original
- return (
-
-
-
- View user
-
-
onEditUser(user)}
- >
-
- Edit user
-
-
-
-
-
- More actions
-
-
-
-
- View Details
-
-
- Send Email
-
-
- Reset Password
-
-
- onDeleteUser(user.id)}
- >
-
- Delete User
-
-
-
-
- )
- },
- },
- ]
-
- const table = useReactTable({
- data: users,
- columns,
- onSortingChange: setSorting,
- onColumnFiltersChange: setColumnFilters,
- getCoreRowModel: getCoreRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- getSortedRowModel: getSortedRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- onColumnVisibilityChange: setColumnVisibility,
- onRowSelectionChange: setRowSelection,
- onGlobalFilterChange: setGlobalFilter,
- state: {
- sorting,
- columnFilters,
- columnVisibility,
- rowSelection,
- globalFilter,
- },
- })
-
- const roleFilter = table.getColumn("role")?.getFilterValue() as string
- const planFilter = table.getColumn("plan")?.getFilterValue() as string
- const statusFilter = table.getColumn("status")?.getFilterValue() as string
-
- return (
-
-
-
-
-
- setGlobalFilter(String(event.target.value))}
- className="pl-9"
- />
-
-
-
-
-
- Export
-
-
-
-
-
-
-
-
- Role
-
-
- table.getColumn("role")?.setFilterValue(value === "all" ? "" : value)
- }
- >
-
-
-
-
- All Roles
- Admin
- Author
- Editor
- Maintainer
- Subscriber
-
-
-
-
-
- Plan
-
-
- table.getColumn("plan")?.setFilterValue(value === "all" ? "" : value)
- }
- >
-
-
-
-
- All Plans
- Basic
- Professional
- Enterprise
-
-
-
-
-
- Status
-
-
- table.getColumn("status")?.setFilterValue(value === "all" ? "" : value)
- }
- >
-
-
-
-
- All Status
- Active
- Pending
- Error
- Inactive
-
-
-
-
-
-
- Column Visibility
-
-
-
-
- Columns
-
-
-
- {table
- .getAllColumns()
- .filter((column) => column.getCanHide())
- .map((column) => {
- return (
-
- column.toggleVisibility(!!value)
- }
- >
- {column.id}
-
- )
- })}
-
-
-
-
-
-
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => {
- return (
-
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
-
- )
- })}
-
- ))}
-
-
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => (
-
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext()
- )}
-
- ))}
-
- ))
- ) : (
-
-
- No results.
-
-
- )}
-
-
-
-
-
-
-
-
- Show
-
- {
- table.setPageSize(Number(value))
- }}
- >
-
-
-
-
- {[10, 20, 30, 40, 50].map((pageSize) => (
-
- {pageSize}
-
- ))}
-
-
-
-
- {table.getFilteredSelectedRowModel().rows.length} of{" "}
- {table.getFilteredRowModel().rows.length} row(s) selected.
-
-
-
-
Page
-
- {table.getState().pagination.pageIndex + 1} of{" "}
- {table.getPageCount()}
-
-
-
- table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- className="cursor-pointer"
- >
- Previous
-
- table.nextPage()}
- disabled={!table.getCanNextPage()}
- className="cursor-pointer"
- >
- Next
-
-
-
-
-
- )
-}
diff --git a/src/app/(dashboard)/users/components/stat-cards.tsx b/src/app/(dashboard)/users/components/stat-cards.tsx
deleted file mode 100644
index 0c1d559..0000000
--- a/src/app/(dashboard)/users/components/stat-cards.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Card, CardContent } from "@/components/ui/card"
-import {Users, CreditCard, UserCheck, Clock5, TrendingUp, TrendingDown, ArrowUpRight} from "lucide-react"
-import { Badge } from "@/components/ui/badge"
-import { cn } from '@/lib/utils'
-
-
-const performanceMetrics = [
- {
- title: 'Total Users',
- current: '$2.4M',
- previous: '$1.8M',
- growth: 33.3,
- icon: Users,
- },
- {
- title: 'Paid Users',
- current: '12.5K',
- previous: '9.2K',
- growth: 35.9,
- icon: CreditCard,
- },
- {
- title: 'Active Users',
- current: '8.9k',
- previous: '6.7k',
- growth: 32.8,
- icon: UserCheck,
- },
- {
- title: 'Pending Users',
- current: '17%',
- previous: '24%',
- growth: -8.0,
- icon: Clock5,
- },
-]
-
-export function StatCards() {
- return (
-
- {performanceMetrics.map((metric, index) => (
-
-
-
-
- = 0
- ? 'border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950/20 dark:text-green-400'
- : 'border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950/20 dark:text-red-400',
- )}
- >
- {metric.growth >= 0 ? (
- <>
-
- {metric.growth >= 0 ? '+' : ''}
- {metric.growth}%
- >
- ) : (
- <>
-
- {metric.growth}%
- >
- )}
-
-
-
-
-
{metric.title}
-
{metric.current}
-
-
from {metric.previous}
-
-
-
-
-
- ))}
-
- )
-}
diff --git a/src/app/(dashboard)/users/components/user-form-dialog.tsx b/src/app/(dashboard)/users/components/user-form-dialog.tsx
deleted file mode 100644
index fafac3d..0000000
--- a/src/app/(dashboard)/users/components/user-form-dialog.tsx
+++ /dev/null
@@ -1,231 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Plus } from "lucide-react"
-import { useForm } from "react-hook-form"
-import { z } from "zod"
-import { zodResolver } from "@hookform/resolvers/zod"
-
-const userFormSchema = z.object({
- name: z.string().min(2, {
- message: "Name must be at least 2 characters.",
- }),
- email: z.string().email({
- message: "Please enter a valid email address.",
- }),
- role: z.string().min(1, {
- message: "Please select a role.",
- }),
- plan: z.string().min(1, {
- message: "Please select a plan.",
- }),
- billing: z.string().min(1, {
- message: "Please select a billing method.",
- }),
- status: z.string().min(1, {
- message: "Please select a status.",
- }),
-})
-
-type UserFormValues = z.infer
-
-interface UserFormDialogProps {
- onAddUser: (user: UserFormValues) => void
-}
-
-export function UserFormDialog({ onAddUser }: UserFormDialogProps) {
- const [open, setOpen] = useState(false)
-
- const form = useForm({
- resolver: zodResolver(userFormSchema),
- defaultValues: {
- name: "",
- email: "",
- role: "",
- plan: "",
- billing: "",
- status: "",
- },
- })
-
- function onSubmit(data: UserFormValues) {
- onAddUser(data)
- form.reset()
- setOpen(false)
- }
-
- return (
-
-
-
-
- Add New User
-
-
-
-
- Add New User
-
- Create a new user account. Click save when you're done.
-
-
-
-
- (
-
- Name
-
-
-
-
-
- )}
- />
- (
-
- Email
-
-
-
-
-
- )}
- />
-
- (
-
- Role
-
-
-
-
-
-
-
- Admin
- Author
- Editor
- Maintainer
- Subscriber
-
-
-
-
- )}
- />
- (
-
- Plan
-
-
-
-
-
-
-
- Basic
- Professional
- Enterprise
-
-
-
-
- )}
- />
-
-
- (
-
- Billing
-
-
-
-
-
-
-
- Auto Debit
- UPI
- Paypal
-
-
-
-
- )}
- />
- (
-
- Status
-
-
-
-
-
-
-
- Active
- Pending
- Error
- Inactive
-
-
-
-
- )}
- />
-
-
-
- Save User
-
-
-
-
-
-
- )
-}
diff --git a/src/app/(dashboard)/users/data.json b/src/app/(dashboard)/users/data.json
deleted file mode 100644
index 6541617..0000000
--- a/src/app/(dashboard)/users/data.json
+++ /dev/null
@@ -1,182 +0,0 @@
-[
- {
- "id": 1,
- "name": "Sarah Johnson",
- "email": "sarah.johnson@example.com",
- "avatar": "SJ",
- "role": "Admin",
- "plan": "Enterprise",
- "billing": "UPI",
- "status": "Active",
- "joinedDate": "2024-01-15",
- "lastLogin": "2024-08-12"
- },
- {
- "id": 2,
- "name": "Michael Chen",
- "email": "michael.chen@example.com",
- "avatar": "MC",
- "role": "Editor",
- "plan": "Professional",
- "billing": "Auto Debit",
- "status": "Active",
- "joinedDate": "2024-02-20",
- "lastLogin": "2024-08-13"
- },
- {
- "id": 3,
- "name": "Emily Rodriguez",
- "email": "emily.rodriguez@example.com",
- "avatar": "ER",
- "role": "Author",
- "plan": "Basic",
- "billing": "Paypal",
- "status": "Pending",
- "joinedDate": "2024-03-10",
- "lastLogin": "2024-08-10"
- },
- {
- "id": 4,
- "name": "David Thompson",
- "email": "david.thompson@example.com",
- "avatar": "DT",
- "role": "Maintainer",
- "plan": "Enterprise",
- "billing": "Auto Debit",
- "status": "Active",
- "joinedDate": "2024-01-25",
- "lastLogin": "2024-08-13"
- },
- {
- "id": 5,
- "name": "Jessica Parker",
- "email": "jessica.parker@example.com",
- "avatar": "JP",
- "role": "Subscriber",
- "plan": "Basic",
- "billing": "UPI",
- "status": "Inactive",
- "joinedDate": "2024-04-05",
- "lastLogin": "2024-07-20"
- },
- {
- "id": 6,
- "name": "Robert Wilson",
- "email": "robert.wilson@example.com",
- "avatar": "RW",
- "role": "Admin",
- "plan": "Professional",
- "billing": "Auto Debit",
- "status": "Active",
- "joinedDate": "2024-02-14",
- "lastLogin": "2024-08-12"
- },
- {
- "id": 7,
- "name": "Amanda Foster",
- "email": "amanda.foster@example.com",
- "avatar": "AF",
- "role": "Author",
- "plan": "Professional",
- "billing": "Paypal",
- "status": "Error",
- "joinedDate": "2024-03-22",
- "lastLogin": "2024-08-08"
- },
- {
- "id": 8,
- "name": "Christopher Lee",
- "email": "christopher.lee@example.com",
- "avatar": "CL",
- "role": "Editor",
- "plan": "Enterprise",
- "billing": "Auto Debit",
- "status": "Active",
- "joinedDate": "2024-01-30",
- "lastLogin": "2024-08-13"
- },
- {
- "id": 9,
- "name": "Lisa Martinez",
- "email": "lisa.martinez@example.com",
- "avatar": "LM",
- "role": "Maintainer",
- "plan": "Basic",
- "billing": "UPI",
- "status": "Pending",
- "joinedDate": "2024-04-18",
- "lastLogin": "2024-08-11"
- },
- {
- "id": 10,
- "name": "James Anderson",
- "email": "james.anderson@example.com",
- "avatar": "JA",
- "role": "Subscriber",
- "plan": "Professional",
- "billing": "Auto Debit",
- "status": "Active",
- "joinedDate": "2024-02-28",
- "lastLogin": "2024-08-12"
- },
- {
- "id": 11,
- "name": "Maria Garcia",
- "email": "maria.garcia@example.com",
- "avatar": "MG",
- "role": "Editor",
- "plan": "Enterprise",
- "billing": "Paypal",
- "status": "Active",
- "joinedDate": "2024-03-15",
- "lastLogin": "2024-08-13"
- },
- {
- "id": 12,
- "name": "Kevin Taylor",
- "email": "kevin.taylor@example.com",
- "avatar": "KT",
- "role": "Author",
- "plan": "Basic",
- "billing": "Auto Debit",
- "status": "Inactive",
- "joinedDate": "2024-04-12",
- "lastLogin": "2024-07-25"
- },
- {
- "id": 13,
- "name": "Rachel Brown",
- "email": "rachel.brown@example.com",
- "avatar": "RB",
- "role": "Admin",
- "plan": "Enterprise",
- "billing": "UPI",
- "status": "Active",
- "joinedDate": "2024-01-08",
- "lastLogin": "2024-08-13"
- },
- {
- "id": 14,
- "name": "Daniel Kim",
- "email": "daniel.kim@example.com",
- "avatar": "DK",
- "role": "Maintainer",
- "plan": "Professional",
- "billing": "Auto Debit",
- "status": "Pending",
- "joinedDate": "2024-03-28",
- "lastLogin": "2024-08-09"
- },
- {
- "id": 15,
- "name": "Ashley White",
- "email": "ashley.white@example.com",
- "avatar": "AW",
- "role": "Subscriber",
- "plan": "Basic",
- "billing": "Paypal",
- "status": "Error",
- "joinedDate": "2024-04-22",
- "lastLogin": "2024-08-05"
- }
-]
diff --git a/src/app/(dashboard)/users/page.tsx b/src/app/(dashboard)/users/page.tsx
deleted file mode 100644
index a86a0d3..0000000
--- a/src/app/(dashboard)/users/page.tsx
+++ /dev/null
@@ -1,84 +0,0 @@
-"use client"
-
-import { useState } from "react"
-import { StatCards } from "./components/stat-cards"
-import { DataTable } from "./components/data-table"
-
-import initialUsersData from "./data.json"
-
-interface User {
- id: number
- name: string
- email: string
- avatar: string
- role: string
- plan: string
- billing: string
- status: string
- joinedDate: string
- lastLogin: string
-}
-
-interface UserFormValues {
- name: string
- email: string
- role: string
- plan: string
- billing: string
- status: string
-}
-
-export default function UsersPage() {
- const [users, setUsers] = useState(initialUsersData)
-
- const generateAvatar = (name: string) => {
- const names = name.split(" ")
- if (names.length >= 2) {
- return `${names[0][0]}${names[1][0]}`.toUpperCase()
- }
- return name.substring(0, 2).toUpperCase()
- }
-
- const handleAddUser = (userData: UserFormValues) => {
- const newUser: User = {
- id: Math.max(...users.map(u => u.id)) + 1,
- name: userData.name,
- email: userData.email,
- avatar: generateAvatar(userData.name),
- role: userData.role,
- plan: userData.plan,
- billing: userData.billing,
- status: userData.status,
- joinedDate: new Date().toISOString().split('T')[0],
- lastLogin: new Date().toISOString().split('T')[0],
- }
- setUsers(prev => [newUser, ...prev])
- }
-
- const handleDeleteUser = (id: number) => {
- setUsers(prev => prev.filter(user => user.id !== id))
- }
-
- const handleEditUser = (user: User) => {
- // For now, just log the user to edit
- // In a real app, you'd open an edit dialog
- console.log("Edit user:", user)
- }
-
- return (
-
- )
-}
diff --git a/src/app/api/payments/polar/callback/route.ts b/src/app/api/payments/polar/callback/route.ts
index e7f5120..654017c 100644
--- a/src/app/api/payments/polar/callback/route.ts
+++ b/src/app/api/payments/polar/callback/route.ts
@@ -1,108 +1,5 @@
-import { NextRequest, NextResponse } from "next/server";
-import { WebhookVerificationError } from "@polar-sh/sdk/webhooks";
-import { Query } from "node-appwrite";
-
-import { logAudit } from "@/lib/appwrite/audit";
-import { DATABASE_ID, TABLES, type SubscriptionPayment } from "@/lib/appwrite/schema";
-import { createAdminClient } from "@/lib/appwrite/server";
-import { verifyAndParsePolarWebhook } from "@/lib/payments/polar";
-
-const PRO_VALIDITY_DAYS = 30;
-
-export async function POST(req: NextRequest): Promise {
- let rawBody: string;
- try {
- rawBody = await req.text();
- } catch {
- return NextResponse.json({ error: "invalid body" }, { status: 400 });
- }
-
- const headers: Record = {};
- req.headers.forEach((value, key) => { headers[key] = value; });
-
- let event: ReturnType;
- try {
- event = verifyAndParsePolarWebhook(headers, rawBody);
- } catch (e) {
- if (e instanceof WebhookVerificationError) {
- console.error("[polar/callback] signature mismatch");
- return NextResponse.json({ error: "signature mismatch" }, { status: 403 });
- }
- return NextResponse.json({ error: "invalid payload" }, { status: 400 });
- }
-
- // order.created veya checkout.updated (status=confirmed) eventlerini işle
- const isOrderCreated = event.type === "order.created";
- const isCheckoutConfirmed =
- event.type === "checkout.updated" && event.data.status === "confirmed";
-
- if (!isOrderCreated && !isCheckoutConfirmed) {
- return new NextResponse("OK", { status: 200 });
- }
-
- const metadata = (event.data.metadata ?? {}) as Record;
- const crmOrderId = metadata.crm_order_id ?? "";
-
- if (!crmOrderId) {
- console.error("[polar/callback] no crm_order_id in metadata", event.type);
- return new NextResponse("OK", { status: 200 });
- }
-
- const { tablesDB } = createAdminClient();
-
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.subscriptionPayments,
- queries: [
- Query.equal("orderId", crmOrderId),
- Query.equal("provider", "polar"),
- Query.limit(1),
- ],
- });
-
- const payment = result.rows[0] as unknown as SubscriptionPayment | undefined;
- if (!payment) {
- console.error("[polar/callback] payment not found", crmOrderId);
- return new NextResponse("OK", { status: 200 });
- }
-
- if (payment.status === "success" || payment.status === "failed") {
- return new NextResponse("OK", { status: 200 });
- }
-
- const now = new Date();
- const expires = new Date(now.getTime() + PRO_VALIDITY_DAYS * 24 * 60 * 60 * 1000);
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.subscriptionPayments, payment.$id, {
- status: "success",
- processedAt: now.toISOString(),
- providerPayload: JSON.stringify({ polar: true, eventType: event.type, ...metadata }),
- });
-
- const settingsResult = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.tenantSettings,
- queries: [Query.equal("tenantId", payment.tenantId), Query.limit(1)],
- });
- const settings = settingsResult.rows[0] as unknown as { $id: string } | undefined;
-
- if (settings) {
- await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, settings.$id, {
- plan: payment.plan,
- planStartedAt: now.toISOString(),
- planExpiresAt: expires.toISOString(),
- lastPaymentId: payment.$id,
- });
- }
-
- await logAudit({
- tenantId: payment.tenantId,
- userId: payment.createdBy,
- action: "update",
- entityType: "subscription_payment",
- entityId: payment.$id,
- changes: { provider: "polar", status: "success", plan: payment.plan },
- });
+import { NextResponse } from "next/server";
+export async function POST() {
return new NextResponse("OK", { status: 200 });
}
diff --git a/src/app/api/payments/shopier/callback/route.ts b/src/app/api/payments/shopier/callback/route.ts
index 8dc8ed5..654017c 100644
--- a/src/app/api/payments/shopier/callback/route.ts
+++ b/src/app/api/payments/shopier/callback/route.ts
@@ -1,137 +1,5 @@
-import { NextRequest, NextResponse } from "next/server";
-import { Query } from "node-appwrite";
-
-import { logAudit } from "@/lib/appwrite/audit";
-import { DATABASE_ID, TABLES, type SubscriptionPayment } from "@/lib/appwrite/schema";
-import { createAdminClient } from "@/lib/appwrite/server";
-import {
- verifyShopierWebhookSignature,
- type ShopierWebhookOrder,
-} from "@/lib/payments/shopier";
-
-const PRO_VALIDITY_DAYS = 30;
-
-export async function POST(req: NextRequest): Promise {
- const signature = req.headers.get("Shopier-Signature") ?? "";
- const event = req.headers.get("Shopier-Event") ?? "";
-
- // Raw body'yi hem imza doğrulama hem de parse için kullan
- let rawBody: string;
- try {
- rawBody = await req.text();
- } catch {
- return NextResponse.json({ error: "invalid body" }, { status: 400 });
- }
-
- if (!verifyShopierWebhookSignature(signature, rawBody)) {
- console.error("[shopier/callback] signature mismatch", { event });
- return NextResponse.json({ error: "signature mismatch" }, { status: 403 });
- }
-
- // Yalnızca sipariş oluşturma eventini işle
- if (event !== "order.created") {
- return new NextResponse("OK", { status: 200 });
- }
-
- let order: ShopierWebhookOrder;
- try {
- order = JSON.parse(rawBody) as ShopierWebhookOrder;
- } catch {
- return NextResponse.json({ error: "invalid json" }, { status: 400 });
- }
-
- // Yalnızca ödenmiş siparişleri işle
- if (order.paymentStatus !== "paid") {
- return new NextResponse("OK", { status: 200 });
- }
-
- const buyerEmail = order.shippingInfo?.email ?? "";
- if (!buyerEmail) {
- console.error("[shopier/callback] no buyer email in order", order.id);
- return new NextResponse("OK", { status: 200 });
- }
-
- const { tablesDB } = createAdminClient();
-
- // Bekleyen Shopier ödemelerini al, email ile eşleştir
- const pendingResult = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.subscriptionPayments,
- queries: [
- Query.equal("provider", "shopier"),
- Query.equal("status", "pending"),
- Query.orderDesc("$createdAt"),
- Query.limit(20),
- ],
- });
-
- const pendingPayments = pendingResult.rows as unknown as SubscriptionPayment[];
-
- const payment = pendingPayments.find((p) => {
- try {
- const payload = JSON.parse(p.providerPayload ?? "{}") as { userEmail?: string };
- return payload.userEmail?.toLowerCase() === buyerEmail.toLowerCase();
- } catch {
- return false;
- }
- });
-
- if (!payment) {
- console.error("[shopier/callback] no matching pending payment for email", buyerEmail);
- // 200 döndür — Shopier'in retry yapmasını engelle, durumu logla
- return new NextResponse("OK", { status: 200 });
- }
-
- // Idempotency — zaten işlendiyse atla
- if (payment.status === "success" || payment.status === "failed") {
- return new NextResponse("OK", { status: 200 });
- }
-
- const now = new Date();
- const expires = new Date(now.getTime() + PRO_VALIDITY_DAYS * 24 * 60 * 60 * 1000);
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.subscriptionPayments, payment.$id, {
- status: "success",
- processedAt: now.toISOString(),
- providerPayload: JSON.stringify({
- shopierOrderId: order.id,
- buyerEmail,
- total: order.totals?.total,
- currency: order.currency,
- dateCreated: order.dateCreated,
- }),
- });
-
- // Tenant planını aktive et
- const settingsResult = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.tenantSettings,
- queries: [Query.equal("tenantId", payment.tenantId), Query.limit(1)],
- });
- const settings = settingsResult.rows[0] as unknown as { $id: string } | undefined;
-
- if (settings) {
- await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, settings.$id, {
- plan: payment.plan,
- planStartedAt: now.toISOString(),
- planExpiresAt: expires.toISOString(),
- lastPaymentId: payment.$id,
- });
- }
-
- await logAudit({
- tenantId: payment.tenantId,
- userId: payment.createdBy,
- action: "update",
- entityType: "subscription_payment",
- entityId: payment.$id,
- changes: {
- provider: "shopier",
- status: "success",
- plan: payment.plan,
- shopierOrderId: order.id,
- },
- });
+import { NextResponse } from "next/server";
+export async function POST() {
return new NextResponse("OK", { status: 200 });
}
diff --git a/src/app/d/[code]/page.tsx b/src/app/d/[code]/page.tsx
index 53d7ec5..15dc5e1 100644
--- a/src/app/d/[code]/page.tsx
+++ b/src/app/d/[code]/page.tsx
@@ -22,7 +22,7 @@ async function getCompanyName(tenantId: string): Promise {
tableId: TABLES.tenantSettings,
queries: [Query.equal("tenantId", tenantId), Query.limit(1)],
});
- return (result.rows[0] as unknown as TenantSettings | undefined)?.companyName ?? null;
+ return (result.rows[0] as unknown as TenantSettings | undefined)?.officeName ?? null;
} catch {
return null;
}
@@ -44,7 +44,7 @@ export default async function InvitePage({
- İşletmem
+ KovakEmlak
{!invite || invite.status === "cancelled" ? (
diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx
index c34e406..b4308d4 100644
--- a/src/components/app-sidebar.tsx
+++ b/src/components/app-sidebar.tsx
@@ -2,16 +2,14 @@
import * as React from "react";
import {
- Briefcase,
- Calendar,
- CheckSquare,
+ Activity,
+ Building2,
CreditCard,
FileText,
LayoutDashboard,
- Package,
- Receipt,
+ Presentation,
+ Search,
Settings,
- TrendingUp,
Users,
Wallet,
} from "lucide-react";
@@ -37,79 +35,54 @@ const navGroups = [
label: "Genel",
items: [
{
- title: "Genel bakış",
+ title: "Genel Bakış",
url: "/dashboard",
icon: LayoutDashboard,
},
],
},
{
- label: "İşletme",
+ label: "Portföy",
items: [
{
- title: "Müşteri Adayları",
- url: "/leads",
- icon: TrendingUp,
+ title: "İlanlar",
+ url: "/properties",
+ icon: Building2,
},
+ ],
+ },
+ {
+ label: "İlişkiler",
+ items: [
{
title: "Müşteriler",
url: "/customers",
icon: Users,
- },
- {
- title: "Hizmetler",
- url: "/services",
- icon: Briefcase,
- },
- {
- title: "Yazılımlarımız",
- url: "/software",
- icon: Package,
- },
- ],
- },
- {
- label: "Operasyon",
- items: [
- {
- title: "Takvim",
- url: "/calendar",
- icon: Calendar,
- },
- {
- title: "Görevler",
- url: "/tasks",
- icon: CheckSquare,
- },
- ],
- },
- {
- label: "Finans",
- items: [
- {
- title: "Gelir / Gider",
- url: "/finance",
- icon: Wallet,
- },
- {
- title: "Bankalar",
- url: "/finance/banks",
- icon: Briefcase,
items: [
- { title: "Banka hesapları", url: "/finance/banks" },
- { title: "Krediler", url: "/finance/loans" },
- { title: "Kredi kartları", url: "/finance/cards" },
+ { title: "Müşteri Listesi", url: "/customers" },
+ { title: "Arama Kriterleri", url: "/customers/searches" },
+ { title: "Eşleşmeler", url: "/customers/matches" },
],
},
{
- title: "Faturalar",
- url: "/invoices",
- icon: Receipt,
+ title: "Yatırımcılar",
+ url: "/investors",
+ icon: Wallet,
+ },
+ ],
+ },
+ {
+ label: "Satış",
+ items: [
+ {
+ title: "Sunumlar",
+ url: "/presentations",
+ icon: Presentation,
},
{
- title: "Rapor",
- url: "/finance/reports",
- icon: FileText,
+ title: "Aktiviteler",
+ url: "/activities",
+ icon: Activity,
},
],
},
@@ -117,13 +90,12 @@ const navGroups = [
label: "Hesap",
items: [
{
- title: "Çalışma alanı",
+ title: "Çalışma Alanı",
url: "/settings/workspace",
icon: Settings,
items: [
- { title: "Şirket bilgileri", url: "/settings/workspace" },
- { title: "Ekip üyeleri", url: "/settings/members" },
- { title: "Faturalama", url: "/settings/billing" },
+ { title: "Ofis Bilgileri", url: "/settings/workspace" },
+ { title: "Ekip Üyeleri", url: "/settings/members" },
],
},
{
@@ -131,11 +103,6 @@ const navGroups = [
url: "/settings/account",
icon: FileText,
},
- {
- title: "Plan",
- url: "/pricing",
- icon: CreditCard,
- },
],
},
];
@@ -170,7 +137,7 @@ export function AppSidebar({
)}
- İşletmem
+ KovakEmlak
{company.name}
diff --git a/src/components/billing/usage-banner.tsx b/src/components/billing/usage-banner.tsx
deleted file mode 100644
index ad7b875..0000000
--- a/src/components/billing/usage-banner.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import Link from "next/link";
-import { AlertTriangle, Crown } from "lucide-react";
-
-import { Button } from "@/components/ui/button";
-import { Card, CardContent } from "@/components/ui/card";
-import {
- RESOURCE_LABELS,
- type PlanResource,
- type PlanUsage,
-} from "@/lib/appwrite/plan-limits";
-
-const SOFT_THRESHOLD = 0.8;
-
-export function UsageBanner({
- usage,
- resource,
-}: {
- usage: PlanUsage;
- resource: PlanResource;
-}) {
- if (usage.plan === "pro") return null;
-
- const u = usage.usage[resource];
- if (u.limit === Number.POSITIVE_INFINITY) return null;
-
- const ratio = u.used / u.limit;
- if (ratio < SOFT_THRESHOLD) return null;
-
- const label = RESOURCE_LABELS[resource];
- const reached = u.reached;
-
- return (
-
-
-
-
-
-
- {reached ? "Sınıra ulaşıldı" : "Sınıra yaklaşıyorsun"}:
- {" "}
-
- {u.used} / {u.limit} {label}.
-
- {reached && (
- Yeni {label} eklemek için Pro'ya geç.
- )}
-
-
-
-
-
- Pro'ya geç
-
-
-
-
- );
-}
diff --git a/src/components/command-search.tsx b/src/components/command-search.tsx
index d20aacf..09e7868 100644
--- a/src/components/command-search.tsx
+++ b/src/components/command-search.tsx
@@ -4,18 +4,12 @@ import * as React from "react";
import { useRouter } from "next/navigation";
import { Command as CommandPrimitive } from "cmdk";
import {
- Briefcase,
- Calendar as CalendarIcon,
- CheckSquare,
- CircleDollarSign,
- FilePlus,
+ Activity,
+ Building2,
LayoutDashboard,
- Loader2,
- Package,
- Receipt,
+ Presentation,
Search,
Settings,
- UserPlus,
Users,
Wallet,
type LucideIcon,
@@ -23,12 +17,6 @@ import {
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
-import {
- globalSearchAction,
- type SearchGroup,
- type SearchHit,
- type SearchResults,
-} from "@/lib/appwrite/search-actions";
const Command = React.forwardRef<
React.ElementRef
,
@@ -41,333 +29,81 @@ const Command = React.forwardRef<
className,
)}
{...props}
- shouldFilter={false}
/>
));
Command.displayName = CommandPrimitive.displayName;
-const CommandInput = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-
-
-
-));
-CommandInput.displayName = CommandPrimitive.Input.displayName;
+type NavItem = { title: string; url: string; icon: LucideIcon };
-const CommandList = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-CommandList.displayName = CommandPrimitive.List.displayName;
-
-const CommandEmpty = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->((props, ref) => (
-
-));
-CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
-
-const CommandGroup = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-CommandGroup.displayName = CommandPrimitive.Group.displayName;
-
-const CommandItem = React.forwardRef<
- React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
-));
-CommandItem.displayName = CommandPrimitive.Item.displayName;
-
-type NavItem = { key: string; title: string; url: string; icon: LucideIcon };
-
-const NAV: NavItem[] = [
- { key: "nav-dashboard", title: "Genel bakış", url: "/dashboard", icon: LayoutDashboard },
- { key: "nav-customers", title: "Müşteriler", url: "/customers", icon: Users },
- { key: "nav-services", title: "Hizmetler", url: "/services", icon: Briefcase },
- { key: "nav-software", title: "Yazılımlarımız", url: "/software", icon: Package },
- { key: "nav-calendar", title: "Takvim", url: "/calendar", icon: CalendarIcon },
- { key: "nav-tasks", title: "Görevler", url: "/tasks", icon: CheckSquare },
- { key: "nav-finance", title: "Gelir / Gider", url: "/finance", icon: Wallet },
- { key: "nav-invoices", title: "Faturalar", url: "/invoices", icon: Receipt },
+const NAV_ITEMS: NavItem[] = [
+ { title: "Genel Bakış", url: "/dashboard", icon: LayoutDashboard },
+ { title: "İlanlar", url: "/properties", icon: Building2 },
+ { title: "Müşteriler", url: "/customers", icon: Users },
+ { title: "Yatırımcılar", url: "/investors", icon: Wallet },
+ { title: "Sunumlar", url: "/presentations", icon: Presentation },
+ { title: "Aktiviteler", url: "/activities", icon: Activity },
+ { title: "Ofis Ayarları", url: "/settings/workspace", icon: Settings },
];
-const QUICK_ACTIONS: NavItem[] = [
- { key: "qa-customer", title: "Yeni müşteri ekle", url: "/customers", icon: UserPlus },
- { key: "qa-invoice", title: "Yeni fatura kes", url: "/invoices", icon: Receipt },
- { key: "qa-task", title: "Yeni görev oluştur", url: "/tasks", icon: FilePlus },
- { key: "qa-event", title: "Takvime etkinlik ekle", url: "/calendar", icon: CalendarIcon },
- { key: "qa-finance", title: "Yeni gelir/gider girişi", url: "/finance", icon: CircleDollarSign },
-];
-
-const SETTINGS_NAV: NavItem[] = [
- { key: "set-workspace", title: "Şirket bilgileri", url: "/settings/workspace", icon: Settings },
- { key: "set-members", title: "Ekip üyeleri", url: "/settings/members", icon: Users },
- { key: "set-account", title: "Profil", url: "/settings/account", icon: Settings },
-];
-
-const GROUP_LABEL: Record = {
- customers: "Müşteriler",
- invoices: "Faturalar",
- tasks: "Görevler",
- services: "Hizmetler",
- software: "Yazılımlar",
- events: "Takvim",
- finance: "Finans",
-};
-
-const GROUP_ICON: Record = {
- customers: Users,
- invoices: Receipt,
- tasks: CheckSquare,
- services: Briefcase,
- software: Package,
- events: CalendarIcon,
- finance: Wallet,
-};
-
-const GROUP_ORDER: SearchGroup[] = [
- "customers",
- "invoices",
- "tasks",
- "events",
- "finance",
- "services",
- "software",
-];
-
-interface CommandSearchProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
-}
-
-function filterNav(items: NavItem[], q: string): NavItem[] {
- if (!q) return items;
- const lower = q.toLocaleLowerCase("tr-TR");
- return items.filter((i) => i.title.toLocaleLowerCase("tr-TR").includes(lower));
-}
-
-export function CommandSearch({ open, onOpenChange }: CommandSearchProps) {
- const router = useRouter();
+export function CommandSearch() {
+ const [open, setOpen] = React.useState(false);
const [query, setQuery] = React.useState("");
- const [results, setResults] = React.useState(null);
- const [loading, setLoading] = React.useState(false);
+ const router = useRouter();
React.useEffect(() => {
- if (!open) {
- setQuery("");
- setResults(null);
- setLoading(false);
- }
- }, [open]);
-
- React.useEffect(() => {
- const trimmed = query.trim();
- if (trimmed.length < 2) {
- setResults(null);
- setLoading(false);
- return;
- }
- setLoading(true);
- const t = setTimeout(async () => {
- try {
- const r = await globalSearchAction(trimmed);
- setResults(r);
- } catch {
- setResults(null);
- } finally {
- setLoading(false);
+ const down = (e: KeyboardEvent) => {
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ setOpen((o) => !o);
}
- }, 220);
- return () => clearTimeout(t);
- }, [query]);
+ };
+ document.addEventListener("keydown", down);
+ return () => document.removeEventListener("keydown", down);
+ }, []);
- const handleSelect = (url: string) => {
- router.push(url);
- onOpenChange(false);
- };
-
- const navMatches = filterNav(NAV, query);
- const quickMatches = filterNav(QUICK_ACTIONS, query);
- const settingsMatches = filterNav(SETTINGS_NAV, query);
-
- const totalEntityHits = results
- ? GROUP_ORDER.reduce((s, g) => s + results[g].length, 0)
- : 0;
-
- const showEmpty =
- query.trim().length >= 2 &&
- !loading &&
- totalEntityHits === 0 &&
- navMatches.length === 0 &&
- quickMatches.length === 0 &&
- settingsMatches.length === 0;
+ const filtered = query
+ ? NAV_ITEMS.filter((item) => item.title.toLowerCase().includes(query.toLowerCase()))
+ : NAV_ITEMS;
return (
-
-
- Arama
-
-
-
- {loading && (
-
-
- Aranıyor...
-
- )}
+ <>
+ setOpen(true)}
+ className="bg-muted text-muted-foreground flex items-center gap-2 rounded-md px-3 py-1.5 text-sm"
+ >
+
+ Ara...
+ ⌘K
+
- {showEmpty && Sonuç bulunamadı. }
-
- {results &&
- !loading &&
- GROUP_ORDER.map((group) => {
- const hits = results[group];
- if (hits.length === 0) return null;
- const Icon = GROUP_ICON[group];
- return (
-
- {hits.map((h: SearchHit) => (
- handleSelect(h.url)}
- >
-
-
- {h.title}
- {h.subtitle && (
-
- {h.subtitle}
-
- )}
-
-
- ))}
-
- );
- })}
-
- {navMatches.length > 0 && (
-
- {navMatches.map((item) => {
- const Icon = item.icon;
- return (
- handleSelect(item.url)}
- >
-
- {item.title}
-
- );
- })}
-
- )}
-
- {quickMatches.length > 0 && (
-
- {quickMatches.map((item) => {
- const Icon = item.icon;
- return (
- handleSelect(item.url)}
- >
-
- {item.title}
-
- );
- })}
-
- )}
-
- {settingsMatches.length > 0 && (
-
- {settingsMatches.map((item) => {
- const Icon = item.icon;
- return (
- handleSelect(item.url)}
- >
-
- {item.title}
-
- );
- })}
-
- )}
-
-
-
- ↵ Aç · ↑↓ Gez · Esc Kapat
- ⌘K
-
-
-
-
- );
-}
-
-export function SearchTrigger({ onClick }: { onClick: () => void }) {
- return (
-
-
- Hızlı ara...
-
- ⌘ K
-
-
+
+
+ Arama
+
+
+
+ setQuery(e.target.value)}
+ placeholder="Sayfa ara..."
+ className="placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none"
+ />
+
+
+ {filtered.map((item) => (
+ { router.push(item.url); setOpen(false); setQuery(""); }}
+ className="hover:bg-accent flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm"
+ >
+
+ {item.title}
+
+ ))}
+
+
+
+
+ >
);
}
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index f8e8ddb..c930856 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -3,7 +3,7 @@
import * as React from "react";
import { Building2 } from "lucide-react";
-import { CommandSearch, SearchTrigger } from "@/components/command-search";
+import { CommandSearch } from "@/components/command-search";
import { ModeToggle } from "@/components/mode-toggle";
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";
@@ -11,46 +11,29 @@ import { SidebarTrigger } from "@/components/ui/sidebar";
import type { ShellCompany } from "@/app/(dashboard)/dashboard-shell";
export function SiteHeader({ company }: { company?: ShellCompany }) {
- const [searchOpen, setSearchOpen] = React.useState(false);
-
- React.useEffect(() => {
- const down = (e: KeyboardEvent) => {
- if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
- e.preventDefault();
- setSearchOpen((open) => !open);
- }
- };
-
- document.addEventListener("keydown", down);
- return () => document.removeEventListener("keydown", down);
- }, []);
-
return (
- <>
-
-
-
-
+
+
+
+
- {company && (
-
-
- {company.name}
-
- )}
-
-
-
- setSearchOpen(true)} />
-
-
+ {company && (
+
+
+ {company.name}
+ )}
+
+
-
-
- >
+
+
);
}
diff --git a/src/lib/appwrite/audit.ts b/src/lib/appwrite/audit.ts
index 5c5cf1a..95fcf08 100644
--- a/src/lib/appwrite/audit.ts
+++ b/src/lib/appwrite/audit.ts
@@ -4,7 +4,9 @@ import { headers } from "next/headers";
import { ID, Permission, Role } from "node-appwrite";
import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type AuditAction } from "./schema";
+import { DATABASE_ID, TABLES } from "./schema";
+
+type AuditAction = string;
export async function logAudit(args: {
tenantId: string;
@@ -23,7 +25,7 @@ export async function logAudit(args: {
const { tablesDB } = createAdminClient();
await tablesDB.createRow(
DATABASE_ID,
- TABLES.auditLogs,
+ "audit_logs",
ID.unique(),
{
tenantId: args.tenantId,
diff --git a/src/lib/appwrite/bank-account-actions.ts b/src/lib/appwrite/bank-account-actions.ts
deleted file mode 100644
index f7e0ea3..0000000
--- a/src/lib/appwrite/bank-account-actions.ts
+++ /dev/null
@@ -1,243 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Query } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import { DATABASE_ID, TABLES, type BankAccount } from "./schema";
-import { canAccessRow, scopedRowPermissions } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { BankAccountActionState } from "./bank-account-types";
-import { bankAccountSchema } from "@/lib/validation/bank-accounts";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) return e.message || "Beklenmeyen hata.";
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record
{
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function pickFormFields(formData: FormData) {
- return {
- bankName: String(formData.get("bankName") ?? "").trim(),
- accountName: String(formData.get("accountName") ?? "").trim(),
- iban: String(formData.get("iban") ?? "").trim(),
- openingBalance: String(formData.get("openingBalance") ?? "0"),
- notes: String(formData.get("notes") ?? "").trim(),
- scope: (formData.get("scope") as "company" | "personal" | null) ?? "company",
- };
-}
-
-export async function createBankAccountAction(
- _prev: BankAccountActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = bankAccountSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.bankAccounts,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...parsed.data,
- },
- scopedRowPermissions(ctx.tenantId, ctx.user.id, parsed.data.scope),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "bank_account",
- entityId: row.$id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/banks");
- return { ok: true };
-}
-
-export async function updateBankAccountAction(
- _prev: BankAccountActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = bankAccountSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.bankAccounts,
- id,
- )) as unknown as BankAccount;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.updateRow(
- DATABASE_ID,
- TABLES.bankAccounts,
- id,
- parsed.data,
- scopedRowPermissions(ctx.tenantId, existing.createdBy, parsed.data.scope),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "bank_account",
- entityId: id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/banks");
- return { ok: true };
-}
-
-export async function archiveBankAccountAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.bankAccounts,
- id,
- )) as unknown as BankAccount;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const newArchivedState = !existing.archived;
- await tablesDB.updateRow(DATABASE_ID, TABLES.bankAccounts, id, {
- archived: newArchivedState,
- });
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "bank_account",
- entityId: id,
- changes: { archived: newArchivedState },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/banks");
- return { ok: true };
-}
-
-export async function deleteBankAccountAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.bankAccounts,
- id,
- )) as unknown as BankAccount;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- // Block delete if any finance_entry still references this account.
- const linked = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("bankAccountId", id),
- Query.limit(1),
- ],
- });
- if (linked.rows.length > 0) {
- return {
- ok: false,
- error:
- "Bu hesaba bağlı finans hareketleri var. Önce hesabı arşivleyin veya hareketleri başka bir hesaba taşıyın.",
- };
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.bankAccounts, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "bank_account",
- entityId: id,
- changes: { bankName: existing.bankName, accountName: existing.accountName },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/banks");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/bank-account-queries.ts b/src/lib/appwrite/bank-account-queries.ts
deleted file mode 100644
index 6175162..0000000
--- a/src/lib/appwrite/bank-account-queries.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { canAccessRow } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import {
- DATABASE_ID,
- TABLES,
- type BankAccount,
- type FinanceEntry,
-} from "./schema";
-
-/**
- * Returns bank accounts the current user is allowed to see:
- * - all `company` scope rows
- * - personal-scope rows where createdBy === currentUserId
- */
-export async function listBankAccounts(
- tenantId: string,
- currentUserId?: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.bankAccounts,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderAsc("bankName"),
- Query.limit(200),
- ],
- });
- const rows = result.rows as unknown as BankAccount[];
- if (!currentUserId) return rows;
- return rows.filter((r) => canAccessRow(r, currentUserId));
- } catch {
- return [];
- }
-}
-
-/**
- * Computes a current balance for each visible account.
- */
-export async function getBankAccountBalances(
- tenantId: string,
- currentUserId?: string,
-): Promise> {
- const balances = new Map();
- try {
- const accounts = await listBankAccounts(tenantId, currentUserId);
- const visibleIds = new Set(accounts.map((a) => a.$id));
- for (const a of accounts) balances.set(a.$id, a.openingBalance ?? 0);
-
- const { tablesDB } = createAdminClient();
- const entries = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.isNotNull("bankAccountId"),
- Query.limit(5000),
- ],
- });
- for (const e of entries.rows as unknown as FinanceEntry[]) {
- if (!e.bankAccountId || !visibleIds.has(e.bankAccountId)) continue;
- const cur = balances.get(e.bankAccountId);
- if (cur === undefined) continue;
- if (e.type === "income") balances.set(e.bankAccountId, cur + e.amount);
- else if (e.type === "expense") balances.set(e.bankAccountId, cur - e.amount);
- }
- } catch {
- /* ignore */
- }
- return balances;
-}
-
-export async function listEntriesForAccount(
- tenantId: string,
- bankAccountId: string,
- limit = 25,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("bankAccountId", bankAccountId),
- Query.orderDesc("date"),
- Query.limit(limit),
- ],
- });
- return result.rows as unknown as FinanceEntry[];
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/bank-account-types.ts b/src/lib/appwrite/bank-account-types.ts
deleted file mode 100644
index 0e93c69..0000000
--- a/src/lib/appwrite/bank-account-types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type BankAccountActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
-};
-
-export const initialBankAccountState: BankAccountActionState = { ok: false };
diff --git a/src/lib/appwrite/calendar-actions.ts b/src/lib/appwrite/calendar-actions.ts
deleted file mode 100644
index e512fe3..0000000
--- a/src/lib/appwrite/calendar-actions.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Permission, Role } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import { DATABASE_ID, TABLES, type CalendarEvent } from "./schema";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { CalendarActionState } from "./calendar-types";
-import { calendarEventSchema } from "@/lib/validation/calendar";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) return e.message || "Beklenmeyen hata.";
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function teamRowPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ];
-}
-
-function pickFormFields(formData: FormData) {
- return {
- title: String(formData.get("title") ?? "").trim(),
- description: String(formData.get("description") ?? "").trim(),
- start: String(formData.get("start") ?? ""),
- end: String(formData.get("end") ?? ""),
- allDay: formData.get("allDay") ?? false,
- customerId: String(formData.get("customerId") ?? ""),
- color: String(formData.get("color") ?? ""),
- };
-}
-
-function toIso(v: string, allDay: boolean | undefined): string {
- if (!v) return v;
- // datetime-local => "YYYY-MM-DDTHH:mm"; date => "YYYY-MM-DD"
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
- return `${v}T00:00:00.000+00:00`;
- }
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(v)) {
- return new Date(v).toISOString();
- }
- if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(v)) {
- return new Date(v).toISOString();
- }
- // fallback for already-iso
- return v;
-}
-
-export async function createCalendarEventAction(
- _prev: CalendarActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = calendarEventSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const data = {
- ...parsed.data,
- start: toIso(parsed.data.start, parsed.data.allDay),
- end: toIso(parsed.data.end, parsed.data.allDay),
- };
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.calendarEvents,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...data,
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "calendar_event",
- entityId: row.$id,
- changes: data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/calendar");
- return { ok: true };
-}
-
-export async function updateCalendarEventAction(
- _prev: CalendarActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = calendarEventSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.calendarEvents,
- id,
- )) as unknown as CalendarEvent;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const data = {
- ...parsed.data,
- start: toIso(parsed.data.start, parsed.data.allDay),
- end: toIso(parsed.data.end, parsed.data.allDay),
- };
- await tablesDB.updateRow(DATABASE_ID, TABLES.calendarEvents, id, data);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "calendar_event",
- entityId: id,
- changes: data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/calendar");
- return { ok: true };
-}
-
-export async function deleteCalendarEventAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.calendarEvents,
- id,
- )) as unknown as CalendarEvent;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.calendarEvents, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "calendar_event",
- entityId: id,
- changes: { title: existing.title },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/calendar");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/calendar-queries.ts b/src/lib/appwrite/calendar-queries.ts
deleted file mode 100644
index 2f17037..0000000
--- a/src/lib/appwrite/calendar-queries.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type CalendarEvent } from "./schema";
-
-export async function listCalendarEvents(
- tenantId: string,
- rangeStart?: string,
- rangeEnd?: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const queries = [Query.equal("tenantId", tenantId), Query.limit(1000)];
- if (rangeStart) queries.push(Query.greaterThanEqual("start", rangeStart));
- if (rangeEnd) queries.push(Query.lessThanEqual("start", rangeEnd));
- queries.push(Query.orderAsc("start"));
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.calendarEvents,
- queries,
- });
- return result.rows as unknown as CalendarEvent[];
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/calendar-types.ts b/src/lib/appwrite/calendar-types.ts
deleted file mode 100644
index 4ffe8a8..0000000
--- a/src/lib/appwrite/calendar-types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type CalendarActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
-};
-
-export const initialCalendarState: CalendarActionState = { ok: false };
diff --git a/src/lib/appwrite/credit-card-actions.ts b/src/lib/appwrite/credit-card-actions.ts
deleted file mode 100644
index 66a3ec5..0000000
--- a/src/lib/appwrite/credit-card-actions.ts
+++ /dev/null
@@ -1,504 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Query } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import {
- DATABASE_ID,
- TABLES,
- type CreditCard,
- type CreditCardStatement,
-} from "./schema";
-import { canAccessRow, scopedRowPermissions } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { CreditCardActionState } from "./credit-card-types";
-import { creditCardSchema, statementSchema } from "@/lib/validation/credit-cards";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) return e.message || "Beklenmeyen hata.";
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function toIso(v: string): string {
- if (!v) return v;
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return `${v}T00:00:00.000+00:00`;
- return v;
-}
-
-// ---------------- Cards ----------------
-
-function pickCardFields(formData: FormData) {
- return {
- bankName: String(formData.get("bankName") ?? "").trim(),
- cardName: String(formData.get("cardName") ?? "").trim(),
- last4: String(formData.get("last4") ?? "").trim(),
- creditLimit: String(formData.get("creditLimit") ?? "0"),
- statementDay: String(formData.get("statementDay") ?? "1"),
- dueDay: String(formData.get("dueDay") ?? "10"),
- interestRate: String(formData.get("interestRate") ?? "4.25"),
- bankAccountId: String(formData.get("bankAccountId") ?? ""),
- notes: String(formData.get("notes") ?? "").trim(),
- scope: (formData.get("scope") as "company" | "personal" | null) ?? "company",
- };
-}
-
-export async function createCreditCardAction(
- _prev: CreditCardActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = creditCardSchema.safeParse(pickCardFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.creditCards,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...parsed.data,
- },
- scopedRowPermissions(ctx.tenantId, ctx.user.id, parsed.data.scope),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "credit_card",
- entityId: row.$id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/cards");
- return { ok: true };
-}
-
-export async function updateCreditCardAction(
- _prev: CreditCardActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = creditCardSchema.safeParse(pickCardFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCards,
- id,
- )) as unknown as CreditCard;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.updateRow(
- DATABASE_ID,
- TABLES.creditCards,
- id,
- parsed.data,
- scopedRowPermissions(ctx.tenantId, existing.createdBy, parsed.data.scope),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "credit_card",
- entityId: id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/cards");
- return { ok: true };
-}
-
-export async function archiveCreditCardAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCards,
- id,
- )) as unknown as CreditCard;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
- const newArchivedState = !existing.archived;
- await tablesDB.updateRow(DATABASE_ID, TABLES.creditCards, id, {
- archived: newArchivedState,
- });
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "credit_card",
- entityId: id,
- changes: { archived: newArchivedState },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/cards");
- return { ok: true };
-}
-
-export async function deleteCreditCardAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCards,
- id,
- )) as unknown as CreditCard;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- // Cascade delete statements + their finance entries
- const statements = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.creditCardStatements,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("cardId", id),
- Query.limit(500),
- ],
- });
- for (const s of statements.rows as unknown as CreditCardStatement[]) {
- if (s.financeEntryId) {
- try {
- await tablesDB.deleteRow(DATABASE_ID, TABLES.financeEntries, s.financeEntryId);
- } catch {
- /* ignore */
- }
- }
- await tablesDB.deleteRow(DATABASE_ID, TABLES.creditCardStatements, s.$id);
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.creditCards, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "credit_card",
- entityId: id,
- changes: { bankName: existing.bankName, cardName: existing.cardName, statements: statements.rows.length },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/cards");
- return { ok: true };
-}
-
-// ---------------- Statements ----------------
-
-function pickStatementFields(formData: FormData) {
- return {
- cardId: String(formData.get("cardId") ?? ""),
- period: String(formData.get("period") ?? "").trim(),
- statementDate: String(formData.get("statementDate") ?? ""),
- dueDate: String(formData.get("dueDate") ?? ""),
- totalDebt: String(formData.get("totalDebt") ?? "0"),
- minimumPayment: String(formData.get("minimumPayment") ?? "0"),
- notes: String(formData.get("notes") ?? "").trim(),
- };
-}
-
-function computeStatus(
- totalDebt: number,
- paidAmount: number,
- dueDate: string,
-): "pending" | "partial" | "paid" | "overdue" {
- if (paidAmount >= totalDebt) return "paid";
- const past = new Date(dueDate).getTime() < Date.now();
- if (paidAmount > 0) return past ? "overdue" : "partial";
- return past ? "overdue" : "pending";
-}
-
-export async function createStatementAction(
- _prev: CreditCardActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = statementSchema.safeParse(pickStatementFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const card = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCards,
- parsed.data.cardId,
- )) as unknown as CreditCard;
- if (card.tenantId !== ctx.tenantId || !canAccessRow(card, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const status = computeStatus(parsed.data.totalDebt, 0, parsed.data.dueDate);
- // Statements inherit the card's scope.
- const cardScope = card.scope ?? "company";
-
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.creditCardStatements,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- cardId: parsed.data.cardId,
- period: parsed.data.period,
- statementDate: toIso(parsed.data.statementDate),
- dueDate: toIso(parsed.data.dueDate),
- totalDebt: parsed.data.totalDebt,
- minimumPayment: parsed.data.minimumPayment,
- paidAmount: 0,
- status,
- notes: parsed.data.notes,
- },
- scopedRowPermissions(ctx.tenantId, ctx.user.id, cardScope),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "credit_card_statement",
- entityId: row.$id,
- changes: { cardId: parsed.data.cardId, period: parsed.data.period, totalDebt: parsed.data.totalDebt },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/cards");
- return { ok: true };
-}
-
-export async function payStatementAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- const amountStr = String(formData.get("amount") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCardStatements,
- id,
- )) as unknown as CreditCardStatement;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const remaining = (existing.totalDebt ?? 0) - (existing.paidAmount ?? 0);
- if (remaining <= 0) {
- return { ok: false, error: "Bu ekstrenin bakiyesi kalmamış." };
- }
-
- const amount = amountStr
- ? Number(amountStr.replace(",", "."))
- : remaining;
- if (!Number.isFinite(amount) || amount <= 0) {
- return { ok: false, error: "Tutar geçersiz.", fieldErrors: { amount: "Geçersiz" } };
- }
- const payAmount = Math.min(amount, remaining);
-
- const card = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCards,
- existing.cardId,
- )) as unknown as CreditCard;
- if (!canAccessRow(card, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
- const cardScope = card.scope ?? "company";
-
- const fe = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.financeEntries,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- type: "expense",
- amount: payAmount,
- date: new Date().toISOString(),
- description: `${card.bankName} ${card.cardName} ${existing.period} ekstre ödemesi`,
- bankAccountId: card.bankAccountId,
- scope: cardScope,
- },
- scopedRowPermissions(ctx.tenantId, ctx.user.id, cardScope),
- );
-
- const newPaid = (existing.paidAmount ?? 0) + payAmount;
- const newStatus = computeStatus(existing.totalDebt ?? 0, newPaid, existing.dueDate);
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.creditCardStatements, id, {
- paidAmount: Number(newPaid.toFixed(2)),
- status: newStatus,
- financeEntryId: newStatus === "paid" ? fe.$id : existing.financeEntryId ?? fe.$id,
- });
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "credit_card_statement",
- entityId: id,
- changes: { paidAmount: newPaid, status: newStatus, payAmount },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/cards");
- revalidatePath("/finance");
- return { ok: true };
-}
-
-export async function deleteStatementAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCardStatements,
- id,
- )) as unknown as CreditCardStatement;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
- // Statement inherits its parent card's scope.
- const parent = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.creditCards,
- existing.cardId,
- )) as unknown as CreditCard;
- if (!canAccessRow(parent, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
- if (existing.financeEntryId) {
- try {
- await tablesDB.deleteRow(
- DATABASE_ID,
- TABLES.financeEntries,
- existing.financeEntryId,
- );
- } catch {
- /* ignore */
- }
- }
- await tablesDB.deleteRow(DATABASE_ID, TABLES.creditCardStatements, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "credit_card_statement",
- entityId: id,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/cards");
- revalidatePath("/finance");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/credit-card-queries.ts b/src/lib/appwrite/credit-card-queries.ts
deleted file mode 100644
index bdb98c9..0000000
--- a/src/lib/appwrite/credit-card-queries.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { canAccessRow } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import {
- DATABASE_ID,
- TABLES,
- type CreditCard,
- type CreditCardStatement,
-} from "./schema";
-
-export async function listCreditCards(
- tenantId: string,
- currentUserId?: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.creditCards,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderAsc("bankName"),
- Query.limit(200),
- ],
- });
- const rows = result.rows as unknown as CreditCard[];
- if (!currentUserId) return rows;
- return rows.filter((r) => canAccessRow(r, currentUserId));
- } catch {
- return [];
- }
-}
-
-/**
- * Lists statements whose parent card is visible to the user.
- */
-export async function listStatements(
- tenantId: string,
- currentUserId?: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const [allStmt, allCards] = await Promise.all([
- tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.creditCardStatements,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("statementDate"),
- Query.limit(500),
- ],
- }),
- tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.creditCards,
- queries: [Query.equal("tenantId", tenantId), Query.limit(200)],
- }),
- ]);
- if (!currentUserId) return allStmt.rows as unknown as CreditCardStatement[];
- const visibleCardIds = new Set(
- (allCards.rows as unknown as CreditCard[])
- .filter((c) => canAccessRow(c, currentUserId))
- .map((c) => c.$id),
- );
- return (allStmt.rows as unknown as CreditCardStatement[]).filter((s) =>
- visibleCardIds.has(s.cardId),
- );
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/credit-card-types.ts b/src/lib/appwrite/credit-card-types.ts
deleted file mode 100644
index 3e79492..0000000
--- a/src/lib/appwrite/credit-card-types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type CreditCardActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
-};
-
-export const initialCreditCardState: CreditCardActionState = { ok: false };
diff --git a/src/lib/appwrite/customer-actions.ts b/src/lib/appwrite/customer-actions.ts
deleted file mode 100644
index 8169bc4..0000000
--- a/src/lib/appwrite/customer-actions.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Permission, Role } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import {
- isPlanLimitError,
- planLimitMessage,
- requirePlanCapacity,
-} from "./plan-limits";
-import { DATABASE_ID, TABLES, type Customer } from "./schema";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { CustomerActionState } from "./customer-types";
-import { customerSchema } from "@/lib/validation/customers";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) {
- return e.message || "Beklenmeyen bir hata oluştu.";
- }
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function pickFormFields(formData: FormData) {
- return {
- name: String(formData.get("name") ?? "").trim(),
- email: String(formData.get("email") ?? "").trim(),
- phone: String(formData.get("phone") ?? "").trim(),
- taxId: String(formData.get("taxId") ?? "").trim(),
- address: String(formData.get("address") ?? "").trim(),
- notes: String(formData.get("notes") ?? "").trim(),
- status: (formData.get("status") as "active" | "passive" | null) ?? "active",
- };
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function teamRowPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ];
-}
-
-export async function createCustomerAction(
- _prev: CustomerActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = customerSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- await requirePlanCapacity(ctx, "customers");
- } catch (e) {
- if (isPlanLimitError(e)) {
- return {
- ok: false,
- error: planLimitMessage(e.resource, e.limit),
- code: "PLAN_LIMIT_EXCEEDED",
- };
- }
- throw e;
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.customers,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...parsed.data,
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "customer",
- entityId: row.$id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/customers");
- return { ok: true };
-}
-
-export async function updateCustomerAction(
- _prev: CustomerActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = customerSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.customers,
- id,
- )) as unknown as Customer;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.customers, id, parsed.data);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "customer",
- entityId: id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/customers");
- return { ok: true };
-}
-
-export async function deleteCustomerAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.customers,
- id,
- )) as unknown as Customer;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.customers, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "customer",
- entityId: id,
- changes: { name: existing.name },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/customers");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/customer-queries.ts b/src/lib/appwrite/customer-queries.ts
deleted file mode 100644
index 28e7f01..0000000
--- a/src/lib/appwrite/customer-queries.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type Customer } from "./schema";
-
-export async function listCustomers(tenantId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.customers,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("$createdAt"),
- Query.limit(500),
- ],
- });
- return result.rows as unknown as Customer[];
- } catch {
- return [];
- }
-}
-
-export async function getCustomer(
- tenantId: string,
- id: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const row = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.customers,
- id,
- )) as unknown as Customer;
- if (row.tenantId !== tenantId) return null;
- return row;
- } catch {
- return null;
- }
-}
diff --git a/src/lib/appwrite/customer-types.ts b/src/lib/appwrite/customer-types.ts
deleted file mode 100644
index bca98d7..0000000
--- a/src/lib/appwrite/customer-types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export type CustomerActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
- code?: "PLAN_LIMIT_EXCEEDED";
-};
-
-export const initialCustomerState: CustomerActionState = { ok: false };
diff --git a/src/lib/appwrite/dashboard-queries.ts b/src/lib/appwrite/dashboard-queries.ts
deleted file mode 100644
index 529e492..0000000
--- a/src/lib/appwrite/dashboard-queries.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import {
- DATABASE_ID,
- TABLES,
- type Customer,
- type FinanceEntry,
- type Invoice,
- type Task,
-} from "./schema";
-
-export type DashboardData = {
- metrics: {
- totalCustomers: number;
- activeCustomers: number;
- monthIncome: number; // current month income
- prevMonthIncome: number; // previous month income (for delta)
- outstanding: number; // unpaid invoices total (draft+sent+overdue)
- overdueCount: number;
- openTasks: number;
- urgentTasks: number;
- };
- monthlyIncome: { month: string; income: number; expense: number }[]; // last 12 months
- topCustomers: { name: string; total: number }[]; // top 5 by paid invoice total
- recentTransactions: {
- id: string;
- type: FinanceEntry["type"];
- amount: number;
- date: string;
- customerName: string;
- description: string;
- }[];
- topServices: { name: string; total: number; count: number }[]; // top 5 services by revenue (qty*unitPrice)
- newCustomersMonthly: { month: string; count: number }[]; // last 6 months
-};
-
-const MONTH_SHORT = ["Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"];
-
-function monthKey(d: Date): string {
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
-}
-
-function monthLabel(d: Date): string {
- return MONTH_SHORT[d.getMonth()];
-}
-
-export async function getDashboardData(
- tenantId: string,
- currentUserId?: string,
-): Promise {
- const { tablesDB } = createAdminClient();
-
- const [customers, invoices, financeEntries, tasks, services] = await Promise.all([
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.customers,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.invoices,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("date"),
- Query.limit(2000),
- ],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.tasks,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.services,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- ]);
-
- const customerList = customers.rows as unknown as Customer[];
- const invoiceList = invoices.rows as unknown as Invoice[];
- // Dashboard KPIs reflect company finances only — personal-scope rows belong
- // to a single user and shouldn't influence team-level metrics.
- const entryList = (financeEntries.rows as unknown as FinanceEntry[]).filter(
- (e) => (e.scope ?? "company") === "company",
- );
- const taskList = tasks.rows as unknown as Task[];
-
- const customerMap = new Map(customerList.map((c) => [c.$id, c.name]));
-
- // ---------------- Metrics ----------------
- const now = new Date();
- const thisMonth = monthKey(now);
- const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
- const prevMonth = monthKey(prev);
-
- let monthIncome = 0;
- let prevMonthIncome = 0;
- for (const e of entryList) {
- if (e.type !== "income") continue;
- const k = monthKey(new Date(e.date));
- if (k === thisMonth) monthIncome += e.amount;
- else if (k === prevMonth) prevMonthIncome += e.amount;
- }
-
- let outstanding = 0;
- let overdueCount = 0;
- const today = new Date();
- for (const inv of invoiceList) {
- const status = inv.status ?? "draft";
- if (status === "paid" || status === "cancelled") continue;
- outstanding += inv.total ?? 0;
- if (status === "overdue" || (inv.dueDate && new Date(inv.dueDate) < today)) {
- overdueCount += 1;
- }
- }
-
- let openTasks = 0;
- let urgentTasks = 0;
- for (const t of taskList) {
- if ((t.status ?? "todo") === "done") continue;
- // Personal scope: own assigned + unassigned. Falls back to all if no userId.
- if (currentUserId) {
- const assignee = t.assigneeId ?? "";
- if (assignee && assignee !== currentUserId) continue;
- }
- openTasks += 1;
- if ((t.priority ?? "medium") === "urgent") urgentTasks += 1;
- }
-
- const activeCustomers = customerList.filter((c) => (c.status ?? "active") === "active").length;
-
- // ---------------- Monthly income/expense (last 12 months) ----------------
- const monthSeries: DashboardData["monthlyIncome"] = [];
- for (let i = 11; i >= 0; i--) {
- const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
- monthSeries.push({ month: monthLabel(d), income: 0, expense: 0 });
- }
- for (const e of entryList) {
- const ed = new Date(e.date);
- const monthsAgo =
- (now.getFullYear() - ed.getFullYear()) * 12 + (now.getMonth() - ed.getMonth());
- if (monthsAgo < 0 || monthsAgo > 11) continue;
- const idx = 11 - monthsAgo;
- if (e.type === "income") monthSeries[idx].income += e.amount;
- else if (e.type === "expense") monthSeries[idx].expense += e.amount;
- }
-
- // ---------------- Top customers (by paid invoice total) ----------------
- const customerRevenue = new Map();
- for (const inv of invoiceList) {
- if (inv.status !== "paid") continue;
- customerRevenue.set(
- inv.customerId,
- (customerRevenue.get(inv.customerId) ?? 0) + (inv.total ?? 0),
- );
- }
- const topCustomers = Array.from(customerRevenue.entries())
- .map(([id, total]) => ({ name: customerMap.get(id) ?? "—", total }))
- .sort((a, b) => b.total - a.total)
- .slice(0, 5);
-
- // ---------------- Recent transactions (last 8) ----------------
- const recentTransactions = entryList.slice(0, 8).map((e) => ({
- id: e.$id,
- type: e.type,
- amount: e.amount,
- date: e.date,
- customerName: e.customerId ? customerMap.get(e.customerId) ?? "" : "",
- description: e.description ?? "",
- }));
-
- // ---------------- Top services (by current MRR estimate) ----------------
- const svcMap = new Map();
- for (const s of services.rows as unknown as { name: string; unitPrice?: number }[]) {
- const key = s.name;
- const entry = svcMap.get(key) ?? { name: s.name, total: 0, count: 0 };
- entry.total += s.unitPrice ?? 0;
- entry.count += 1;
- svcMap.set(key, entry);
- }
- const topServices = Array.from(svcMap.values())
- .sort((a, b) => b.total - a.total)
- .slice(0, 5);
-
- // ---------------- New customers per month (last 6) ----------------
- const newCustomersMonthly: DashboardData["newCustomersMonthly"] = [];
- for (let i = 5; i >= 0; i--) {
- const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
- newCustomersMonthly.push({ month: monthLabel(d), count: 0 });
- }
- for (const c of customerList) {
- const cd = new Date(c.$createdAt);
- const monthsAgo =
- (now.getFullYear() - cd.getFullYear()) * 12 + (now.getMonth() - cd.getMonth());
- if (monthsAgo < 0 || monthsAgo > 5) continue;
- const idx = 5 - monthsAgo;
- newCustomersMonthly[idx].count += 1;
- }
-
- return {
- metrics: {
- totalCustomers: customerList.length,
- activeCustomers,
- monthIncome,
- prevMonthIncome,
- outstanding,
- overdueCount,
- openTasks,
- urgentTasks,
- },
- monthlyIncome: monthSeries,
- topCustomers,
- recentTransactions,
- topServices,
- newCustomersMonthly,
- };
-}
diff --git a/src/lib/appwrite/finance-actions.ts b/src/lib/appwrite/finance-actions.ts
deleted file mode 100644
index c2fbe3b..0000000
--- a/src/lib/appwrite/finance-actions.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import {
- isPlanLimitError,
- planLimitMessage,
- requirePlanCapacity,
-} from "./plan-limits";
-import { DATABASE_ID, TABLES, type FinanceEntry } from "./schema";
-import { canAccessRow, scopedRowPermissions } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { FinanceActionState } from "./finance-types";
-import { financeEntrySchema } from "@/lib/validation/finance";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) return e.message || "Beklenmeyen hata.";
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function pickFormFields(formData: FormData) {
- return {
- type: formData.get("type") as "income" | "expense" | "debt" | "receivable",
- amount: String(formData.get("amount") ?? "0"),
- date: String(formData.get("date") ?? ""),
- description: String(formData.get("description") ?? "").trim(),
- customerId: String(formData.get("customerId") ?? ""),
- invoiceId: String(formData.get("invoiceId") ?? ""),
- paymentMethod: formData.get("paymentMethod") as
- | "cash"
- | "transfer"
- | "card"
- | "check"
- | "other"
- | null,
- bankAccountId: String(formData.get("bankAccountId") ?? ""),
- scope: (formData.get("scope") as "company" | "personal" | null) ?? "company",
- };
-}
-
-function toIso(v: string): string {
- if (!v) return v;
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return `${v}T00:00:00.000+00:00`;
- return v;
-}
-
-export async function createFinanceEntryAction(
- _prev: FinanceActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = financeEntrySchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- await requirePlanCapacity(ctx, "financeEntries");
- } catch (e) {
- if (isPlanLimitError(e)) {
- return {
- ok: false,
- error: planLimitMessage(e.resource, e.limit),
- code: "PLAN_LIMIT_EXCEEDED",
- };
- }
- throw e;
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const data = { ...parsed.data, date: toIso(parsed.data.date) };
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.financeEntries,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...data,
- },
- scopedRowPermissions(ctx.tenantId, ctx.user.id, parsed.data.scope),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "finance_entry",
- entityId: row.$id,
- changes: data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance");
- return { ok: true };
-}
-
-export async function updateFinanceEntryAction(
- _prev: FinanceActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = financeEntrySchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.financeEntries,
- id,
- )) as unknown as FinanceEntry;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const data = { ...parsed.data, date: toIso(parsed.data.date) };
- await tablesDB.updateRow(
- DATABASE_ID,
- TABLES.financeEntries,
- id,
- data,
- scopedRowPermissions(ctx.tenantId, existing.createdBy, parsed.data.scope),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "finance_entry",
- entityId: id,
- changes: data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance");
- return { ok: true };
-}
-
-export async function deleteFinanceEntryAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.financeEntries,
- id,
- )) as unknown as FinanceEntry;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.financeEntries, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "finance_entry",
- entityId: id,
- changes: { type: existing.type, amount: existing.amount },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/finance-queries.ts b/src/lib/appwrite/finance-queries.ts
deleted file mode 100644
index 7b88595..0000000
--- a/src/lib/appwrite/finance-queries.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { canAccessRow } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type FinanceEntry } from "./schema";
-
-export async function listFinanceEntries(
- tenantId: string,
- currentUserId?: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("date"),
- Query.limit(1000),
- ],
- });
- const rows = result.rows as unknown as FinanceEntry[];
- if (!currentUserId) return rows;
- return rows.filter((r) => canAccessRow(r, currentUserId));
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/finance-report-queries.ts b/src/lib/appwrite/finance-report-queries.ts
deleted file mode 100644
index 2ff3a86..0000000
--- a/src/lib/appwrite/finance-report-queries.ts
+++ /dev/null
@@ -1,401 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import {
- DATABASE_ID,
- TABLES,
- type BankAccount,
- type BankLoan,
- type CreditCard,
- type CreditCardStatement,
- type Customer,
- type FinanceEntry,
- type Invoice,
- type LoanInstallment,
-} from "./schema";
-
-export type ReportPeriod = "month" | "quarter" | "year" | "all";
-
-export type FinancialReport = {
- period: ReportPeriod;
- periodStart: string | null;
- periodEnd: string | null;
- // KPIs
- kpi: {
- income: number;
- expense: number;
- net: number;
- cashPosition: number; // bank balances + receivables - loan remaining - card outstanding
- };
- // Cash composition (right now, not period-bound)
- composition: {
- bankBalances: number;
- receivables: number; // unpaid invoices total (not paid, not cancelled)
- loanRemaining: number;
- cardOutstanding: number;
- };
- // Trend (last 12 months income/expense)
- trend: { month: string; income: number; expense: number; net: number }[];
- // Top customers by paid invoice total (period-bound when period != all)
- topCustomers: { name: string; total: number }[];
- // Top expense buckets — auto-grouped by source
- expenseBreakdown: {
- invoices: number; // expenses linked to invoices? we don't track AP invoices yet — leave 0
- loans: number; // finance_entries linked through loan installments (description match heuristic)
- cards: number;
- other: number;
- };
- // Active loans
- loans: {
- id: string;
- bankName: string;
- loanName: string;
- principal: number;
- remaining: number;
- monthlyPayment: number;
- nextDue: string | null;
- }[];
- // Credit card outstanding statements
- cardStatements: {
- id: string;
- cardLabel: string;
- period: string;
- dueDate: string;
- remaining: number;
- status: "pending" | "partial" | "overdue";
- }[];
- // Outstanding (unpaid) invoices
- outstandingInvoices: {
- id: string;
- number: string;
- customerName: string;
- dueDate: string;
- total: number;
- overdue: boolean;
- }[];
-};
-
-const MONTH_SHORT = ["Oca", "Şub", "Mar", "Nis", "May", "Haz", "Tem", "Ağu", "Eyl", "Eki", "Kas", "Ara"];
-
-function periodBounds(p: ReportPeriod): { start: Date | null; end: Date | null } {
- const now = new Date();
- if (p === "month") {
- return {
- start: new Date(now.getFullYear(), now.getMonth(), 1),
- end: new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59),
- };
- }
- if (p === "quarter") {
- const q = Math.floor(now.getMonth() / 3);
- return {
- start: new Date(now.getFullYear(), q * 3, 1),
- end: new Date(now.getFullYear(), q * 3 + 3, 0, 23, 59, 59),
- };
- }
- if (p === "year") {
- return {
- start: new Date(now.getFullYear(), 0, 1),
- end: new Date(now.getFullYear(), 11, 31, 23, 59, 59),
- };
- }
- return { start: null, end: null };
-}
-
-function inRange(iso: string, start: Date | null, end: Date | null): boolean {
- if (!start || !end) return true;
- const t = new Date(iso).getTime();
- return t >= start.getTime() && t <= end.getTime();
-}
-
-export async function getFinancialReport(
- tenantId: string,
- period: ReportPeriod = "month",
-): Promise {
- const { tablesDB } = createAdminClient();
-
- const [
- customers,
- invoices,
- finance,
- bankAccounts,
- loans,
- installments,
- cards,
- statements,
- ] = await Promise.all([
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.customers,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.invoices,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [Query.equal("tenantId", tenantId), Query.limit(5000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.bankAccounts,
- queries: [Query.equal("tenantId", tenantId), Query.limit(200)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.bankLoans,
- queries: [Query.equal("tenantId", tenantId), Query.limit(200)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.loanInstallments,
- queries: [Query.equal("tenantId", tenantId), Query.limit(5000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.creditCards,
- queries: [Query.equal("tenantId", tenantId), Query.limit(200)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.creditCardStatements,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- })
- .catch(() => ({ rows: [] as unknown[] })),
- ]);
-
- // Reports reflect COMPANY finances only — personal-scope entities are
- // private to their creator and must not flow into team-level metrics.
- const isCompany = (r: T) =>
- (r.scope ?? "company") === "company";
-
- const customerList = customers.rows as unknown as Customer[];
- const invoiceList = invoices.rows as unknown as Invoice[];
- const entryList = (finance.rows as unknown as FinanceEntry[]).filter(isCompany);
- const bankList = (bankAccounts.rows as unknown as BankAccount[]).filter(isCompany);
- const loanList = (loans.rows as unknown as BankLoan[]).filter(isCompany);
- const cardList = (cards.rows as unknown as CreditCard[]).filter(isCompany);
- const visibleLoanIds = new Set(loanList.map((l) => l.$id));
- const visibleCardIds = new Set(cardList.map((c) => c.$id));
- const installmentList = (installments.rows as unknown as LoanInstallment[]).filter(
- (i) => visibleLoanIds.has(i.loanId),
- );
- const statementList = (statements.rows as unknown as CreditCardStatement[]).filter(
- (s) => visibleCardIds.has(s.cardId),
- );
-
- const customerMap = new Map(customerList.map((c) => [c.$id, c.name]));
- const cardMap = new Map(
- cardList.map((c) => [c.$id, `${c.bankName} — ${c.cardName}${c.last4 ? ` **${c.last4}` : ""}`]),
- );
-
- const { start, end } = periodBounds(period);
-
- // ---------- KPIs (period-bound for income/expense, current for cash) ----------
- let income = 0;
- let expense = 0;
- for (const e of entryList) {
- if (!inRange(e.date, start, end)) continue;
- if (e.type === "income") income += e.amount;
- else if (e.type === "expense") expense += e.amount;
- }
-
- // bank balance (today, not period-bound)
- const balances = new Map();
- for (const a of bankList) balances.set(a.$id, a.openingBalance ?? 0);
- for (const e of entryList) {
- if (!e.bankAccountId) continue;
- const cur = balances.get(e.bankAccountId);
- if (cur === undefined) continue;
- if (e.type === "income") balances.set(e.bankAccountId, cur + e.amount);
- else if (e.type === "expense") balances.set(e.bankAccountId, cur - e.amount);
- }
- const bankBalances = Array.from(balances.values()).reduce((s, n) => s + n, 0);
-
- // receivables = sum of unpaid invoices
- const receivables = invoiceList.reduce((s, inv) => {
- const st = inv.status ?? "draft";
- if (st === "paid" || st === "cancelled") return s;
- return s + (inv.total ?? 0);
- }, 0);
-
- // loan remaining = sum of unpaid installments
- const loanRemaining = installmentList.reduce(
- (s, i) => (i.paid ? s : s + (i.amount ?? 0)),
- 0,
- );
-
- // card outstanding = sum of (totalDebt - paidAmount) for non-paid statements
- const cardOutstanding = statementList.reduce(
- (s, st) =>
- st.status === "paid" ? s : s + ((st.totalDebt ?? 0) - (st.paidAmount ?? 0)),
- 0,
- );
-
- const cashPosition = bankBalances + receivables - loanRemaining - cardOutstanding;
-
- // ---------- Trend (last 12 months always) ----------
- const trend: FinancialReport["trend"] = [];
- const now = new Date();
- for (let i = 11; i >= 0; i--) {
- const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
- trend.push({
- month: MONTH_SHORT[d.getMonth()],
- income: 0,
- expense: 0,
- net: 0,
- });
- }
- for (const e of entryList) {
- const ed = new Date(e.date);
- const monthsAgo =
- (now.getFullYear() - ed.getFullYear()) * 12 + (now.getMonth() - ed.getMonth());
- if (monthsAgo < 0 || monthsAgo > 11) continue;
- const idx = 11 - monthsAgo;
- if (e.type === "income") trend[idx].income += e.amount;
- else if (e.type === "expense") trend[idx].expense += e.amount;
- }
- for (const t of trend) t.net = t.income - t.expense;
-
- // ---------- Top customers (period-bound paid invoices) ----------
- const customerRevenue = new Map();
- for (const inv of invoiceList) {
- if (inv.status !== "paid") continue;
- if (start && !inRange(inv.issueDate, start, end)) continue;
- customerRevenue.set(
- inv.customerId,
- (customerRevenue.get(inv.customerId) ?? 0) + (inv.total ?? 0),
- );
- }
- const topCustomers = Array.from(customerRevenue.entries())
- .map(([id, total]) => ({ name: customerMap.get(id) ?? "—", total }))
- .sort((a, b) => b.total - a.total)
- .slice(0, 8);
-
- // ---------- Expense breakdown (period-bound) ----------
- let expLoans = 0;
- let expCards = 0;
- let expOther = 0;
- const installmentEntryIds = new Set(
- installmentList.map((i) => i.financeEntryId).filter(Boolean) as string[],
- );
- const statementEntryIds = new Set(
- statementList.map((s) => s.financeEntryId).filter(Boolean) as string[],
- );
- for (const e of entryList) {
- if (e.type !== "expense") continue;
- if (!inRange(e.date, start, end)) continue;
- if (installmentEntryIds.has(e.$id)) expLoans += e.amount;
- else if (statementEntryIds.has(e.$id)) expCards += e.amount;
- else expOther += e.amount;
- }
-
- // ---------- Active loans summary ----------
- const installmentsByLoan = new Map();
- for (const i of installmentList) {
- const arr = installmentsByLoan.get(i.loanId) ?? [];
- arr.push(i);
- installmentsByLoan.set(i.loanId, arr);
- }
- const loansSummary = loanList
- .filter((l) => (l.status ?? "active") === "active")
- .map((l) => {
- const items = installmentsByLoan.get(l.$id) ?? [];
- const remaining = items.filter((i) => !i.paid).reduce((s, i) => s + i.amount, 0);
- const unpaid = items.filter((i) => !i.paid).sort(
- (a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(),
- );
- return {
- id: l.$id,
- bankName: l.bankName,
- loanName: l.loanName,
- principal: l.principal,
- remaining,
- monthlyPayment: l.monthlyPayment ?? 0,
- nextDue: unpaid[0]?.dueDate ?? null,
- };
- })
- .sort((a, b) => b.remaining - a.remaining);
-
- // ---------- Credit card outstanding statements ----------
- const cardStmts = statementList
- .filter((s) => s.status !== "paid")
- .map((s) => ({
- id: s.$id,
- cardLabel: cardMap.get(s.cardId) ?? "—",
- period: s.period,
- dueDate: s.dueDate,
- remaining: (s.totalDebt ?? 0) - (s.paidAmount ?? 0),
- status: (s.status ?? "pending") as "pending" | "partial" | "overdue",
- }))
- .sort((a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime());
-
- // ---------- Outstanding invoices ----------
- const today = new Date();
- const outstandingInvoices = invoiceList
- .filter((inv) => {
- const st = inv.status ?? "draft";
- return st !== "paid" && st !== "cancelled";
- })
- .map((inv) => ({
- id: inv.$id,
- number: inv.number,
- customerName: customerMap.get(inv.customerId) ?? "—",
- dueDate: inv.dueDate,
- total: inv.total ?? 0,
- overdue: new Date(inv.dueDate) < today,
- }))
- .sort((a, b) => {
- if (a.overdue !== b.overdue) return a.overdue ? -1 : 1;
- return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
- })
- .slice(0, 12);
-
- return {
- period,
- periodStart: start ? start.toISOString() : null,
- periodEnd: end ? end.toISOString() : null,
- kpi: {
- income,
- expense,
- net: income - expense,
- cashPosition,
- },
- composition: {
- bankBalances,
- receivables,
- loanRemaining,
- cardOutstanding,
- },
- trend,
- topCustomers,
- expenseBreakdown: {
- invoices: 0,
- loans: expLoans,
- cards: expCards,
- other: expOther,
- },
- loans: loansSummary.slice(0, 8),
- cardStatements: cardStmts.slice(0, 12),
- outstandingInvoices,
- };
-}
diff --git a/src/lib/appwrite/finance-types.ts b/src/lib/appwrite/finance-types.ts
deleted file mode 100644
index 5a63312..0000000
--- a/src/lib/appwrite/finance-types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export type FinanceActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
- code?: "PLAN_LIMIT_EXCEEDED";
-};
-
-export const initialFinanceState: FinanceActionState = { ok: false };
diff --git a/src/lib/appwrite/invoice-actions.ts b/src/lib/appwrite/invoice-actions.ts
deleted file mode 100644
index 7e71c11..0000000
--- a/src/lib/appwrite/invoice-actions.ts
+++ /dev/null
@@ -1,620 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import {
- DATABASE_ID,
- TABLES,
- type Invoice,
- type InvoiceItem,
- type TenantSettings,
-} from "./schema";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { InvoiceActionState } from "./invoice-types";
-import { invoiceItemSchema, invoiceSchema } from "@/lib/validation/invoices";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) return e.message || "Beklenmeyen hata.";
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function teamRowPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ];
-}
-
-function toIso(v: string): string {
- if (!v) return v;
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return `${v}T00:00:00.000+00:00`;
- return v;
-}
-
-async function nextInvoiceNumber(
- tenantId: string,
-): Promise<{ number: string; settingsId: string | null }> {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.tenantSettings,
- queries: [Query.equal("tenantId", tenantId), Query.limit(1)],
- });
- const settings = result.rows[0] as unknown as TenantSettings | undefined;
- const prefix = settings?.invoicePrefix || "INV";
- const counter = (settings?.invoiceCounter ?? 0) + 1;
- const year = new Date().getFullYear();
- const number = `${prefix}-${year}-${String(counter).padStart(4, "0")}`;
-
- if (settings) {
- await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, settings.$id, {
- invoiceCounter: counter,
- });
- return { number, settingsId: settings.$id };
- }
- return { number, settingsId: null };
-}
-
-/**
- * Reflect invoice payment status into finance_entries.
- * - status === "paid": ensure exactly one income entry exists for this invoice.
- * - otherwise: remove any income entries linked to this invoice (auto-generated).
- *
- * Best-effort. Failures here must not break the invoice mutation; we log the
- * error to audit but do not throw.
- */
-async function syncPaymentEntry(
- tenantId: string,
- userId: string,
- invoice: Invoice,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const linked = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("invoiceId", invoice.$id),
- Query.equal("type", "income"),
- Query.limit(10),
- ],
- });
-
- if (invoice.status === "paid") {
- if (linked.rows.length === 0) {
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.financeEntries,
- ID.unique(),
- {
- tenantId,
- createdBy: userId,
- type: "income",
- amount: Number((invoice.total ?? 0).toFixed(2)),
- date: new Date().toISOString(),
- description: `Fatura ${invoice.number} tahsilatı`,
- customerId: invoice.customerId,
- invoiceId: invoice.$id,
- scope: "company",
- },
- [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ],
- );
- await logAudit({
- tenantId,
- userId,
- action: "create",
- entityType: "finance_entry",
- entityId: row.$id,
- changes: { auto: "invoice_paid", invoiceId: invoice.$id, amount: invoice.total },
- });
- } else {
- // Keep the first; resync amount in case the invoice total changed
- const first = linked.rows[0];
- const desiredAmount = Number((invoice.total ?? 0).toFixed(2));
- if (
- (first as unknown as { amount: number }).amount !== desiredAmount
- ) {
- await tablesDB.updateRow(DATABASE_ID, TABLES.financeEntries, first.$id, {
- amount: desiredAmount,
- });
- }
- }
- } else {
- for (const r of linked.rows) {
- await tablesDB.deleteRow(DATABASE_ID, TABLES.financeEntries, r.$id);
- await logAudit({
- tenantId,
- userId,
- action: "delete",
- entityType: "finance_entry",
- entityId: r.$id,
- changes: { auto: "invoice_unpaid", invoiceId: invoice.$id },
- });
- }
- }
- } catch {
- // best-effort; don't block the invoice update
- }
-}
-
-async function recomputeTotals(
- tenantId: string,
- invoiceId: string,
- userId?: string,
-): Promise<{ subtotal: number; vatTotal: number; total: number }> {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.invoiceItems,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("invoiceId", invoiceId),
- Query.limit(500),
- ],
- });
- let subtotal = 0;
- let vatTotal = 0;
- for (const r of result.rows as unknown as InvoiceItem[]) {
- const lineNet = (r.quantity ?? 0) * (r.unitPrice ?? 0);
- const lineVat = lineNet * ((r.vatRate ?? 0) / 100);
- subtotal += lineNet;
- vatTotal += lineVat;
- }
- const total = subtotal + vatTotal;
- await tablesDB.updateRow(DATABASE_ID, TABLES.invoices, invoiceId, {
- subtotal: Number(subtotal.toFixed(2)),
- vatTotal: Number(vatTotal.toFixed(2)),
- total: Number(total.toFixed(2)),
- });
-
- // If invoice is paid, keep the linked income entry's amount in sync.
- if (userId) {
- try {
- const refreshed = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoices,
- invoiceId,
- )) as unknown as Invoice;
- if (refreshed.status === "paid") {
- await syncPaymentEntry(tenantId, userId, refreshed);
- }
- } catch {
- /* best-effort */
- }
- }
-
- return { subtotal, vatTotal, total };
-}
-
-// -------------------- Invoice header --------------------
-
-function pickInvoiceFields(formData: FormData) {
- return {
- customerId: String(formData.get("customerId") ?? ""),
- issueDate: String(formData.get("issueDate") ?? ""),
- dueDate: String(formData.get("dueDate") ?? ""),
- status:
- (formData.get("status") as "draft" | "sent" | "paid" | "overdue" | "cancelled" | null) ??
- "draft",
- notes: String(formData.get("notes") ?? "").trim(),
- };
-}
-
-export async function createInvoiceAction(
- _prev: InvoiceActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = invoiceSchema.safeParse(pickInvoiceFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- let invoiceId = "";
- try {
- const { tablesDB } = createAdminClient();
- const { number } = await nextInvoiceNumber(ctx.tenantId);
-
- const data = {
- ...parsed.data,
- issueDate: toIso(parsed.data.issueDate),
- dueDate: toIso(parsed.data.dueDate),
- number,
- subtotal: 0,
- vatTotal: 0,
- total: 0,
- };
-
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.invoices,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...data,
- },
- teamRowPermissions(ctx.tenantId),
- );
- invoiceId = row.$id;
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "invoice",
- entityId: row.$id,
- changes: { number, customerId: parsed.data.customerId },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/invoices");
- return { ok: true, invoiceId };
-}
-
-export async function updateInvoiceAction(
- _prev: InvoiceActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = invoiceSchema.safeParse(pickInvoiceFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoices,
- id,
- )) as unknown as Invoice;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const data = {
- ...parsed.data,
- issueDate: toIso(parsed.data.issueDate),
- dueDate: toIso(parsed.data.dueDate),
- };
- await tablesDB.updateRow(DATABASE_ID, TABLES.invoices, id, data);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "invoice",
- entityId: id,
- changes: data,
- });
-
- // Sync payment ↔ finance entry on every save (cheap; idempotent).
- const refreshed = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoices,
- id,
- )) as unknown as Invoice;
- await syncPaymentEntry(ctx.tenantId, ctx.user.id, refreshed);
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/invoices");
- revalidatePath(`/invoices/${id}`);
- revalidatePath("/finance");
- return { ok: true, invoiceId: id };
-}
-
-export async function deleteInvoiceAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoices,
- id,
- )) as unknown as Invoice;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- // Delete items first
- const items = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.invoiceItems,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("invoiceId", id),
- Query.limit(500),
- ],
- });
- for (const it of items.rows) {
- await tablesDB.deleteRow(DATABASE_ID, TABLES.invoiceItems, it.$id);
- }
-
- // Cascade-delete any auto-generated income entries linked to this invoice
- const linkedEntries = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.financeEntries,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("invoiceId", id),
- Query.limit(50),
- ],
- });
- for (const r of linkedEntries.rows) {
- await tablesDB.deleteRow(DATABASE_ID, TABLES.financeEntries, r.$id);
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.invoices, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "invoice",
- entityId: id,
- changes: {
- number: existing.number,
- items: items.rows.length,
- linkedFinanceEntries: linkedEntries.rows.length,
- },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/invoices");
- revalidatePath("/finance");
- return { ok: true };
-}
-
-// -------------------- Invoice items --------------------
-
-function pickItemFields(formData: FormData) {
- return {
- description: String(formData.get("description") ?? "").trim(),
- quantity: String(formData.get("quantity") ?? "1"),
- unitPrice: String(formData.get("unitPrice") ?? "0"),
- vatRate: String(formData.get("vatRate") ?? "20"),
- };
-}
-
-function lineTotal(qty: number, unitPrice: number, vatRate: number): number {
- const net = qty * unitPrice;
- const vat = net * (vatRate / 100);
- return Number((net + vat).toFixed(2));
-}
-
-export async function addInvoiceItemAction(
- _prev: InvoiceActionState,
- formData: FormData,
-): Promise {
- const invoiceId = String(formData.get("invoiceId") ?? "");
- if (!invoiceId) return { ok: false, error: "Fatura ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = invoiceItemSchema.safeParse(pickItemFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Kalem geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const invoice = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoices,
- invoiceId,
- )) as unknown as Invoice;
- if (invoice.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const total = lineTotal(parsed.data.quantity, parsed.data.unitPrice, parsed.data.vatRate ?? 0);
-
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.invoiceItems,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- invoiceId,
- description: parsed.data.description,
- quantity: parsed.data.quantity,
- unitPrice: parsed.data.unitPrice,
- vatRate: parsed.data.vatRate ?? 0,
- lineTotal: total,
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await recomputeTotals(ctx.tenantId, invoiceId, ctx.user.id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "invoice_item",
- entityId: row.$id,
- changes: { invoiceId, ...parsed.data, lineTotal: total },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath(`/invoices/${invoiceId}`);
- return { ok: true, invoiceId };
-}
-
-export async function updateInvoiceItemAction(
- _prev: InvoiceActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = invoiceItemSchema.safeParse(pickItemFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Kalem geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoiceItems,
- id,
- )) as unknown as InvoiceItem;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const total = lineTotal(parsed.data.quantity, parsed.data.unitPrice, parsed.data.vatRate ?? 0);
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.invoiceItems, id, {
- description: parsed.data.description,
- quantity: parsed.data.quantity,
- unitPrice: parsed.data.unitPrice,
- vatRate: parsed.data.vatRate ?? 0,
- lineTotal: total,
- });
-
- await recomputeTotals(ctx.tenantId, existing.invoiceId, ctx.user.id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "invoice_item",
- entityId: id,
- changes: { ...parsed.data, lineTotal: total },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath(`/invoices/${(await getItemInvoiceId(id)) ?? ""}`);
- return { ok: true };
-}
-
-async function getItemInvoiceId(itemId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const row = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoiceItems,
- itemId,
- )) as unknown as InvoiceItem;
- return row.invoiceId;
- } catch {
- return null;
- }
-}
-
-export async function deleteInvoiceItemAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoiceItems,
- id,
- )) as unknown as InvoiceItem;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.invoiceItems, id);
- await recomputeTotals(ctx.tenantId, existing.invoiceId, ctx.user.id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "invoice_item",
- entityId: id,
- changes: { invoiceId: existing.invoiceId },
- });
-
- revalidatePath(`/invoices/${existing.invoiceId}`);
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- return { ok: true };
-}
diff --git a/src/lib/appwrite/invoice-queries.ts b/src/lib/appwrite/invoice-queries.ts
deleted file mode 100644
index c7d90e7..0000000
--- a/src/lib/appwrite/invoice-queries.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type Invoice, type InvoiceItem } from "./schema";
-
-export async function listInvoices(tenantId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.invoices,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("issueDate"),
- Query.limit(500),
- ],
- });
- return result.rows as unknown as Invoice[];
- } catch {
- return [];
- }
-}
-
-export async function getInvoice(
- tenantId: string,
- id: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const row = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.invoices,
- id,
- )) as unknown as Invoice;
- if (row.tenantId !== tenantId) return null;
- return row;
- } catch {
- return null;
- }
-}
-
-export async function listInvoiceItems(
- tenantId: string,
- invoiceId: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.invoiceItems,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("invoiceId", invoiceId),
- Query.orderAsc("$createdAt"),
- Query.limit(500),
- ],
- });
- return result.rows as unknown as InvoiceItem[];
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/invoice-types.ts b/src/lib/appwrite/invoice-types.ts
deleted file mode 100644
index 5a269db..0000000
--- a/src/lib/appwrite/invoice-types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export type InvoiceActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
- invoiceId?: string;
-};
-
-export const initialInvoiceState: InvoiceActionState = { ok: false };
diff --git a/src/lib/appwrite/lead-actions.ts b/src/lib/appwrite/lead-actions.ts
deleted file mode 100644
index 5b708f6..0000000
--- a/src/lib/appwrite/lead-actions.ts
+++ /dev/null
@@ -1,291 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Permission, Role } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import type { LeadActionState } from "./lead-types";
-import { DATABASE_ID, TABLES, type Lead, type LeadStatus } from "./schema";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import { leadSchema } from "@/lib/validation/leads";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) return e.message || "Beklenmeyen bir hata oluştu.";
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function teamRowPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ];
-}
-
-function pickFormFields(formData: FormData) {
- return {
- name: String(formData.get("name") ?? "").trim(),
- contactName: String(formData.get("contactName") ?? "").trim(),
- email: String(formData.get("email") ?? "").trim(),
- phone: String(formData.get("phone") ?? "").trim(),
- source: String(formData.get("source") ?? "other"),
- status: String(formData.get("status") ?? "cold"),
- estimatedValue: String(formData.get("estimatedValue") ?? ""),
- currency: String(formData.get("currency") ?? "TRY"),
- notes: String(formData.get("notes") ?? "").trim(),
- assigneeId: String(formData.get("assigneeId") ?? ""),
- };
-}
-
-export async function createLeadAction(
- _prev: LeadActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = leadSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.leads,
- ID.unique(),
- { tenantId: ctx.tenantId, createdBy: ctx.user.id, ...parsed.data },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "lead",
- entityId: row.$id,
- changes: { name: parsed.data.name, status: parsed.data.status },
- });
-
- revalidatePath("/leads");
- return { ok: true, leadId: row.$id };
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-}
-
-export async function updateLeadAction(
- _prev: LeadActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = leadSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(DATABASE_ID, TABLES.leads, id)) as unknown as Lead;
- if (existing.tenantId !== ctx.tenantId) return { ok: false, error: "Erişim engellendi." };
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.leads, id, parsed.data);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "lead",
- entityId: id,
- changes: parsed.data,
- });
-
- revalidatePath("/leads");
- return { ok: true, leadId: id };
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-}
-
-export async function moveLeadAction(
- leadId: string,
- status: LeadStatus,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(DATABASE_ID, TABLES.leads, leadId)) as unknown as Lead;
- if (existing.tenantId !== ctx.tenantId) return { ok: false, error: "Erişim engellendi." };
-
- const prevStatus = existing.status;
- await tablesDB.updateRow(DATABASE_ID, TABLES.leads, leadId, {
- status,
- lastContactAt: new Date().toISOString(),
- });
-
- // Auto-create a status_change activity
- await tablesDB.createRow(
- DATABASE_ID,
- TABLES.leadActivities,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- leadId,
- type: "status_change",
- content: `${prevStatus ?? "cold"} → ${status}`,
- occurredAt: new Date().toISOString(),
- },
- [
- Permission.read(Role.team(ctx.tenantId)),
- Permission.update(Role.team(ctx.tenantId)),
- Permission.delete(Role.team(ctx.tenantId, "owner")),
- ],
- );
-
- revalidatePath("/leads");
- return { ok: true };
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-}
-
-export async function deleteLeadAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(DATABASE_ID, TABLES.leads, id)) as unknown as Lead;
- if (existing.tenantId !== ctx.tenantId) return { ok: false, error: "Erişim engellendi." };
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.leads, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "lead",
- entityId: id,
- changes: { name: existing.name },
- });
-
- revalidatePath("/leads");
- return { ok: true };
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-}
-
-export async function convertLeadToCustomerAction(formData: FormData): Promise {
- const leadId = String(formData.get("leadId") ?? "");
- if (!leadId) return { ok: false, error: "Lead ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const lead = (await tablesDB.getRow(DATABASE_ID, TABLES.leads, leadId)) as unknown as Lead;
- if (lead.tenantId !== ctx.tenantId) return { ok: false, error: "Erişim engellendi." };
-
- const permissions = teamRowPermissions(ctx.tenantId);
-
- // Create customer from lead data
- const customer = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.customers,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- name: lead.contactName || lead.name,
- email: lead.email,
- phone: lead.phone,
- notes: lead.notes,
- status: "active",
- },
- permissions,
- );
-
- // Update lead: mark converted + link customerId
- await tablesDB.updateRow(DATABASE_ID, TABLES.leads, leadId, {
- status: "converted",
- customerId: customer.$id,
- lastContactAt: new Date().toISOString(),
- });
-
- await tablesDB.createRow(
- DATABASE_ID,
- TABLES.leadActivities,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- leadId,
- type: "status_change",
- content: `Müşteriye dönüştürüldü → ${lead.contactName || lead.name}`,
- occurredAt: new Date().toISOString(),
- },
- permissions,
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "customer_from_lead",
- entityId: customer.$id,
- changes: { leadId, customerId: customer.$id },
- });
-
- revalidatePath("/leads");
- revalidatePath("/customers");
- return { ok: true, leadId: customer.$id };
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-}
diff --git a/src/lib/appwrite/lead-activity-actions.ts b/src/lib/appwrite/lead-activity-actions.ts
deleted file mode 100644
index 5bdee4b..0000000
--- a/src/lib/appwrite/lead-activity-actions.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { ID, Permission, Role } from "node-appwrite";
-
-import { DATABASE_ID, TABLES, type Lead, type LeadActivityType } from "./schema";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-
-export type ActivityActionState = { ok: boolean; error?: string };
-
-export async function addLeadActivityAction(
- _prev: ActivityActionState,
- formData: FormData,
-): Promise {
- const leadId = String(formData.get("leadId") ?? "");
- const type = String(formData.get("type") ?? "note") as LeadActivityType;
- const content = String(formData.get("content") ?? "").trim();
- const occurredAt = String(formData.get("occurredAt") ?? "") || new Date().toISOString();
-
- if (!leadId || !content) return { ok: false, error: "Zorunlu alanlar eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const lead = (await tablesDB.getRow(DATABASE_ID, TABLES.leads, leadId)) as unknown as Lead;
- if (lead.tenantId !== ctx.tenantId) return { ok: false, error: "Erişim engellendi." };
-
- await tablesDB.createRow(
- DATABASE_ID,
- TABLES.leadActivities,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- leadId,
- type,
- content,
- occurredAt,
- },
- [
- Permission.read(Role.team(ctx.tenantId)),
- Permission.update(Role.team(ctx.tenantId)),
- Permission.delete(Role.team(ctx.tenantId, "owner")),
- ],
- );
-
- // Update lastContactAt on the lead
- await tablesDB.updateRow(DATABASE_ID, TABLES.leads, leadId, {
- lastContactAt: new Date().toISOString(),
- });
-
- revalidatePath("/leads");
- return { ok: true };
- } catch (e) {
- return { ok: false, error: e instanceof Error ? e.message : "Hata oluştu." };
- }
-}
-
-export async function scheduleFollowUpAction(
- _prev: ActivityActionState,
- formData: FormData,
-): Promise {
- const leadId = String(formData.get("leadId") ?? "");
- const followUpAt = String(formData.get("followUpAt") ?? "");
- const note = String(formData.get("note") ?? "").trim();
-
- if (!leadId || !followUpAt) return { ok: false, error: "Tarih seçin." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const lead = (await tablesDB.getRow(DATABASE_ID, TABLES.leads, leadId)) as unknown as Lead;
- if (lead.tenantId !== ctx.tenantId) return { ok: false, error: "Erişim engellendi." };
-
- const followUpDate = new Date(followUpAt);
- const endDate = new Date(followUpDate.getTime() + 60 * 60 * 1000); // +1h
-
- const permissions = [
- Permission.read(Role.team(ctx.tenantId)),
- Permission.update(Role.team(ctx.tenantId)),
- Permission.delete(Role.team(ctx.tenantId, "owner")),
- ];
-
- // Create calendar event
- const event = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.calendarEvents,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- title: `Takip: ${lead.contactName || lead.name}`,
- description: note || `Lead takip görüşmesi — ${lead.name}`,
- start: followUpDate.toISOString(),
- end: endDate.toISOString(),
- allDay: false,
- leadId,
- color: "#f97316", // orange — lead events
- },
- permissions,
- );
-
- // Update lead nextFollowUpAt + calendarEventId
- await tablesDB.updateRow(DATABASE_ID, TABLES.leads, leadId, {
- nextFollowUpAt: followUpDate.toISOString(),
- calendarEventId: event.$id,
- });
-
- // Log activity
- await tablesDB.createRow(
- DATABASE_ID,
- TABLES.leadActivities,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- leadId,
- type: "meeting",
- content: `Takip planlandı: ${followUpDate.toLocaleString("tr-TR")}${note ? ` — ${note}` : ""}`,
- calendarEventId: event.$id,
- occurredAt: new Date().toISOString(),
- },
- permissions,
- );
-
- revalidatePath("/leads");
- revalidatePath("/calendar");
- return { ok: true };
- } catch (e) {
- return { ok: false, error: e instanceof Error ? e.message : "Hata oluştu." };
- }
-}
diff --git a/src/lib/appwrite/lead-queries.ts b/src/lib/appwrite/lead-queries.ts
deleted file mode 100644
index 6029f8a..0000000
--- a/src/lib/appwrite/lead-queries.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type Lead, type LeadActivity } from "./schema";
-
-export async function listLeads(tenantId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.leads,
- queries: [Query.equal("tenantId", tenantId), Query.orderDesc("$createdAt"), Query.limit(500)],
- });
- return result.rows as unknown as Lead[];
- } catch {
- return [];
- }
-}
-
-export async function getLead(tenantId: string, leadId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.getRow(DATABASE_ID, TABLES.leads, leadId);
- const lead = row as unknown as Lead;
- if (lead.tenantId !== tenantId) return null;
- return lead;
- } catch {
- return null;
- }
-}
-
-export async function listLeadActivities(tenantId: string, leadId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.leadActivities,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("leadId", leadId),
- Query.orderDesc("$createdAt"),
- Query.limit(100),
- ],
- });
- return result.rows as unknown as LeadActivity[];
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/lead-types.ts b/src/lib/appwrite/lead-types.ts
deleted file mode 100644
index 86a4954..0000000
--- a/src/lib/appwrite/lead-types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export type LeadActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
- leadId?: string;
-};
-
-export const initialLeadState: LeadActionState = { ok: false };
diff --git a/src/lib/appwrite/loan-actions.ts b/src/lib/appwrite/loan-actions.ts
deleted file mode 100644
index 729f2b4..0000000
--- a/src/lib/appwrite/loan-actions.ts
+++ /dev/null
@@ -1,442 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Query } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import {
- DATABASE_ID,
- TABLES,
- type BankLoan,
- type LoanInstallment,
-} from "./schema";
-import { canAccessRow, scopedRowPermissions } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { LoanActionState } from "./loan-types";
-import { bankLoanSchema } from "@/lib/validation/bank-loans";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) return e.message || "Beklenmeyen hata.";
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function pickLoanFields(formData: FormData) {
- return {
- bankAccountId: String(formData.get("bankAccountId") ?? ""),
- bankName: String(formData.get("bankName") ?? "").trim(),
- loanName: String(formData.get("loanName") ?? "").trim(),
- loanType: formData.get("loanType") as
- | "consumer"
- | "vehicle"
- | "housing"
- | "commercial"
- | "kmh"
- | "other"
- | null,
- principal: String(formData.get("principal") ?? "0"),
- interestRate: String(formData.get("interestRate") ?? "0"),
- termMonths: String(formData.get("termMonths") ?? "12"),
- startDate: String(formData.get("startDate") ?? ""),
- paymentDay: String(formData.get("paymentDay") ?? "1"),
- notes: String(formData.get("notes") ?? "").trim(),
- scope: (formData.get("scope") as "company" | "personal" | null) ?? "company",
- };
-}
-
-function toIso(v: string): string {
- if (!v) return v;
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return `${v}T00:00:00.000+00:00`;
- return v;
-}
-
-/**
- * Standard amortization (eşit taksitli kredi).
- * monthlyPayment = P × r × (1+r)^n / ((1+r)^n − 1)
- * where r = monthly interest rate (decimal), n = termMonths
- */
-function computeAmortization(
- principal: number,
- monthlyRatePct: number,
- n: number,
-): {
- monthlyPayment: number;
- schedule: Array<{ principalPart: number; interestPart: number; amount: number }>;
-} {
- const r = monthlyRatePct / 100;
- let monthlyPayment: number;
- if (r === 0) {
- monthlyPayment = principal / n;
- } else {
- const factor = Math.pow(1 + r, n);
- monthlyPayment = (principal * r * factor) / (factor - 1);
- }
- monthlyPayment = Number(monthlyPayment.toFixed(2));
-
- const schedule: Array<{ principalPart: number; interestPart: number; amount: number }> = [];
- let remaining = principal;
- for (let i = 0; i < n; i++) {
- const interestPart = Number((remaining * r).toFixed(2));
- let principalPart = Number((monthlyPayment - interestPart).toFixed(2));
- // Final installment absorbs rounding drift
- if (i === n - 1) {
- principalPart = Number(remaining.toFixed(2));
- }
- const amount = Number((interestPart + principalPart).toFixed(2));
- remaining = Number((remaining - principalPart).toFixed(2));
- schedule.push({ principalPart, interestPart, amount });
- }
- return { monthlyPayment, schedule };
-}
-
-function shiftMonth(date: Date, monthsAhead: number, paymentDay: number): Date {
- const d = new Date(date.getFullYear(), date.getMonth() + monthsAhead, 1);
- // clamp paymentDay to last day of that month
- const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
- d.setDate(Math.min(paymentDay, lastDay));
- return d;
-}
-
-export async function createLoanAction(
- _prev: LoanActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = bankLoanSchema.safeParse(pickLoanFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- const { schedule, monthlyPayment } = computeAmortization(
- parsed.data.principal,
- parsed.data.interestRate,
- parsed.data.termMonths,
- );
-
- let loanId: string | null = null;
- const admin = createAdminClient();
- // Installments inherit the loan's scope so personal-loan installments stay
- // hidden from the rest of the team too.
- const rowPerms = scopedRowPermissions(ctx.tenantId, ctx.user.id, parsed.data.scope);
- try {
- const loan = await admin.tablesDB.createRow(
- DATABASE_ID,
- TABLES.bankLoans,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- bankAccountId: parsed.data.bankAccountId,
- bankName: parsed.data.bankName,
- loanName: parsed.data.loanName,
- loanType: parsed.data.loanType,
- principal: parsed.data.principal,
- interestRate: parsed.data.interestRate,
- termMonths: parsed.data.termMonths,
- monthlyPayment,
- startDate: toIso(parsed.data.startDate),
- paymentDay: parsed.data.paymentDay,
- status: "active",
- notes: parsed.data.notes,
- scope: parsed.data.scope,
- },
- rowPerms,
- );
- loanId = loan.$id;
-
- const start = new Date(toIso(parsed.data.startDate));
- for (let i = 0; i < parsed.data.termMonths; i++) {
- const due = shiftMonth(start, i + 1, parsed.data.paymentDay ?? 1);
- const slice = schedule[i];
- await admin.tablesDB.createRow(
- DATABASE_ID,
- TABLES.loanInstallments,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- loanId,
- installmentNo: i + 1,
- dueDate: due.toISOString(),
- amount: slice.amount,
- principalPart: slice.principalPart,
- interestPart: slice.interestPart,
- paid: false,
- },
- rowPerms,
- );
- }
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "bank_loan",
- entityId: loan.$id,
- changes: { ...parsed.data, monthlyPayment, installments: parsed.data.termMonths },
- });
- } catch (e) {
- if (loanId) {
- // Best-effort rollback: delete partially-created installments + loan
- try {
- const partial = await admin.tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.loanInstallments,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("loanId", loanId),
- Query.limit(500),
- ],
- });
- for (const r of partial.rows) {
- await admin.tablesDB.deleteRow(DATABASE_ID, TABLES.loanInstallments, r.$id);
- }
- await admin.tablesDB.deleteRow(DATABASE_ID, TABLES.bankLoans, loanId);
- } catch {
- /* ignore */
- }
- }
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/loans");
- return { ok: true, loanId };
-}
-
-export async function deleteLoanAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.bankLoans,
- id,
- )) as unknown as BankLoan;
- if (existing.tenantId !== ctx.tenantId || !canAccessRow(existing, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- // Delete installments first
- const installments = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.loanInstallments,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("loanId", id),
- Query.limit(500),
- ],
- });
- for (const r of installments.rows) {
- await tablesDB.deleteRow(DATABASE_ID, TABLES.loanInstallments, r.$id);
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.bankLoans, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "bank_loan",
- entityId: id,
- changes: { loanName: existing.loanName, installments: installments.rows.length },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/loans");
- return { ok: true };
-}
-
-export async function payInstallmentAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.loanInstallments,
- id,
- )) as unknown as LoanInstallment;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
- if (existing.paid) {
- return { ok: false, error: "Bu taksit zaten ödenmiş." };
- }
-
- const loan = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.bankLoans,
- existing.loanId,
- )) as unknown as BankLoan;
- if (!canAccessRow(loan, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const loanScope = loan.scope ?? "company";
- // Create finance entry: expense, linked. Inherit loan's scope so personal
- // installments don't leak into company finance.
- const fe = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.financeEntries,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- type: "expense",
- amount: existing.amount,
- date: new Date().toISOString(),
- description: `${loan.bankName} — ${loan.loanName} #${existing.installmentNo} taksit ödemesi`,
- bankAccountId: loan.bankAccountId,
- scope: loanScope,
- },
- scopedRowPermissions(ctx.tenantId, ctx.user.id, loanScope),
- );
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.loanInstallments, id, {
- paid: true,
- paidAt: new Date().toISOString(),
- financeEntryId: fe.$id,
- });
-
- // If this was the last unpaid one, mark loan closed
- const remaining = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.loanInstallments,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("loanId", existing.loanId),
- Query.equal("paid", false),
- Query.limit(1),
- ],
- });
- if (remaining.rows.length === 0) {
- await tablesDB.updateRow(DATABASE_ID, TABLES.bankLoans, existing.loanId, {
- status: "closed",
- });
- }
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "loan_installment",
- entityId: id,
- changes: { paid: true, financeEntryId: fe.$id, amount: existing.amount },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/loans");
- revalidatePath("/finance");
- return { ok: true };
-}
-
-export async function unpayInstallmentAction(
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.loanInstallments,
- id,
- )) as unknown as LoanInstallment;
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
- if (!existing.paid) return { ok: true };
-
- // Verify the parent loan is also accessible (handles personal scope).
- const parent = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.bankLoans,
- existing.loanId,
- )) as unknown as BankLoan;
- if (!canAccessRow(parent, ctx.user.id)) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- if (existing.financeEntryId) {
- try {
- await tablesDB.deleteRow(
- DATABASE_ID,
- TABLES.financeEntries,
- existing.financeEntryId,
- );
- } catch {
- /* ignore: maybe already gone */
- }
- }
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.loanInstallments, id, {
- paid: false,
- paidAt: null,
- financeEntryId: null,
- });
-
- // If loan was closed, reopen it
- await tablesDB.updateRow(DATABASE_ID, TABLES.bankLoans, existing.loanId, {
- status: "active",
- });
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "loan_installment",
- entityId: id,
- changes: { paid: false },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/finance/loans");
- revalidatePath("/finance");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/loan-queries.ts b/src/lib/appwrite/loan-queries.ts
deleted file mode 100644
index 0751a2a..0000000
--- a/src/lib/appwrite/loan-queries.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { canAccessRow } from "./scope-permissions";
-import { createAdminClient } from "./server";
-import {
- DATABASE_ID,
- TABLES,
- type BankLoan,
- type LoanInstallment,
-} from "./schema";
-
-export async function listLoans(
- tenantId: string,
- currentUserId?: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.bankLoans,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("$createdAt"),
- Query.limit(200),
- ],
- });
- const rows = result.rows as unknown as BankLoan[];
- if (!currentUserId) return rows;
- return rows.filter((r) => canAccessRow(r, currentUserId));
- } catch {
- return [];
- }
-}
-
-export async function listInstallmentsForLoan(
- tenantId: string,
- loanId: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.loanInstallments,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("loanId", loanId),
- Query.orderAsc("installmentNo"),
- Query.limit(500),
- ],
- });
- return result.rows as unknown as LoanInstallment[];
- } catch {
- return [];
- }
-}
-
-/**
- * Pulls all installments and filters to those whose parent loan is visible to the user.
- */
-export async function listAllInstallments(
- tenantId: string,
- currentUserId?: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const [allInst, allLoans] = await Promise.all([
- tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.loanInstallments,
- queries: [Query.equal("tenantId", tenantId), Query.limit(2000)],
- }),
- tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.bankLoans,
- queries: [Query.equal("tenantId", tenantId), Query.limit(500)],
- }),
- ]);
- const visibleLoanIds = new Set(
- (allLoans.rows as unknown as BankLoan[])
- .filter((l) => !currentUserId || canAccessRow(l, currentUserId))
- .map((l) => l.$id),
- );
- return (allInst.rows as unknown as LoanInstallment[]).filter((i) =>
- visibleLoanIds.has(i.loanId),
- );
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/loan-types.ts b/src/lib/appwrite/loan-types.ts
deleted file mode 100644
index 9786e77..0000000
--- a/src/lib/appwrite/loan-types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export type LoanActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
- loanId?: string;
-};
-
-export const initialLoanState: LoanActionState = { ok: false };
diff --git a/src/lib/appwrite/plan-limits.ts b/src/lib/appwrite/plan-limits.ts
deleted file mode 100644
index 9bb4ba8..0000000
--- a/src/lib/appwrite/plan-limits.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type TenantPlan } from "./schema";
-import type { TenantContext } from "./tenant-guard";
-
-export type PlanResource = "customers" | "financeEntries" | "software" | "members";
-
-export const PLAN_LIMIT_EXCEEDED = "PLAN_LIMIT_EXCEEDED";
-
-const INF = Number.POSITIVE_INFINITY;
-
-export const PLAN_LIMITS: Record> = {
- free: {
- customers: 50,
- financeEntries: 100,
- software: 5,
- members: 1,
- },
- pro: {
- customers: INF,
- financeEntries: INF,
- software: INF,
- members: INF,
- },
-};
-
-export const RESOURCE_LABELS: Record = {
- customers: "müşteri",
- financeEntries: "finans kaydı",
- software: "yazılım",
- members: "ekip üyesi",
-};
-
-export function getEffectivePlan(ctx: TenantContext): TenantPlan {
- const plan = ctx.settings?.plan ?? "free";
- if (plan === "pro") {
- const expires = ctx.settings?.planExpiresAt;
- if (expires && new Date(expires).getTime() < Date.now()) {
- return "free";
- }
- }
- return plan;
-}
-
-async function countResource(
- tenantId: string,
- resource: PlanResource,
-): Promise {
- const { tablesDB, teams } = createAdminClient();
-
- if (resource === "members") {
- const result = await teams.listMemberships(tenantId);
- return result.total;
- }
-
- const tableMap: Record, string> = {
- customers: TABLES.customers,
- financeEntries: TABLES.financeEntries,
- software: TABLES.software,
- };
-
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: tableMap[resource],
- queries: [Query.equal("tenantId", tenantId), Query.limit(1)],
- });
- return result.total;
-}
-
-export type PlanUsage = {
- plan: TenantPlan;
- usage: Record;
-};
-
-export async function getPlanUsage(ctx: TenantContext): Promise {
- const plan = getEffectivePlan(ctx);
- const limits = PLAN_LIMITS[plan];
-
- const resources: PlanResource[] = ["customers", "financeEntries", "software", "members"];
- const counts = await Promise.all(resources.map((r) => countResource(ctx.tenantId, r)));
-
- const usage = {} as PlanUsage["usage"];
- resources.forEach((r, i) => {
- const used = counts[i];
- const limit = limits[r];
- usage[r] = { used, limit, reached: used >= limit };
- });
-
- return { plan, usage };
-}
-
-export class PlanLimitError extends Error {
- code = PLAN_LIMIT_EXCEEDED;
- constructor(public resource: PlanResource, public limit: number) {
- super(`Plan limit reached for ${resource} (${limit})`);
- }
-}
-
-export async function requirePlanCapacity(
- ctx: TenantContext,
- resource: PlanResource,
-): Promise {
- const plan = getEffectivePlan(ctx);
- const limit = PLAN_LIMITS[plan][resource];
- if (limit === INF) return;
-
- const used = await countResource(ctx.tenantId, resource);
- if (used >= limit) {
- throw new PlanLimitError(resource, limit);
- }
-}
-
-export function isPlanLimitError(e: unknown): e is PlanLimitError {
- return e instanceof PlanLimitError;
-}
-
-export function planLimitMessage(resource: PlanResource, limit: number): string {
- const label = RESOURCE_LABELS[resource];
- return `Ücretsiz planda en fazla ${limit} ${label} ekleyebilirsiniz. Pro'ya geçerek sınırı kaldırın.`;
-}
diff --git a/src/lib/appwrite/saved-card-actions.ts b/src/lib/appwrite/saved-card-actions.ts
deleted file mode 100644
index b365cae..0000000
--- a/src/lib/appwrite/saved-card-actions.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { ID, Permission, Query, Role } from "node-appwrite";
-
-import { logAudit } from "./audit";
-import { DATABASE_ID, TABLES, type CardBrand, type SavedCard } from "./schema";
-import { createAdminClient } from "./server";
-import { requireRole, requireTenant } from "./tenant-guard";
-
-const VALID_BRANDS: CardBrand[] = ["visa", "mastercard", "amex", "troy", "unknown"];
-
-function teamCardPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId, "owner")),
- Permission.read(Role.team(tenantId, "admin")),
- Permission.update(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "owner")),
- ];
-}
-
-async function clearOtherDefaults(tenantId: string, exceptId?: string): Promise {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.savedCards,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("isDefault", true),
- Query.limit(20),
- ],
- });
- for (const row of result.rows) {
- if (exceptId && row.$id === exceptId) continue;
- await tablesDB.updateRow(DATABASE_ID, TABLES.savedCards, row.$id, {
- isDefault: false,
- });
- }
-}
-
-type SaveCardInput = {
- brand: string;
- last4: string;
- expiryMonth: number;
- expiryYear: number;
- holderName: string;
- makeDefault: boolean;
-};
-
-export async function persistCardFromMockCheckout(input: SaveCardInput): Promise {
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const brand = (VALID_BRANDS as string[]).includes(input.brand)
- ? (input.brand as CardBrand)
- : "unknown";
- const last4 = input.last4.replace(/\D/g, "").slice(-4).padStart(4, "0");
-
- if (last4.length !== 4) throw new Error("Geçersiz kart son 4 hanesi.");
- if (input.expiryMonth < 1 || input.expiryMonth > 12) throw new Error("Geçersiz ay.");
- if (input.expiryYear < 2026 || input.expiryYear > 2099) throw new Error("Geçersiz yıl.");
-
- if (input.makeDefault) {
- await clearOtherDefaults(ctx.tenantId);
- }
-
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.savedCards,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- brand,
- last4,
- expiryMonth: input.expiryMonth,
- expiryYear: input.expiryYear,
- holderName: input.holderName.trim().slice(0, 128) || undefined,
- provider: "mock",
- isDefault: input.makeDefault,
- },
- teamCardPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "saved_card",
- entityId: row.$id,
- changes: { brand, last4, isDefault: input.makeDefault },
- });
-
- revalidatePath("/settings/billing");
-}
-
-export async function setDefaultCardAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) throw new Error("ID eksik.");
-
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.savedCards,
- id,
- )) as unknown as SavedCard;
- if (existing.tenantId !== ctx.tenantId) throw new Error("Erişim engellendi.");
-
- await clearOtherDefaults(ctx.tenantId, id);
- await tablesDB.updateRow(DATABASE_ID, TABLES.savedCards, id, { isDefault: true });
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "saved_card",
- entityId: id,
- changes: { isDefault: true },
- });
-
- revalidatePath("/settings/billing");
-}
-
-export async function removeCardAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) throw new Error("ID eksik.");
-
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.savedCards,
- id,
- )) as unknown as SavedCard;
- if (existing.tenantId !== ctx.tenantId) throw new Error("Erişim engellendi.");
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.savedCards, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "saved_card",
- entityId: id,
- changes: { last4: existing.last4 },
- });
-
- revalidatePath("/settings/billing");
-}
diff --git a/src/lib/appwrite/saved-card-queries.ts b/src/lib/appwrite/saved-card-queries.ts
deleted file mode 100644
index 50deb54..0000000
--- a/src/lib/appwrite/saved-card-queries.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { DATABASE_ID, TABLES, type SavedCard } from "./schema";
-import { createAdminClient } from "./server";
-
-export async function listSavedCards(tenantId: string): Promise {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.savedCards,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("isDefault"),
- Query.orderDesc("$createdAt"),
- Query.limit(20),
- ],
- });
- return result.rows as unknown as SavedCard[];
-}
-
-export async function getDefaultCard(tenantId: string): Promise {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.savedCards,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("isDefault", true),
- Query.limit(1),
- ],
- });
- return (result.rows[0] as unknown as SavedCard) ?? null;
-}
diff --git a/src/lib/appwrite/schema.ts b/src/lib/appwrite/schema.ts
index 14021a5..abe0189 100644
--- a/src/lib/appwrite/schema.ts
+++ b/src/lib/appwrite/schema.ts
@@ -1,31 +1,20 @@
-export const DATABASE_ID = "isletmem";
+export const DATABASE_ID = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID ?? "kovakemlak-db";
export const BUCKETS = {
+ propertyImages: "property-images",
tenantLogos: "tenant-logos",
} as const;
export const TABLES = {
- tenantSettings: "tenant_settings",
+ properties: "properties",
customers: "customers",
- services: "services",
- software: "software",
- customerSoftware: "customer_software",
- calendarEvents: "calendar_events",
- tasks: "tasks",
- financeEntries: "finance_entries",
- invoices: "invoices",
- invoiceItems: "invoice_items",
- auditLogs: "audit_logs",
+ customerSearches: "customer_searches",
+ propertyMatches: "property_matches",
+ presentations: "presentations",
+ investors: "investors",
+ activities: "activities",
+ tenantSettings: "tenant_settings",
inviteLinks: "invite_links",
- bankAccounts: "bank_accounts",
- bankLoans: "bank_loans",
- loanInstallments: "loan_installments",
- creditCards: "credit_cards",
- creditCardStatements: "credit_card_statements",
- subscriptionPayments: "subscription_payments",
- savedCards: "saved_cards",
- leads: "leads",
- leadActivities: "lead_activities",
} as const;
export type TableId = (typeof TABLES)[keyof typeof TABLES];
@@ -43,318 +32,196 @@ export type SystemRow = {
type Row = SystemRow;
export type TenantRole = "owner" | "admin" | "member";
-
-export type TenantPlan = "free" | "pro";
-
-export interface TenantSettings extends Row {
- tenantId: string;
- companyName: string;
- companyTaxId?: string;
- companyAddress?: string;
- companyEmail?: string;
- companyPhone?: string;
- logo?: string;
- defaultVatRate?: number;
- invoicePrefix?: string;
- invoiceCounter?: number;
- plan?: TenantPlan;
- planStartedAt?: string;
- planExpiresAt?: string;
- lastPaymentId?: string;
-}
-
-export type CustomerStatus = "active" | "passive";
-
-export interface Customer extends Row {
- tenantId: string;
- createdBy: string;
- name: string;
- email?: string;
- phone?: string;
- taxId?: string;
- address?: string;
- notes?: string;
- status?: CustomerStatus;
-}
-
-export type BillingPeriod = "monthly" | "yearly" | "onetime";
-
-export interface Service extends Row {
- tenantId: string;
- createdBy: string;
- customerId: string;
- name: string;
- description?: string;
- unitPrice: number;
- currency?: string;
- recurring?: boolean;
- billingPeriod?: BillingPeriod;
- assigneeIds?: string[];
-}
-
-export interface Software extends Row {
- tenantId: string;
- createdBy: string;
- name: string;
- version?: string;
- description?: string;
- defaultFee?: number;
-}
-
-export interface CustomerSoftware extends Row {
- tenantId: string;
- createdBy: string;
- customerId: string;
- softwareId: string;
- startDate?: string;
- endDate?: string;
- fee?: number;
- billingPeriod?: BillingPeriod;
- notes?: string;
-}
-
-export interface CalendarEvent extends Row {
- tenantId: string;
- createdBy: string;
- title: string;
- description?: string;
- start: string;
- end: string;
- allDay?: boolean;
- customerId?: string;
- leadId?: string;
- color?: string;
-}
-
-export type LeadStatus = "cold" | "warm" | "hot" | "converted" | "lost";
-export type LeadSource = "website" | "social" | "referral" | "cold_call" | "event" | "other";
-export type LeadActivityType = "note" | "call" | "meeting" | "email" | "status_change";
-
-export interface Lead extends Row {
- tenantId: string;
- createdBy: string;
- name: string;
- contactName?: string;
- email?: string;
- phone?: string;
- source?: LeadSource;
- status?: LeadStatus;
- estimatedValue?: number;
- currency?: string;
- notes?: string;
- assigneeId?: string;
- lastContactAt?: string;
- nextFollowUpAt?: string;
- calendarEventId?: string;
- customerId?: string;
-}
-
-export interface LeadActivity extends Row {
- tenantId: string;
- createdBy: string;
- leadId: string;
- type: LeadActivityType;
- content: string;
- calendarEventId?: string;
- occurredAt?: string;
-}
-
-export type TaskStatus = "backlog" | "todo" | "in_progress" | "done";
-export type TaskPriority = "low" | "medium" | "high" | "urgent";
-
-export interface Task extends Row {
- tenantId: string;
- createdBy: string;
- title: string;
- description?: string;
- status?: TaskStatus;
- priority?: TaskPriority;
- dueDate?: string;
- assigneeId?: string;
- customerId?: string;
- order?: number;
-}
-
-export type FinanceType = "income" | "expense" | "debt" | "receivable";
-export type PaymentMethod = "cash" | "transfer" | "card" | "check" | "other";
-
-export interface FinanceEntry extends Row {
- tenantId: string;
- createdBy: string;
- type: FinanceType;
- amount: number;
- date: string;
- description?: string;
- customerId?: string;
- invoiceId?: string;
- paymentMethod?: PaymentMethod;
- bankAccountId?: string;
- scope?: "company" | "personal";
-}
-
-export type InvoiceStatus = "draft" | "sent" | "paid" | "overdue" | "cancelled";
-
-export interface Invoice extends Row {
- tenantId: string;
- createdBy: string;
- number: string;
- customerId: string;
- issueDate: string;
- dueDate: string;
- status?: InvoiceStatus;
- subtotal?: number;
- vatTotal?: number;
- total?: number;
- notes?: string;
-}
-
-export interface InvoiceItem extends Row {
- tenantId: string;
- createdBy: string;
- invoiceId: string;
- description: string;
- quantity: number;
- unitPrice: number;
- vatRate?: number;
- lineTotal: number;
-}
-
-export type AuditAction = "create" | "update" | "delete";
-
-export interface AuditLog extends Row {
- tenantId: string;
- userId: string;
- action: AuditAction;
- entityType: string;
- entityId: string;
- changes?: string;
- ipAddress?: string;
- userAgent?: string;
-}
-
-export type FinanceScope = "company" | "personal";
-
-export interface BankAccount extends Row {
- tenantId: string;
- createdBy: string;
- bankName: string;
- accountName: string;
- iban?: string;
- openingBalance?: number;
- notes?: string;
- archived?: boolean;
- scope?: FinanceScope;
-}
-
-export type LoanType = "consumer" | "vehicle" | "housing" | "commercial" | "kmh" | "other";
-export type LoanStatus = "active" | "closed" | "defaulted";
-
-export interface BankLoan extends Row {
- tenantId: string;
- createdBy: string;
- bankAccountId?: string;
- bankName: string;
- loanName: string;
- loanType?: LoanType;
- principal: number;
- interestRate: number; // monthly nominal %
- termMonths: number;
- monthlyPayment?: number;
- startDate: string;
- paymentDay?: number;
- status?: LoanStatus;
- notes?: string;
- scope?: FinanceScope;
-}
-
-export interface LoanInstallment extends Row {
- tenantId: string;
- loanId: string;
- installmentNo: number;
- dueDate: string;
- amount: number;
- principalPart?: number;
- interestPart?: number;
- paid?: boolean;
- paidAt?: string;
- financeEntryId?: string;
-}
-
-export interface CreditCard extends Row {
- tenantId: string;
- createdBy: string;
- bankName: string;
- cardName: string;
- last4?: string;
- creditLimit?: number;
- statementDay?: number;
- dueDay?: number;
- interestRate?: number;
- bankAccountId?: string;
- archived?: boolean;
- notes?: string;
- scope?: FinanceScope;
-}
-
-export type StatementStatus = "pending" | "partial" | "paid" | "overdue";
-
-export interface CreditCardStatement extends Row {
- tenantId: string;
- createdBy: string;
- cardId: string;
- period: string; // YYYY-MM
- statementDate: string;
- dueDate: string;
- totalDebt: number;
- minimumPayment?: number;
- paidAmount?: number;
- status?: StatementStatus;
- financeEntryId?: string;
- notes?: string;
-}
-
-export type SubscriptionStatus = "pending" | "success" | "failed" | "refunded";
-export type SubscriptionProvider = "mock" | "shopier" | "polar";
-
-export type CardBrand = "visa" | "mastercard" | "amex" | "troy" | "unknown";
-
-export interface SavedCard extends Row {
- tenantId: string;
- createdBy: string;
- brand?: CardBrand;
- last4: string;
- expiryMonth: number;
- expiryYear: number;
- holderName?: string;
- providerToken?: string;
- provider?: SubscriptionProvider;
- isDefault?: boolean;
-}
-
-export interface SubscriptionPayment extends Row {
- tenantId: string;
- createdBy: string;
- orderId: string;
- plan: TenantPlan;
- amount: number;
- currency?: string;
- status?: SubscriptionStatus;
- provider?: SubscriptionProvider;
- providerPayload?: string;
- processedAt?: string;
-}
-
export type InviteRole = "admin" | "member";
-export type InviteStatus = "pending" | "accepted" | "cancelled" | "expired";
-export interface InviteLink extends Row {
+export interface InviteLink extends SystemRow {
tenantId: string;
code: string;
email: string;
- role?: InviteRole;
- status?: InviteStatus;
+ role: InviteRole;
+ status: "pending" | "accepted" | "expired" | "cancelled";
invitedBy: string;
- expiresAt?: string;
+ expiresAt: string;
acceptedAt?: string;
acceptedBy?: string;
}
+
+export type PropertyType = "daire" | "villa" | "arsa" | "dukkan" | "ofis" | "depo";
+export type ListingType = "satilik" | "kiralik";
+export type PropertyStatus = "aktif" | "pasif" | "satildi" | "kiralandit";
+export type CustomerType = "alici" | "kiraci" | "yatirimci";
+export type ActivityType = "gorusme" | "teklif" | "ziyaret" | "arama" | "not";
+
+export interface Property extends Row {
+ tenantId: string;
+ title: string;
+ description?: string;
+ propertyType: PropertyType;
+ listingType: ListingType;
+ status: PropertyStatus;
+ price: number;
+ currency?: string;
+ roomCount?: string;
+ grossM2?: number;
+ netM2?: number;
+ floor?: number;
+ totalFloors?: number;
+ buildingAge?: number;
+ city: string;
+ district?: string;
+ neighborhood?: string;
+ address?: string;
+ mapLat?: number;
+ mapLng?: number;
+ featuresJson?: string;
+ imageIds?: string;
+ createdBy: string;
+ assigneeId?: string;
+}
+
+export interface Customer extends Row {
+ tenantId: string;
+ name: string;
+ email?: string;
+ phone?: string;
+ type: CustomerType;
+ notes?: string;
+ createdBy: string;
+}
+
+export interface CustomerSearch extends Row {
+ tenantId: string;
+ customerId: string;
+ listingType?: ListingType;
+ propertyTypes?: string;
+ roomCounts?: string;
+ minPrice?: number;
+ maxPrice?: number;
+ minM2?: number;
+ maxM2?: number;
+ cities?: string;
+ districts?: string;
+ featuresJson?: string;
+ isActive?: boolean;
+ notes?: string;
+ createdBy: string;
+}
+
+export interface PropertyMatch extends Row {
+ tenantId: string;
+ propertyId: string;
+ customerId: string;
+ searchId: string;
+ notified?: boolean;
+ viewedAt?: string;
+ createdBy: string;
+}
+
+export interface Presentation extends Row {
+ tenantId: string;
+ title: string;
+ customerId?: string;
+ propertyIds: string;
+ shareToken: string;
+ expiresAt?: string;
+ viewCount?: number;
+ notes?: string;
+ createdBy: string;
+}
+
+export interface Investor extends Row {
+ tenantId: string;
+ userId?: string;
+ name: string;
+ email: string;
+ phone?: string;
+ budget?: number;
+ currency?: string;
+ notes?: string;
+ createdBy: string;
+}
+
+export interface Activity extends Row {
+ tenantId: string;
+ customerId?: string;
+ propertyId?: string;
+ type: ActivityType;
+ title: string;
+ description?: string;
+ dueDate?: string;
+ completedAt?: string;
+ createdBy: string;
+}
+
+export interface TenantSettings extends Row {
+ tenantId: string;
+ officeName?: string;
+ logo?: string;
+ defaultCurrency?: string;
+ phone?: string;
+ email?: string;
+ address?: string;
+ createdBy: string;
+}
+
+export type PropertyFeature =
+ | "balkon"
+ | "otopark"
+ | "asansor"
+ | "havuz"
+ | "spor_salonu"
+ | "guvenlik"
+ | "site_ici"
+ | "esyali"
+ | "merkezi_isitma"
+ | "dogalgaz"
+ | "deprem_yalitim"
+ | "bahce";
+
+export const ROOM_COUNT_OPTIONS = [
+ "Stüdyo",
+ "1+0",
+ "1+1",
+ "2+1",
+ "3+1",
+ "4+1",
+ "4+2",
+ "5+1",
+ "5+2",
+ "6+",
+] as const;
+
+export const PROPERTY_TYPE_LABELS: Record = {
+ daire: "Daire",
+ villa: "Villa",
+ arsa: "Arsa",
+ dukkan: "Dükkan",
+ ofis: "Ofis",
+ depo: "Depo",
+};
+
+export const LISTING_TYPE_LABELS: Record = {
+ satilik: "Satılık",
+ kiralik: "Kiralık",
+};
+
+export const PROPERTY_STATUS_LABELS: Record = {
+ aktif: "Aktif",
+ pasif: "Pasif",
+ satildi: "Satıldı",
+ kiralandit: "Kiralandı",
+};
+
+export const CUSTOMER_TYPE_LABELS: Record = {
+ alici: "Alıcı",
+ kiraci: "Kiracı",
+ yatirimci: "Yatırımcı",
+};
+
+export const ACTIVITY_TYPE_LABELS: Record = {
+ gorusme: "Görüşme",
+ teklif: "Teklif",
+ ziyaret: "Ziyaret",
+ arama: "Arama",
+ not: "Not",
+};
diff --git a/src/lib/appwrite/scope-permissions.ts b/src/lib/appwrite/scope-permissions.ts
deleted file mode 100644
index 286bfcb..0000000
--- a/src/lib/appwrite/scope-permissions.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import "server-only";
-
-import { Permission, Role } from "node-appwrite";
-
-import type { FinanceScope } from "./schema";
-
-/**
- * Returns row-level permissions for finance-related entities.
- *
- * - `company`: visible to the whole tenant team. Owner/admin can delete.
- * - `personal`: visible/editable/deletable only by the creator.
- *
- * The Appwrite "users" table-level perms still gate writes; these row-level
- * perms gate reads and per-row mutations.
- */
-export function scopedRowPermissions(
- tenantId: string,
- createdBy: string,
- scope: FinanceScope,
-): string[] {
- if (scope === "personal") {
- return [
- Permission.read(Role.user(createdBy)),
- Permission.update(Role.user(createdBy)),
- Permission.delete(Role.user(createdBy)),
- ];
- }
- return [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ];
-}
-
-export function normalizeScope(v: unknown): FinanceScope {
- return v === "personal" ? "personal" : "company";
-}
-
-/**
- * Returns true if the current user is allowed to read the row.
- * - company-scope: any team member
- * - personal-scope: only the creator
- */
-export function canAccessRow(
- row: { scope?: FinanceScope; createdBy?: string },
- currentUserId: string,
-): boolean {
- if ((row.scope ?? "company") === "personal") {
- return row.createdBy === currentUserId;
- }
- return true;
-}
diff --git a/src/lib/appwrite/search-actions.ts b/src/lib/appwrite/search-actions.ts
deleted file mode 100644
index 3706430..0000000
--- a/src/lib/appwrite/search-actions.ts
+++ /dev/null
@@ -1,241 +0,0 @@
-"use server";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import {
- DATABASE_ID,
- TABLES,
- type CalendarEvent,
- type Customer,
- type FinanceEntry,
- type Invoice,
- type Service,
- type Software,
- type Task,
-} from "./schema";
-import { requireTenant } from "./tenant-guard";
-
-export type SearchHit = {
- id: string;
- title: string;
- subtitle?: string;
- url: string;
- group: SearchGroup;
-};
-
-export type SearchGroup =
- | "customers"
- | "invoices"
- | "tasks"
- | "services"
- | "software"
- | "events"
- | "finance";
-
-export type SearchResults = Record;
-
-const PAGE_LIMIT = 200; // per entity, plenty for search-time scan
-const MAX_HITS_PER_GROUP = 8;
-
-const TYPE_LABEL: Record = {
- income: "Gelir",
- expense: "Gider",
- debt: "Borç",
- receivable: "Alacak",
-};
-
-function tryMatch(haystack: string | undefined | null, needle: string): boolean {
- if (!haystack) return false;
- return haystack.toLocaleLowerCase("tr-TR").includes(needle);
-}
-
-export async function globalSearchAction(rawQuery: string): Promise {
- const empty: SearchResults = {
- customers: [],
- invoices: [],
- tasks: [],
- services: [],
- software: [],
- events: [],
- finance: [],
- };
-
- const q = rawQuery.trim().toLocaleLowerCase("tr-TR");
- if (!q || q.length < 2) return empty;
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return empty;
- }
-
- const { tablesDB } = createAdminClient();
- const tenantQ = [Query.equal("tenantId", ctx.tenantId), Query.limit(PAGE_LIMIT)];
-
- const [customers, invoices, tasks, services, software, events, finance] =
- await Promise.all([
- tablesDB
- .listRows({ databaseId: DATABASE_ID, tableId: TABLES.customers, queries: tenantQ })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({ databaseId: DATABASE_ID, tableId: TABLES.invoices, queries: tenantQ })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({ databaseId: DATABASE_ID, tableId: TABLES.tasks, queries: tenantQ })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({ databaseId: DATABASE_ID, tableId: TABLES.services, queries: tenantQ })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({ databaseId: DATABASE_ID, tableId: TABLES.software, queries: tenantQ })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({ databaseId: DATABASE_ID, tableId: TABLES.calendarEvents, queries: tenantQ })
- .catch(() => ({ rows: [] as unknown[] })),
- tablesDB
- .listRows({ databaseId: DATABASE_ID, tableId: TABLES.financeEntries, queries: tenantQ })
- .catch(() => ({ rows: [] as unknown[] })),
- ]);
-
- const customerMap = new Map();
- for (const c of customers.rows as unknown as Customer[]) {
- customerMap.set(c.$id, c.name);
- }
-
- // ---------- Customers ----------
- const customerHits: SearchHit[] = [];
- for (const c of customers.rows as unknown as Customer[]) {
- if (
- tryMatch(c.name, q) ||
- tryMatch(c.email, q) ||
- tryMatch(c.phone, q) ||
- tryMatch(c.taxId, q)
- ) {
- customerHits.push({
- id: c.$id,
- title: c.name,
- subtitle: c.email || c.phone || c.taxId || undefined,
- url: "/customers",
- group: "customers",
- });
- if (customerHits.length >= MAX_HITS_PER_GROUP) break;
- }
- }
-
- // ---------- Invoices ----------
- const invoiceHits: SearchHit[] = [];
- for (const inv of invoices.rows as unknown as Invoice[]) {
- if (
- tryMatch(inv.number, q) ||
- tryMatch(inv.notes, q) ||
- tryMatch(customerMap.get(inv.customerId), q)
- ) {
- invoiceHits.push({
- id: inv.$id,
- title: inv.number,
- subtitle: customerMap.get(inv.customerId) ?? undefined,
- url: `/invoices/${inv.$id}`,
- group: "invoices",
- });
- if (invoiceHits.length >= MAX_HITS_PER_GROUP) break;
- }
- }
-
- // ---------- Tasks ----------
- const taskHits: SearchHit[] = [];
- for (const t of tasks.rows as unknown as Task[]) {
- if (tryMatch(t.title, q) || tryMatch(t.description, q)) {
- taskHits.push({
- id: t.$id,
- title: t.title,
- subtitle: t.description ? t.description.slice(0, 80) : undefined,
- url: "/tasks",
- group: "tasks",
- });
- if (taskHits.length >= MAX_HITS_PER_GROUP) break;
- }
- }
-
- // ---------- Services ----------
- const serviceHits: SearchHit[] = [];
- for (const s of services.rows as unknown as Service[]) {
- if (tryMatch(s.name, q) || tryMatch(s.description, q)) {
- serviceHits.push({
- id: s.$id,
- title: s.name,
- subtitle: customerMap.get(s.customerId) ?? undefined,
- url: "/services",
- group: "services",
- });
- if (serviceHits.length >= MAX_HITS_PER_GROUP) break;
- }
- }
-
- // ---------- Software ----------
- const softwareHits: SearchHit[] = [];
- for (const s of software.rows as unknown as Software[]) {
- if (tryMatch(s.name, q) || tryMatch(s.version, q) || tryMatch(s.description, q)) {
- softwareHits.push({
- id: s.$id,
- title: s.name,
- subtitle: s.version ? `v${s.version}` : undefined,
- url: "/software",
- group: "software",
- });
- if (softwareHits.length >= MAX_HITS_PER_GROUP) break;
- }
- }
-
- // ---------- Calendar events ----------
- const eventHits: SearchHit[] = [];
- for (const e of events.rows as unknown as CalendarEvent[]) {
- if (tryMatch(e.title, q) || tryMatch(e.description, q)) {
- eventHits.push({
- id: e.$id,
- title: e.title,
- subtitle: new Date(e.start).toLocaleDateString("tr-TR", {
- day: "2-digit",
- month: "short",
- year: "numeric",
- }),
- url: "/calendar",
- group: "events",
- });
- if (eventHits.length >= MAX_HITS_PER_GROUP) break;
- }
- }
-
- // ---------- Finance entries ----------
- const financeHits: SearchHit[] = [];
- for (const e of finance.rows as unknown as FinanceEntry[]) {
- const amountStr = e.amount.toString();
- if (
- tryMatch(e.description, q) ||
- tryMatch(customerMap.get(e.customerId ?? ""), q) ||
- tryMatch(amountStr, q)
- ) {
- financeHits.push({
- id: e.$id,
- title: `${TYPE_LABEL[e.type]} — ${e.amount.toLocaleString("tr-TR", { minimumFractionDigits: 2 })} ₺`,
- subtitle:
- (customerMap.get(e.customerId ?? "") || e.description || "").slice(0, 80) ||
- undefined,
- url: "/finance",
- group: "finance",
- });
- if (financeHits.length >= MAX_HITS_PER_GROUP) break;
- }
- }
-
- return {
- customers: customerHits,
- invoices: invoiceHits,
- tasks: taskHits,
- services: serviceHits,
- software: softwareHits,
- events: eventHits,
- finance: financeHits,
- };
-}
diff --git a/src/lib/appwrite/service-actions.ts b/src/lib/appwrite/service-actions.ts
deleted file mode 100644
index c529c04..0000000
--- a/src/lib/appwrite/service-actions.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Permission, Role } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import { DATABASE_ID, TABLES, type Service } from "./schema";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { ServiceActionState } from "./service-types";
-import { serviceSchema } from "@/lib/validation/services";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) {
- return e.message || "Beklenmeyen bir hata oluştu.";
- }
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function pickFormFields(formData: FormData) {
- return {
- customerId: String(formData.get("customerId") ?? ""),
- name: String(formData.get("name") ?? "").trim(),
- description: String(formData.get("description") ?? "").trim(),
- unitPrice: String(formData.get("unitPrice") ?? "0"),
- currency: (formData.get("currency") as "TRY" | "USD" | "EUR" | null) ?? "TRY",
- recurring: formData.get("recurring") ?? false,
- billingPeriod: (formData.get("billingPeriod") as "monthly" | "yearly" | "onetime" | null) ??
- "onetime",
- assigneeIds: formData.getAll("assigneeIds").map(String).filter(Boolean),
- };
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function teamRowPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ];
-}
-
-export async function createServiceAction(
- _prev: ServiceActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = serviceSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.services,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...parsed.data,
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "service",
- entityId: row.$id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/services");
- return { ok: true };
-}
-
-export async function updateServiceAction(
- _prev: ServiceActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = serviceSchema.safeParse(pickFormFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.services,
- id,
- )) as unknown as Service;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.services, id, parsed.data);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "service",
- entityId: id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/services");
- return { ok: true };
-}
-
-export async function deleteServiceAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.services,
- id,
- )) as unknown as Service;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.services, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "service",
- entityId: id,
- changes: { name: existing.name },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/services");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/service-queries.ts b/src/lib/appwrite/service-queries.ts
deleted file mode 100644
index b9f7a6b..0000000
--- a/src/lib/appwrite/service-queries.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import { DATABASE_ID, TABLES, type Service } from "./schema";
-
-export async function listServices(tenantId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.services,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("$createdAt"),
- Query.limit(500),
- ],
- });
- return result.rows as unknown as Service[];
- } catch {
- return [];
- }
-}
-
-export async function listServicesByCustomer(
- tenantId: string,
- customerId: string,
-): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.services,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.equal("customerId", customerId),
- Query.orderDesc("$createdAt"),
- Query.limit(500),
- ],
- });
- return result.rows as unknown as Service[];
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/service-types.ts b/src/lib/appwrite/service-types.ts
deleted file mode 100644
index 2460937..0000000
--- a/src/lib/appwrite/service-types.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type ServiceActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
-};
-
-export const initialServiceState: ServiceActionState = { ok: false };
diff --git a/src/lib/appwrite/software-actions.ts b/src/lib/appwrite/software-actions.ts
deleted file mode 100644
index 4da7aa8..0000000
--- a/src/lib/appwrite/software-actions.ts
+++ /dev/null
@@ -1,388 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
-import { z } from "zod";
-
-import { logAudit } from "./audit";
-import {
- isPlanLimitError,
- planLimitMessage,
- requirePlanCapacity,
-} from "./plan-limits";
-import {
- DATABASE_ID,
- TABLES,
- type CustomerSoftware,
- type Software,
-} from "./schema";
-import { createAdminClient } from "./server";
-import { requireTenant } from "./tenant-guard";
-import type { SoftwareActionState } from "./software-types";
-import { customerSoftwareSchema, softwareSchema } from "@/lib/validation/software";
-
-function appwriteError(e: unknown): string {
- if (e instanceof AppwriteException) {
- return e.message || "Beklenmeyen bir hata oluştu.";
- }
- return "Bağlantı hatası. Tekrar deneyin.";
-}
-
-function flattenErrors(err: z.ZodError): Record {
- const out: Record = {};
- for (const issue of err.issues) {
- const key = issue.path.join(".");
- if (key && !out[key]) out[key] = issue.message;
- }
- return out;
-}
-
-function teamRowPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId)),
- Permission.update(Role.team(tenantId)),
- Permission.delete(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "admin")),
- ];
-}
-
-// -------------------- Software (catalog) --------------------
-
-function pickSoftwareFields(formData: FormData) {
- return {
- name: String(formData.get("name") ?? "").trim(),
- version: String(formData.get("version") ?? "").trim(),
- description: String(formData.get("description") ?? "").trim(),
- defaultFee: String(formData.get("defaultFee") ?? ""),
- };
-}
-
-export async function createSoftwareAction(
- _prev: SoftwareActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = softwareSchema.safeParse(pickSoftwareFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- await requirePlanCapacity(ctx, "software");
- } catch (e) {
- if (isPlanLimitError(e)) {
- return {
- ok: false,
- error: planLimitMessage(e.resource, e.limit),
- code: "PLAN_LIMIT_EXCEEDED",
- };
- }
- throw e;
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.software,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...parsed.data,
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "software",
- entityId: row.$id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/software");
- return { ok: true };
-}
-
-export async function updateSoftwareAction(
- _prev: SoftwareActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = softwareSchema.safeParse(pickSoftwareFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.software,
- id,
- )) as unknown as Software;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.software, id, parsed.data);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "software",
- entityId: id,
- changes: parsed.data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/software");
- return { ok: true };
-}
-
-export async function deleteSoftwareAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.software,
- id,
- )) as unknown as Software;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- // Detach from all customer_software rows first
- const assignments = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.customerSoftware,
- queries: [
- Query.equal("tenantId", ctx.tenantId),
- Query.equal("softwareId", id),
- Query.limit(500),
- ],
- });
- for (const row of assignments.rows) {
- await tablesDB.deleteRow(DATABASE_ID, TABLES.customerSoftware, row.$id);
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.software, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "software",
- entityId: id,
- changes: { name: existing.name, detachedAssignments: assignments.rows.length },
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/software");
- return { ok: true };
-}
-
-// -------------------- customer_software (assignments) --------------------
-
-function pickAssignmentFields(formData: FormData) {
- return {
- customerId: String(formData.get("customerId") ?? ""),
- softwareId: String(formData.get("softwareId") ?? ""),
- startDate: String(formData.get("startDate") ?? ""),
- endDate: String(formData.get("endDate") ?? ""),
- fee: String(formData.get("fee") ?? ""),
- billingPeriod: (formData.get("billingPeriod") as "monthly" | "yearly" | "onetime" | null) ??
- "monthly",
- notes: String(formData.get("notes") ?? "").trim(),
- };
-}
-
-function toIsoDate(v?: string): string | undefined {
- if (!v) return undefined;
- // input type=date sends YYYY-MM-DD; Appwrite expects ISO with timezone
- if (/^\d{4}-\d{2}-\d{2}$/.test(v)) return `${v}T00:00:00.000+00:00`;
- return v;
-}
-
-export async function createAssignmentAction(
- _prev: SoftwareActionState,
- formData: FormData,
-): Promise {
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = customerSoftwareSchema.safeParse(pickAssignmentFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const data = {
- ...parsed.data,
- startDate: toIsoDate(parsed.data.startDate),
- endDate: toIsoDate(parsed.data.endDate),
- };
- const row = await tablesDB.createRow(
- DATABASE_ID,
- TABLES.customerSoftware,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- ...data,
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "customer_software",
- entityId: row.$id,
- changes: data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/software");
- return { ok: true };
-}
-
-export async function updateAssignmentAction(
- _prev: SoftwareActionState,
- formData: FormData,
-): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- const parsed = customerSoftwareSchema.safeParse(pickAssignmentFields(formData));
- if (!parsed.success) {
- return { ok: false, error: "Form geçersiz.", fieldErrors: flattenErrors(parsed.error) };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.customerSoftware,
- id,
- )) as unknown as CustomerSoftware;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- const data = {
- ...parsed.data,
- startDate: toIsoDate(parsed.data.startDate),
- endDate: toIsoDate(parsed.data.endDate),
- };
- await tablesDB.updateRow(DATABASE_ID, TABLES.customerSoftware, id, data);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "customer_software",
- entityId: id,
- changes: data,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/software");
- return { ok: true };
-}
-
-export async function deleteAssignmentAction(formData: FormData): Promise {
- const id = String(formData.get("id") ?? "");
- if (!id) return { ok: false, error: "ID eksik." };
-
- let ctx;
- try {
- ctx = await requireTenant();
- } catch {
- return { ok: false, error: "Yetkiniz yok." };
- }
-
- try {
- const { tablesDB } = createAdminClient();
- const existing = (await tablesDB.getRow(
- DATABASE_ID,
- TABLES.customerSoftware,
- id,
- )) as unknown as CustomerSoftware;
-
- if (existing.tenantId !== ctx.tenantId) {
- return { ok: false, error: "Erişim engellendi." };
- }
-
- await tablesDB.deleteRow(DATABASE_ID, TABLES.customerSoftware, id);
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "delete",
- entityType: "customer_software",
- entityId: id,
- });
- } catch (e) {
- return { ok: false, error: appwriteError(e) };
- }
-
- revalidatePath("/software");
- return { ok: true };
-}
diff --git a/src/lib/appwrite/software-queries.ts b/src/lib/appwrite/software-queries.ts
deleted file mode 100644
index 9ba32dc..0000000
--- a/src/lib/appwrite/software-queries.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import "server-only";
-
-import { Query } from "node-appwrite";
-
-import { createAdminClient } from "./server";
-import {
- DATABASE_ID,
- TABLES,
- type CustomerSoftware,
- type Software,
-} from "./schema";
-
-export async function listSoftware(tenantId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.software,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderAsc("name"),
- Query.limit(500),
- ],
- });
- return result.rows as unknown as Software[];
- } catch {
- return [];
- }
-}
-
-export async function listAssignments(tenantId: string): Promise {
- try {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.customerSoftware,
- queries: [
- Query.equal("tenantId", tenantId),
- Query.orderDesc("$createdAt"),
- Query.limit(1000),
- ],
- });
- return result.rows as unknown as CustomerSoftware[];
- } catch {
- return [];
- }
-}
diff --git a/src/lib/appwrite/software-types.ts b/src/lib/appwrite/software-types.ts
deleted file mode 100644
index 394b296..0000000
--- a/src/lib/appwrite/software-types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export type SoftwareActionState = {
- ok: boolean;
- error?: string;
- fieldErrors?: Record;
- code?: "PLAN_LIMIT_EXCEEDED";
-};
-
-export const initialSoftwareState: SoftwareActionState = { ok: false };
diff --git a/src/lib/appwrite/subscription-actions.ts b/src/lib/appwrite/subscription-actions.ts
deleted file mode 100644
index e114a95..0000000
--- a/src/lib/appwrite/subscription-actions.ts
+++ /dev/null
@@ -1,323 +0,0 @@
-"use server";
-
-import { revalidatePath } from "next/cache";
-import { redirect } from "next/navigation";
-import { ID, Permission, Query, Role } from "node-appwrite";
-
-import { logAudit } from "./audit";
-import { persistCardFromMockCheckout } from "./saved-card-actions";
-import { getDefaultCard } from "./saved-card-queries";
-import { DATABASE_ID, TABLES, type SubscriptionPayment, type TenantPlan } from "./schema";
-import { createAdminClient } from "./server";
-import { requireRole, requireTenant } from "./tenant-guard";
-import { PLAN_CATALOG } from "./subscription-types";
-import { getShopierPlanUrl, isShopierEnabled } from "../payments/shopier";
-import { createPolarCheckout, isPolarEnabled } from "../payments/polar";
-
-const PRO_VALIDITY_DAYS = 30;
-
-function teamRowPermissions(tenantId: string) {
- return [
- Permission.read(Role.team(tenantId, "owner")),
- Permission.read(Role.team(tenantId, "admin")),
- Permission.update(Role.team(tenantId, "owner")),
- Permission.delete(Role.team(tenantId, "owner")),
- ];
-}
-
-function generateOrderId(): string {
- const t = Date.now().toString(36);
- const r = Math.random().toString(36).slice(2, 10);
- return `ord_${t}_${r}`;
-}
-
-export async function startMockCheckoutAction(formData: FormData): Promise {
- const plan = String(formData.get("plan") ?? "") as TenantPlan;
- if (plan !== "pro") throw new Error("Geçersiz plan.");
-
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const catalog = PLAN_CATALOG[plan];
- const orderId = generateOrderId();
-
- const { tablesDB } = createAdminClient();
- await tablesDB.createRow(
- DATABASE_ID,
- TABLES.subscriptionPayments,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- orderId,
- plan,
- amount: catalog.price,
- currency: catalog.currency,
- status: "pending",
- provider: "mock",
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "subscription_payment",
- entityId: orderId,
- changes: { plan, amount: catalog.price, provider: "mock" },
- });
-
- redirect(`/settings/billing/checkout/${orderId}`);
-}
-
-async function findPendingPaymentByOrderId(
- tenantId: string,
- orderId: string,
-): Promise {
- const { tablesDB } = createAdminClient();
- const result = await tablesDB.listRows({
- databaseId: DATABASE_ID,
- tableId: TABLES.subscriptionPayments,
- queries: [Query.equal("orderId", orderId), Query.equal("tenantId", tenantId), Query.limit(1)],
- });
- return (result.rows[0] as unknown as SubscriptionPayment) ?? null;
-}
-
-export async function confirmMockPaymentAction(formData: FormData): Promise {
- const orderId = String(formData.get("orderId") ?? "");
- if (!orderId) throw new Error("orderId eksik.");
-
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const payment = await findPendingPaymentByOrderId(ctx.tenantId, orderId);
- if (!payment) throw new Error("Ödeme bulunamadı.");
- if (payment.status === "success") {
- redirect(`/settings/billing?upgraded=1`);
- }
- if (payment.provider !== "mock") {
- throw new Error("Bu ödeme mock olarak onaylanamaz.");
- }
-
- const saveCard = String(formData.get("saveCard") ?? "") === "true";
- const useSavedCardId = String(formData.get("savedCardId") ?? "");
-
- const { tablesDB } = createAdminClient();
- const now = new Date();
- const expires = new Date(now.getTime() + PRO_VALIDITY_DAYS * 24 * 60 * 60 * 1000);
-
- await tablesDB.updateRow(DATABASE_ID, TABLES.subscriptionPayments, payment.$id, {
- status: "success",
- processedAt: now.toISOString(),
- providerPayload: JSON.stringify({
- mock: true,
- confirmedBy: ctx.user.id,
- usedSavedCardId: useSavedCardId || undefined,
- }),
- });
-
- if (ctx.settings) {
- await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, {
- plan: payment.plan,
- planStartedAt: now.toISOString(),
- planExpiresAt: expires.toISOString(),
- lastPaymentId: payment.$id,
- });
- }
-
- if (saveCard && !useSavedCardId) {
- const last4 = String(formData.get("cardLast4") ?? "").replace(/\D/g, "").slice(-4);
- const month = parseInt(String(formData.get("cardExpiryMonth") ?? "0"), 10);
- const year = parseInt(String(formData.get("cardExpiryYear") ?? "0"), 10);
- const brand = String(formData.get("cardBrand") ?? "unknown");
- const holder = String(formData.get("cardHolder") ?? "").trim();
-
- if (last4.length === 4 && month > 0 && year >= 2026) {
- const existingDefault = await getDefaultCard(ctx.tenantId);
- try {
- await persistCardFromMockCheckout({
- brand,
- last4,
- expiryMonth: month,
- expiryYear: year,
- holderName: holder,
- makeDefault: !existingDefault,
- });
- } catch {
- // best-effort — payment already succeeded, don't fail upgrade if card save errors
- }
- }
- }
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "subscription_payment",
- entityId: payment.$id,
- changes: { status: "success", plan: payment.plan, expires: expires.toISOString() },
- });
-
- revalidatePath("/settings/billing");
- redirect(`/settings/billing?upgraded=1`);
-}
-
-export async function cancelMockPaymentAction(formData: FormData): Promise {
- const orderId = String(formData.get("orderId") ?? "");
- if (!orderId) throw new Error("orderId eksik.");
-
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const payment = await findPendingPaymentByOrderId(ctx.tenantId, orderId);
- if (!payment) {
- redirect(`/settings/billing`);
- }
- if (payment && payment.status === "pending") {
- const { tablesDB } = createAdminClient();
- await tablesDB.updateRow(DATABASE_ID, TABLES.subscriptionPayments, payment.$id, {
- status: "failed",
- processedAt: new Date().toISOString(),
- providerPayload: JSON.stringify({ mock: true, cancelledBy: ctx.user.id }),
- });
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "subscription_payment",
- entityId: payment.$id,
- changes: { status: "failed" },
- });
- }
-
- revalidatePath("/settings/billing");
- redirect(`/settings/billing?cancelled=1`);
-}
-
-export async function downgradeToFreeAction(): Promise {
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- if (!ctx.settings) throw new Error("Ayar yok.");
-
- const { tablesDB } = createAdminClient();
- await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, ctx.settings.$id, {
- plan: "free",
- planExpiresAt: null,
- });
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "update",
- entityType: "tenant_settings",
- entityId: ctx.settings.$id,
- changes: { plan: "free" },
- });
-
- revalidatePath("/settings/billing");
- redirect(`/settings/billing?downgraded=1`);
-}
-
-export async function startShopierCheckoutAction(formData: FormData): Promise {
- const plan = String(formData.get("plan") ?? "") as TenantPlan;
- if (plan !== "pro") throw new Error("Geçersiz plan.");
-
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const storeUrl = getShopierPlanUrl(plan);
- if (!storeUrl) throw new Error("Shopier mağaza URL'i ayarlanmamış.");
-
- const catalog = PLAN_CATALOG[plan];
- const orderId = generateOrderId();
-
- const { tablesDB } = createAdminClient();
- await tablesDB.createRow(
- DATABASE_ID,
- TABLES.subscriptionPayments,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- orderId,
- plan,
- amount: catalog.price,
- currency: catalog.currency,
- status: "pending",
- provider: "shopier",
- // Webhook'ta tenant eşleştirmek için alıcı emailini sakla
- providerPayload: JSON.stringify({ userEmail: ctx.user.email }),
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "subscription_payment",
- entityId: orderId,
- changes: { plan, amount: catalog.price, provider: "shopier" },
- });
-
- // Kullanıcıyı doğrudan Shopier mağaza ürün sayfasına yönlendir
- redirect(storeUrl);
-}
-
-export async function startPolarCheckoutAction(formData: FormData): Promise {
- const plan = String(formData.get("plan") ?? "") as TenantPlan;
- if (plan !== "pro") throw new Error("Geçersiz plan.");
-
- const ctx = await requireTenant();
- requireRole(ctx, ["owner"]);
-
- const catalog = PLAN_CATALOG[plan];
- const orderId = generateOrderId();
- const appUrl = process.env.APP_URL?.replace(/\/$/, "") ?? "http://localhost:3000";
-
- const { tablesDB } = createAdminClient();
- await tablesDB.createRow(
- DATABASE_ID,
- TABLES.subscriptionPayments,
- ID.unique(),
- {
- tenantId: ctx.tenantId,
- createdBy: ctx.user.id,
- orderId,
- plan,
- amount: catalog.price,
- currency: catalog.currency,
- status: "pending",
- provider: "polar",
- },
- teamRowPermissions(ctx.tenantId),
- );
-
- await logAudit({
- tenantId: ctx.tenantId,
- userId: ctx.user.id,
- action: "create",
- entityType: "subscription_payment",
- entityId: orderId,
- changes: { plan, amount: catalog.price, provider: "polar" },
- });
-
- const checkout = await createPolarCheckout({
- orderId,
- tenantId: ctx.tenantId,
- userEmail: ctx.user.email,
- successUrl: `${appUrl}/settings/billing?upgraded=1`,
- });
-
- redirect(checkout.url);
-}
-
-// Unified entry point — PAYMENT_PROVIDER env ile yönlendirir.
-export async function startCheckoutAction(formData: FormData): Promise