feat(settings): /settings/workspace — edit company info + invoice defaults
Owner/admin edit, member read-only.
Schema/validation:
- lib/validation/workspace.ts (workspaceSettingsSchema)
- lib/appwrite/workspace-actions.ts:
* updateWorkspaceSettingsAction — requireRole owner|admin, upserts the
tenant_settings row (creates one with team-scoped perms if absent,
e.g. for tenants created before tenant_settings was a table; just
defense). Audit-logged.
* Forces invoicePrefix to uppercase. defaultVatRate clamped to [0, 100].
* revalidatePath('/', 'layout') so the new company name updates in
sidebar header and dashboard greeting on next render.
UI:
- /settings/workspace page (server) — pulls active tenant settings
via requireTenant, shows form pre-filled.
- WorkspaceSettingsForm: 2 cards
* Şirket — name (required), tax id, phone, email, address
* Faturalama — invoicePrefix, defaultVatRate, read-only invoiceCounter
- All inputs disabled if user is a member (canEdit=false). Submit button
hidden in that case. Description on the page changes accordingly.
- Toast feedback for success/error.
Skipped: logo upload (storage bucket pending). Will revisit.
This commit is contained in:
@@ -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 (
|
||||
<form action={formAction} className="space-y-6">
|
||||
<fieldset disabled={!canEdit || isPending} className="space-y-6 disabled:opacity-90">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Building2 className="size-4" />
|
||||
Şirket
|
||||
</CardTitle>
|
||||
<CardDescription>Resmi şirket bilgileriniz.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-5 md:grid-cols-2">
|
||||
<div className="md:col-span-2 grid gap-2">
|
||||
<Label htmlFor="companyName">Şirket adı *</Label>
|
||||
<Input
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
defaultValue={defaults.companyName}
|
||||
required
|
||||
placeholder="Örn. Acme Yazılım Ltd. Şti."
|
||||
/>
|
||||
{state.fieldErrors?.companyName && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.companyName}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyTaxId">Vergi numarası</Label>
|
||||
<Input
|
||||
id="companyTaxId"
|
||||
name="companyTaxId"
|
||||
defaultValue={defaults.companyTaxId}
|
||||
inputMode="numeric"
|
||||
placeholder="1234567890"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyPhone">Telefon</Label>
|
||||
<Input
|
||||
id="companyPhone"
|
||||
name="companyPhone"
|
||||
type="tel"
|
||||
defaultValue={defaults.companyPhone}
|
||||
placeholder="+90 555 123 45 67"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyEmail">Email</Label>
|
||||
<Input
|
||||
id="companyEmail"
|
||||
name="companyEmail"
|
||||
type="email"
|
||||
defaultValue={defaults.companyEmail}
|
||||
placeholder="info@firma.com"
|
||||
/>
|
||||
{state.fieldErrors?.companyEmail && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.companyEmail}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="companyAddress">Adres</Label>
|
||||
<Textarea
|
||||
id="companyAddress"
|
||||
name="companyAddress"
|
||||
rows={2}
|
||||
defaultValue={defaults.companyAddress}
|
||||
placeholder="İl, ilçe, açık adres"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Receipt className="size-4" />
|
||||
Faturalama
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Yeni fatura oluştururken kullanılan varsayılanlar.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-5 md:grid-cols-3">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="invoicePrefix">Fatura ön eki</Label>
|
||||
<Input
|
||||
id="invoicePrefix"
|
||||
name="invoicePrefix"
|
||||
defaultValue={defaults.invoicePrefix}
|
||||
maxLength={10}
|
||||
placeholder="INV"
|
||||
style={{ textTransform: "uppercase" }}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Örn. <code>INV-2026-0001</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="defaultVatRate">Varsayılan KDV %</Label>
|
||||
<Input
|
||||
id="defaultVatRate"
|
||||
name="defaultVatRate"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="100"
|
||||
defaultValue={defaults.defaultVatRate}
|
||||
/>
|
||||
{state.fieldErrors?.defaultVatRate && (
|
||||
<p className="text-destructive text-xs">{state.fieldErrors.defaultVatRate}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Sayaç</Label>
|
||||
<div className="bg-muted/50 flex h-9 items-center rounded-md border px-3 text-sm tabular-nums">
|
||||
{defaults.invoiceCounter}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Bir sonraki fatura numarası: bu sayı + 1
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{canEdit && (
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
Kaydediliyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="size-4" />
|
||||
Kaydet
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-muted-foreground text-sm">{ctx.settings?.companyName ?? "Çalışma alanı"}</p>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Şirket bilgileri</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Faturalarda ve panel başlığında görünecek şirket bilgileri.
|
||||
{!canEdit && " Düzenlemek için yönetici yetkisine ihtiyacınız var."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<WorkspaceSettingsForm
|
||||
canEdit={canEdit}
|
||||
defaults={{
|
||||
companyName: ctx.settings?.companyName ?? "",
|
||||
companyTaxId: ctx.settings?.companyTaxId ?? "",
|
||||
companyAddress: ctx.settings?.companyAddress ?? "",
|
||||
companyEmail: ctx.settings?.companyEmail ?? "",
|
||||
companyPhone: ctx.settings?.companyPhone ?? "",
|
||||
defaultVatRate:
|
||||
typeof ctx.settings?.defaultVatRate === "number"
|
||||
? ctx.settings.defaultVatRate
|
||||
: 20,
|
||||
invoicePrefix: ctx.settings?.invoicePrefix ?? "INV",
|
||||
invoiceCounter: ctx.settings?.invoiceCounter ?? 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user