diff --git a/src/app/(dashboard)/settings/workspace/components/workspace-form.tsx b/src/app/(dashboard)/settings/workspace/components/workspace-form.tsx
new file mode 100644
index 0000000..7e6a758
--- /dev/null
+++ b/src/app/(dashboard)/settings/workspace/components/workspace-form.tsx
@@ -0,0 +1,192 @@
+"use client";
+
+import { useActionState, useEffect } from "react";
+import { Building2, Loader2, Receipt, Save } from "lucide-react";
+import { toast } from "sonner";
+
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { updateWorkspaceSettingsAction } from "@/lib/appwrite/workspace-actions";
+import { initialWorkspaceSettingsState } from "@/lib/appwrite/workspace-types";
+
+type Defaults = {
+ companyName: string;
+ companyTaxId: string;
+ companyAddress: string;
+ companyEmail: string;
+ companyPhone: string;
+ defaultVatRate: number;
+ invoicePrefix: string;
+ invoiceCounter: number;
+};
+
+export function WorkspaceSettingsForm({
+ canEdit,
+ defaults,
+}: {
+ canEdit: boolean;
+ defaults: Defaults;
+}) {
+ const [state, formAction, isPending] = useActionState(
+ updateWorkspaceSettingsAction,
+ initialWorkspaceSettingsState,
+ );
+
+ useEffect(() => {
+ if (state.ok) toast.success("Bilgiler güncellendi.");
+ else if (state.error) toast.error(state.error);
+ }, [state]);
+
+ return (
+
+ );
+}
diff --git a/src/app/(dashboard)/settings/workspace/page.tsx b/src/app/(dashboard)/settings/workspace/page.tsx
new file mode 100644
index 0000000..4567287
--- /dev/null
+++ b/src/app/(dashboard)/settings/workspace/page.tsx
@@ -0,0 +1,50 @@
+import type { Metadata } from "next";
+import { redirect } from "next/navigation";
+
+import { requireTenant } from "@/lib/appwrite/tenant-guard";
+import { WorkspaceSettingsForm } from "./components/workspace-form";
+
+export const metadata: Metadata = {
+ title: "İşletmem — Şirket bilgileri",
+};
+
+export default async function WorkspaceSettingsPage() {
+ let ctx;
+ try {
+ ctx = await requireTenant();
+ } catch {
+ redirect("/onboarding");
+ }
+
+ const canEdit = ctx.role === "owner" || ctx.role === "admin";
+
+ return (
+
+
+
{ctx.settings?.companyName ?? "Çalışma alanı"}
+
Şirket bilgileri
+
+ Faturalarda ve panel başlığında görünecek şirket bilgileri.
+ {!canEdit && " Düzenlemek için yönetici yetkisine ihtiyacınız var."}
+
+
+
+
+
+ );
+}
diff --git a/src/lib/appwrite/workspace-actions.ts b/src/lib/appwrite/workspace-actions.ts
new file mode 100644
index 0000000..05cfab9
--- /dev/null
+++ b/src/lib/appwrite/workspace-actions.ts
@@ -0,0 +1,106 @@
+"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 TenantSettings } from "./schema";
+import { createAdminClient } from "./server";
+import { requireRole, requireTenant } from "./tenant-guard";
+import type { WorkspaceSettingsState } from "./workspace-types";
+import { workspaceSettingsSchema } from "@/lib/validation/workspace";
+
+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 {
+ companyName: String(formData.get("companyName") ?? "").trim(),
+ companyTaxId: String(formData.get("companyTaxId") ?? "").trim(),
+ companyAddress: String(formData.get("companyAddress") ?? "").trim(),
+ companyEmail: String(formData.get("companyEmail") ?? "").trim(),
+ companyPhone: String(formData.get("companyPhone") ?? "").trim(),
+ defaultVatRate: String(formData.get("defaultVatRate") ?? "20"),
+ invoicePrefix: String(formData.get("invoicePrefix") ?? "").trim(),
+ };
+}
+
+export async function updateWorkspaceSettingsAction(
+ _prev: WorkspaceSettingsState,
+ formData: FormData,
+): Promise {
+ let ctx;
+ try {
+ ctx = await requireTenant();
+ requireRole(ctx, ["owner", "admin"]);
+ } catch {
+ return { ok: false, error: "Düzenleme yetkiniz yok." };
+ }
+
+ const parsed = workspaceSettingsSchema.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.listRows({
+ databaseId: DATABASE_ID,
+ tableId: TABLES.tenantSettings,
+ queries: [Query.equal("tenantId", ctx.tenantId), Query.limit(1)],
+ });
+ const row = existing.rows[0] as unknown as TenantSettings | undefined;
+
+ if (row) {
+ await tablesDB.updateRow(DATABASE_ID, TABLES.tenantSettings, row.$id, parsed.data);
+ await logAudit({
+ tenantId: ctx.tenantId,
+ userId: ctx.user.id,
+ action: "update",
+ entityType: "tenant_settings",
+ entityId: row.$id,
+ changes: parsed.data,
+ });
+ } else {
+ const created = await tablesDB.createRow(
+ DATABASE_ID,
+ TABLES.tenantSettings,
+ ID.unique(),
+ { tenantId: ctx.tenantId, ...parsed.data },
+ [
+ Permission.read(Role.team(ctx.tenantId)),
+ Permission.update(Role.team(ctx.tenantId, "owner")),
+ Permission.update(Role.team(ctx.tenantId, "admin")),
+ Permission.delete(Role.team(ctx.tenantId, "owner")),
+ ],
+ );
+ await logAudit({
+ tenantId: ctx.tenantId,
+ userId: ctx.user.id,
+ action: "create",
+ entityType: "tenant_settings",
+ entityId: created.$id,
+ changes: parsed.data,
+ });
+ }
+ } catch (e) {
+ return { ok: false, error: appwriteError(e) };
+ }
+
+ // Tenant name shows up everywhere — revalidate broadly
+ revalidatePath("/", "layout");
+ return { ok: true };
+}
diff --git a/src/lib/appwrite/workspace-types.ts b/src/lib/appwrite/workspace-types.ts
new file mode 100644
index 0000000..f2bf218
--- /dev/null
+++ b/src/lib/appwrite/workspace-types.ts
@@ -0,0 +1,7 @@
+export type WorkspaceSettingsState = {
+ ok: boolean;
+ error?: string;
+ fieldErrors?: Record;
+};
+
+export const initialWorkspaceSettingsState: WorkspaceSettingsState = { ok: false };
diff --git a/src/lib/validation/workspace.ts b/src/lib/validation/workspace.ts
new file mode 100644
index 0000000..52611bb
--- /dev/null
+++ b/src/lib/validation/workspace.ts
@@ -0,0 +1,44 @@
+import { z } from "zod";
+
+export const workspaceSettingsSchema = z.object({
+ companyName: z.string().trim().min(1, "Şirket adı zorunlu.").max(255),
+ companyTaxId: z
+ .string()
+ .trim()
+ .max(50)
+ .optional()
+ .transform((v) => (v ? v : undefined)),
+ companyAddress: z
+ .string()
+ .trim()
+ .max(500)
+ .optional()
+ .transform((v) => (v ? v : undefined)),
+ companyEmail: z
+ .union([z.string().email("Geçerli bir email girin."), z.literal("")])
+ .optional()
+ .transform((v) => (v ? v : undefined)),
+ companyPhone: z
+ .string()
+ .trim()
+ .max(30)
+ .optional()
+ .transform((v) => (v ? v : undefined)),
+ defaultVatRate: z
+ .union([z.number(), z.string()])
+ .optional()
+ .transform((v) => {
+ if (v === undefined || v === "") return 20;
+ const n = typeof v === "string" ? Number(v.replace(",", ".")) : v;
+ return Number.isFinite(n) ? n : 20;
+ })
+ .pipe(z.number().min(0, "Negatif olamaz.").max(100, "100'den büyük olamaz.")),
+ invoicePrefix: z
+ .string()
+ .trim()
+ .max(10)
+ .optional()
+ .transform((v) => (v ? v.toUpperCase() : "INV")),
+});
+
+export type WorkspaceSettingsInput = z.infer;