Files
isletmem-kovakcrm/src/app/(dashboard)/software/components/assignment-form-sheet.tsx
T
kovakmedya 1299cd10ce feat: fatura PDF, hizmet/yazılım atama dosya ekleri
- /print/invoices/[id] sayfası: A4 fatura yazdırma/PDF (AutoPrint + PrintActionBar)
- Fatura detayı header'ına PDF butonu eklendi (Yazdır yerine)
- Appwrite Storage: entity-attachments bucket (20MB, şifreli)
- Appwrite Tables: attachments collection (tenantId, entityType, entityId, fileId, name, size, mimeType)
- attachment-actions.ts: fetchAttachmentsAction, uploadAttachmentAction, deleteAttachmentAction
- AttachmentsPanel bileşeni: dosya yükleme/listeleme/silme, edit modunda görünür
- Hizmet ve yazılım atama form sheet'lerine AttachmentsPanel entegrasyonu
- /api/files/[attachmentId]: güvenli proxy indirme (tenant doğrulama + admin key ile Appwrite'a istek)
2026-05-07 20:22:17 +03:00

246 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 { AttachmentsPanel } from "@/components/attachments-panel";
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<string>(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 (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="flex w-full flex-col gap-0 p-0 sm:max-w-xl">
<SheetHeader className="border-b px-6 py-4">
<SheetTitle>
{isEdit ? "Atamayı düzenle" : "Müşteriye yazılım ata"}
</SheetTitle>
<SheetDescription>
{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."}
</SheetDescription>
</SheetHeader>
<form action={formAction} className="flex flex-1 flex-col">
{isEdit && assignment && <input type="hidden" name="id" value={assignment.id} />}
<div className="flex-1 space-y-4 overflow-y-auto px-6 py-5">
<div className="grid gap-2">
<Label htmlFor="customerId">Müşteri *</Label>
<Select
name="customerId"
defaultValue={assignment?.customerId ?? ""}
disabled={blocked}
>
<SelectTrigger id="customerId">
<SelectValue placeholder="Müşteri seçin" />
</SelectTrigger>
<SelectContent>
{customers.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
{state.fieldErrors?.customerId && (
<p className="text-destructive text-xs">{state.fieldErrors.customerId}</p>
)}
</div>
<div className="grid gap-2">
<Label htmlFor="softwareId">Yazılım *</Label>
<Select
name="softwareId"
value={selectedSoftware}
onValueChange={setSelectedSoftware}
disabled={blocked}
>
<SelectTrigger id="softwareId">
<SelectValue placeholder="Yazılım seçin" />
</SelectTrigger>
<SelectContent>
{softwareOptions.map((s) => (
<SelectItem key={s.id} value={s.id}>
{s.name}
{s.version ? ` v${s.version}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
{state.fieldErrors?.softwareId && (
<p className="text-destructive text-xs">{state.fieldErrors.softwareId}</p>
)}
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="fee">Bu müşteri için ücret ()</Label>
<Input
id="fee"
name="fee"
type="number"
step="0.01"
min="0"
defaultValue={assignment?.fee ?? defaultFee ?? ""}
placeholder={defaultFee !== "" ? `Varsayılan: ${defaultFee}` : "0.00"}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="billingPeriod">Faturalama dönemi</Label>
<Select
name="billingPeriod"
defaultValue={assignment?.billingPeriod ?? "monthly"}
>
<SelectTrigger id="billingPeriod">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monthly">Aylık</SelectItem>
<SelectItem value="yearly">Yıllık</SelectItem>
<SelectItem value="onetime">Tek seferlik</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="startDate">Başlangıç</Label>
<Input
id="startDate"
name="startDate"
type="date"
defaultValue={isoToInputDate(assignment?.startDate ?? "")}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="endDate">Bitiş (opsiyonel)</Label>
<Input
id="endDate"
name="endDate"
type="date"
defaultValue={isoToInputDate(assignment?.endDate ?? "")}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="notes">Notlar</Label>
<Textarea
id="notes"
name="notes"
rows={3}
defaultValue={assignment?.notes ?? ""}
placeholder="Lisans bilgileri, özel koşullar"
/>
</div>
{/* Ekler */}
{isEdit && assignment && (
<AttachmentsPanel
entityType="customer_software"
entityId={assignment.id}
/>
)}
</div>
<SheetFooter className="border-t bg-muted/30 px-6 pt-4">
<div className="flex w-full justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isPending}
>
Vazgeç
</Button>
<Button type="submit" disabled={isPending || blocked}>
{isPending ? (
<>
<Loader2 className="size-4 animate-spin" />
Kaydediliyor...
</>
) : (
<>
<Save className="size-4" />
{isEdit ? "Güncelle" : "Ata"}
</>
)}
</Button>
</div>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}