feat(backend): migrate freelancer and portal runtimes
This commit is contained in:
@@ -1,122 +1,72 @@
|
||||
"use server";
|
||||
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { cleanText, decimalToMinor, requiredText } from "@/server/web/form-data";
|
||||
import { requireFreelancerBackend } from "@/server/web/freelancer";
|
||||
|
||||
const TRANSACTION_TYPES = ["income", "expense"] as const;
|
||||
const PAYMENT_STATUSES = ["planned", "pending", "paid", "cancelled"] as const;
|
||||
const TYPES = ["income", "expense"] as const;
|
||||
const STATUSES = ["planned", "pending", "paid", "cancelled"] as const;
|
||||
|
||||
function cleanText(value: FormDataEntryValue | null) {
|
||||
const text = typeof value === "string" ? value.trim() : "";
|
||||
return text.length > 0 && text !== "__none" ? text : null;
|
||||
function enumValue<T extends readonly string[]>(value: FormDataEntryValue | null, values: T, fallback: T[number]): T[number] {
|
||||
return typeof value === "string" && values.includes(value) ? value as T[number] : fallback;
|
||||
}
|
||||
|
||||
function readType(value: FormDataEntryValue | null) {
|
||||
const type = typeof value === "string" ? value : "expense";
|
||||
return TRANSACTION_TYPES.includes(type as (typeof TRANSACTION_TYPES)[number])
|
||||
? type
|
||||
: "expense";
|
||||
}
|
||||
|
||||
function readPaymentStatus(value: FormDataEntryValue | null) {
|
||||
const status = typeof value === "string" ? value : "planned";
|
||||
return PAYMENT_STATUSES.includes(status as (typeof PAYMENT_STATUSES)[number])
|
||||
? status
|
||||
: "planned";
|
||||
}
|
||||
|
||||
function readAmount(value: FormDataEntryValue | null) {
|
||||
const amount = Number(typeof value === "string" ? value.replace(",", ".") : value);
|
||||
return Number.isFinite(amount) && amount >= 0 ? amount : null;
|
||||
}
|
||||
|
||||
async function getCurrentUserId() {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (error || !user) {
|
||||
throw new Error("Finans işlemi için giriş yapmış kullanıcı bulunamadı.");
|
||||
}
|
||||
|
||||
return { supabase, userId: user.id };
|
||||
}
|
||||
|
||||
function readPayload(formData: FormData) {
|
||||
function payload(formData: FormData) {
|
||||
const amountMinor = decimalToMinor(formData.get("amount"));
|
||||
if (amountMinor == null) throw new Error("Tutar zorunludur.");
|
||||
return {
|
||||
type: readType(formData.get("type")),
|
||||
amount: readAmount(formData.get("amount")),
|
||||
currency: cleanText(formData.get("currency")) || "USD",
|
||||
transaction_date: cleanText(formData.get("transaction_date")) || new Date().toISOString().slice(0, 10),
|
||||
type: enumValue(formData.get("type"), TYPES, "expense"),
|
||||
amountMinor,
|
||||
currency: cleanText(formData.get("currency")) ?? "USD",
|
||||
transactionDate: cleanText(formData.get("transaction_date")) ?? new Date().toISOString().slice(0, 10),
|
||||
category: cleanText(formData.get("category")),
|
||||
payment_status: readPaymentStatus(formData.get("payment_status")),
|
||||
client_id: cleanText(formData.get("client_id")),
|
||||
project_id: cleanText(formData.get("project_id")),
|
||||
paymentStatus: enumValue(formData.get("payment_status"), STATUSES, "planned"),
|
||||
clientId: cleanText(formData.get("client_id")),
|
||||
projectId: cleanText(formData.get("project_id")),
|
||||
description: cleanText(formData.get("description")),
|
||||
};
|
||||
}
|
||||
|
||||
function completeRelations(
|
||||
value: ReturnType<typeof payload>,
|
||||
service: Awaited<ReturnType<typeof requireFreelancerBackend>>["service"],
|
||||
actor: Awaited<ReturnType<typeof requireFreelancerBackend>>["actor"],
|
||||
) {
|
||||
const project = value.projectId ? service.getProject(actor, value.projectId) : null;
|
||||
return { ...value, clientId: value.clientId ?? project?.clientId ?? null };
|
||||
}
|
||||
|
||||
export async function createFinanceTransactionRecord(formData: FormData) {
|
||||
const { supabase, userId } = await getCurrentUserId();
|
||||
const payload = readPayload(formData);
|
||||
|
||||
if (payload.amount === null) {
|
||||
throw new Error("Tutar zorunludur.");
|
||||
}
|
||||
|
||||
const { error } = await supabase.from("finance_transactions").insert({
|
||||
user_id: userId,
|
||||
...payload,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Finans işlemi eklenemedi: ${error.message}`);
|
||||
}
|
||||
|
||||
const backend = await requireFreelancerBackend();
|
||||
backend.service.createFinanceTransaction(
|
||||
backend.actor,
|
||||
completeRelations(payload(formData), backend.service, backend.actor),
|
||||
);
|
||||
revalidatePath("/finance");
|
||||
revalidatePath("/clients");
|
||||
revalidatePath("/projects");
|
||||
}
|
||||
|
||||
export async function updateFinanceTransactionRecord(formData: FormData) {
|
||||
const { supabase, userId } = await getCurrentUserId();
|
||||
const id = cleanText(formData.get("id"));
|
||||
const payload = readPayload(formData);
|
||||
|
||||
if (!id || payload.amount === null) {
|
||||
throw new Error("Finans işlemini güncellemek için kayıt kimliği ve tutar zorunludur.");
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("finance_transactions")
|
||||
.update(payload)
|
||||
.eq("id", id)
|
||||
.eq("user_id", userId);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Finans işlemi güncellenemedi: ${error.message}`);
|
||||
}
|
||||
|
||||
const backend = await requireFreelancerBackend();
|
||||
const id = requiredText(formData.get("id"), "Finans kaydı bulunamadı.");
|
||||
backend.service.updateFinanceTransaction(
|
||||
backend.actor,
|
||||
id,
|
||||
completeRelations(payload(formData), backend.service, backend.actor),
|
||||
);
|
||||
revalidatePath("/finance");
|
||||
revalidatePath("/clients");
|
||||
revalidatePath("/projects");
|
||||
}
|
||||
|
||||
export async function deleteFinanceTransactionRecord(formData: FormData) {
|
||||
const { supabase, userId } = await getCurrentUserId();
|
||||
const id = cleanText(formData.get("id"));
|
||||
|
||||
if (!id) {
|
||||
throw new Error("Silinecek finans işlemi bulunamadı.");
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from("finance_transactions")
|
||||
.delete()
|
||||
.eq("id", id)
|
||||
.eq("user_id", userId);
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Finans işlemi silinemedi: ${error.message}`);
|
||||
}
|
||||
|
||||
const { actor, service } = await requireFreelancerBackend();
|
||||
service.deleteFinanceTransaction(
|
||||
actor,
|
||||
requiredText(formData.get("id"), "Silinecek finans kaydı bulunamadı."),
|
||||
);
|
||||
revalidatePath("/finance");
|
||||
revalidatePath("/clients");
|
||||
revalidatePath("/projects");
|
||||
}
|
||||
|
||||
@@ -1,93 +1,34 @@
|
||||
import {
|
||||
FinanceClient,
|
||||
type FinanceRelationOption,
|
||||
type FinanceTransactionItem,
|
||||
} from "@/app/(dashboard)/finance/finance-client";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
|
||||
type FinanceRow = {
|
||||
id: string;
|
||||
type: "income" | "expense";
|
||||
amount: number | string;
|
||||
currency: string;
|
||||
transaction_date: string;
|
||||
category: string | null;
|
||||
payment_status: "planned" | "pending" | "paid" | "cancelled";
|
||||
client_id: string | null;
|
||||
project_id: string | null;
|
||||
description: string | null;
|
||||
clients: { name: string } | { name: string }[] | null;
|
||||
projects: { name: string } | { name: string }[] | null;
|
||||
};
|
||||
import { FinanceClient, type FinanceRelationOption, type FinanceTransactionItem } from "@/app/(dashboard)/finance/finance-client";
|
||||
import { requireFreelancerBackend } from "@/server/web/freelancer";
|
||||
|
||||
export default async function FinancePage() {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
const { actor, service } = await requireFreelancerBackend();
|
||||
const rows = service.listFinanceTransactions(actor);
|
||||
const clientRows = service.listClients(actor);
|
||||
const projectRows = service.listProjects(actor);
|
||||
const clients = new Map(clientRows.map((item) => [item.id, item.name]));
|
||||
const projects = new Map(projectRows.map((item) => [item.id, item.name]));
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [{ data: financeRows }, { data: clientRows }, { data: projectRows }] =
|
||||
await Promise.all([
|
||||
supabase
|
||||
.from("finance_transactions")
|
||||
.select("id, type, amount, currency, transaction_date, category, payment_status, client_id, project_id, description, clients(name), projects(name)")
|
||||
.eq("user_id", user.id)
|
||||
.order("transaction_date", { ascending: false }),
|
||||
supabase
|
||||
.from("clients")
|
||||
.select("id, name")
|
||||
.eq("user_id", user.id)
|
||||
.neq("status", "archived")
|
||||
.order("name", { ascending: true }),
|
||||
supabase
|
||||
.from("projects")
|
||||
.select("id, name, client_id")
|
||||
.eq("user_id", user.id)
|
||||
.neq("status", "cancelled")
|
||||
.order("name", { ascending: true }),
|
||||
]);
|
||||
|
||||
const transactions: FinanceTransactionItem[] = ((financeRows || []) as unknown as FinanceRow[]).map((transaction) => ({
|
||||
const transactions: FinanceTransactionItem[] = rows.map((transaction) => ({
|
||||
id: transaction.id,
|
||||
type: normalizeType(transaction.type),
|
||||
amount: Number(transaction.amount),
|
||||
type: transaction.type,
|
||||
amount: transaction.amountMinor / 100,
|
||||
currency: transaction.currency,
|
||||
transaction_date: transaction.transaction_date,
|
||||
transaction_date: transaction.transactionDate,
|
||||
category: transaction.category,
|
||||
payment_status: normalizePaymentStatus(transaction.payment_status),
|
||||
client_id: transaction.client_id,
|
||||
project_id: transaction.project_id,
|
||||
clientName: getRelationName(transaction.clients),
|
||||
projectName: getRelationName(transaction.projects),
|
||||
payment_status: transaction.paymentStatus,
|
||||
client_id: transaction.clientId,
|
||||
project_id: transaction.projectId,
|
||||
clientName: transaction.clientId ? clients.get(transaction.clientId) ?? null : null,
|
||||
projectName: transaction.projectId ? projects.get(transaction.projectId) ?? null : null,
|
||||
description: transaction.description,
|
||||
}));
|
||||
const clientOptions: FinanceRelationOption[] = clientRows
|
||||
.filter((item) => item.status !== "archived")
|
||||
.map(({ id, name }) => ({ id, name }));
|
||||
const projectOptions: FinanceRelationOption[] = projectRows
|
||||
.filter((item) => item.status !== "cancelled")
|
||||
.map(({ id, name, clientId }) => ({ id, name, client_id: clientId }));
|
||||
|
||||
return (
|
||||
<FinanceClient
|
||||
transactions={transactions}
|
||||
clients={(clientRows || []) as FinanceRelationOption[]}
|
||||
projects={(projectRows || []) as FinanceRelationOption[]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getRelationName(relation: FinanceRow["clients"] | FinanceRow["projects"]) {
|
||||
if (!relation) return null;
|
||||
return Array.isArray(relation) ? relation[0]?.name || null : relation.name;
|
||||
}
|
||||
|
||||
function normalizeType(type: string): FinanceTransactionItem["type"] {
|
||||
return type === "income" ? "income" : "expense";
|
||||
}
|
||||
|
||||
function normalizePaymentStatus(status: string): FinanceTransactionItem["payment_status"] {
|
||||
if (status === "pending" || status === "paid" || status === "cancelled") {
|
||||
return status;
|
||||
}
|
||||
|
||||
return "planned";
|
||||
return <FinanceClient transactions={transactions} clients={clientOptions} projects={projectOptions} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user