feat(backend): migrate freelancer and portal runtimes

This commit is contained in:
poyrazavsever
2026-07-17 00:16:38 +03:00
parent 561af11b70
commit 678c0236db
41 changed files with 5293 additions and 2324 deletions
+56 -161
View File
@@ -1,193 +1,88 @@
"use server";
import { createClient } from "@/lib/supabase/server";
import { revalidatePath } from "next/cache";
import { cleanText, optionalDate, requiredText } from "@/server/web/form-data";
import { requireFreelancerBackend } from "@/server/web/freelancer";
const TASK_STATUSES = ["todo", "in_progress", "done"] as const;
const TASK_PRIORITIES = ["low", "medium", "high", "urgent"] as const;
function cleanText(value: FormDataEntryValue | null) {
const text = typeof value === "string" ? value.trim() : "";
return text.length > 0 ? text : null;
function enumValue<T extends readonly string[]>(value: FormDataEntryValue | string | null, values: T, fallback: T[number]): T[number] {
return typeof value === "string" && values.includes(value) ? value as T[number] : fallback;
}
function cleanRelationId(value: FormDataEntryValue | null) {
const id = cleanText(value);
return id && id !== "__none" ? id : null;
function minutes(value: FormDataEntryValue | null): number | null {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed) : null;
}
function readStatus(value: FormDataEntryValue | null) {
const status = typeof value === "string" ? value : "todo";
return TASK_STATUSES.includes(status as (typeof TASK_STATUSES)[number])
? status
: "todo";
}
function readPriority(value: FormDataEntryValue | null) {
const priority = typeof value === "string" ? value : "medium";
return TASK_PRIORITIES.includes(priority as (typeof TASK_PRIORITIES)[number])
? priority
: "medium";
}
function readMinutes(value: FormDataEntryValue | null) {
const number = Number(value);
return Number.isFinite(number) && number >= 0 ? Math.round(number) : null;
}
async function getCurrentUserId() {
const supabase = await createClient();
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error || !user) {
throw new Error("Görev işlemi için giriş yapmış kullanıcı bulunamadı.");
}
return { supabase, userId: user.id };
}
function readPayload(formData: FormData) {
function payload(formData: FormData) {
const dueAt = optionalDate(formData.get("due_at"));
return {
title: cleanText(formData.get("title")),
title: requiredText(formData.get("title"), "Görev başlığı zorunludur."),
description: cleanText(formData.get("description")),
status: readStatus(formData.get("status")),
priority: readPriority(formData.get("priority")),
client_id: cleanRelationId(formData.get("client_id")),
project_id: cleanRelationId(formData.get("project_id")),
due_at: cleanText(formData.get("due_at")),
estimated_minutes: readMinutes(formData.get("estimated_minutes")),
actual_minutes: readMinutes(formData.get("actual_minutes")),
is_public_to_client: formData.get("is_public_to_client") === "on",
status: enumValue(formData.get("status"), TASK_STATUSES, "todo"),
priority: enumValue(formData.get("priority"), TASK_PRIORITIES, "medium"),
clientId: cleanText(formData.get("client_id")),
projectId: cleanText(formData.get("project_id")),
scheduledDate: dueAt?.toISOString().slice(0, 10) ?? null,
dueAt,
estimatedMinutes: minutes(formData.get("estimated_minutes")),
actualMinutes: minutes(formData.get("actual_minutes")),
isPublicToClient: formData.get("is_public_to_client") === "on",
};
}
export async function createTaskRecord(formData: FormData) {
const { supabase, userId } = await getCurrentUserId();
const payload = readPayload(formData);
if (!payload.title) {
throw new Error("Görev başlığı zorunludur.");
}
const { error } = await supabase.from("tasks").insert({
user_id: userId,
date: payload.due_at || new Date().toISOString(),
...payload,
});
if (error) {
throw new Error(`Görev eklenemedi: ${error.message}`);
}
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 };
}
function revalidate(projectId?: string | null) {
revalidatePath("/tasks");
revalidatePath("/projects");
if (projectId) revalidatePath(`/projects/${projectId}`);
}
if (payload.project_id) {
revalidatePath(`/projects/${payload.project_id}`);
}
export async function createTaskRecord(formData: FormData) {
const { actor, service } = await requireFreelancerBackend();
const value = completeRelations(payload(formData), service, actor);
service.createTask(actor, value);
revalidate(value.projectId);
}
export async function updateTaskRecord(formData: FormData) {
const { supabase, userId } = await getCurrentUserId();
const id = cleanText(formData.get("id"));
const payload = readPayload(formData);
if (!id || !payload.title) {
throw new Error("Görev güncellemek için başlık ve kayıt kimliği zorunludur.");
}
const { error } = await supabase
.from("tasks")
.update(payload)
.eq("id", id)
.eq("user_id", userId);
if (error) {
throw new Error(`Görev güncellenemedi: ${error.message}`);
}
revalidatePath("/tasks");
if (payload.project_id) {
revalidatePath(`/projects/${payload.project_id}`);
}
const { actor, service } = await requireFreelancerBackend();
const id = requiredText(formData.get("id"), "Görev kaydı bulunamadı.");
const value = completeRelations(payload(formData), service, actor);
const current = service.listTasks(actor).find((task) => task.id === id);
service.updateTask(actor, id, value);
revalidate(value.projectId);
if (current?.projectId !== value.projectId) revalidate(current?.projectId);
}
export async function completeTaskRecord(formData: FormData) {
const { supabase, userId } = await getCurrentUserId();
const id = cleanText(formData.get("id"));
const projectId = cleanRelationId(formData.get("project_id"));
if (!id) {
throw new Error("Tamamlanacak görev bulunamadı.");
}
const { error } = await supabase
.from("tasks")
.update({ status: "done" })
.eq("id", id)
.eq("user_id", userId);
if (error) {
throw new Error(`Görev tamamlanamadı: ${error.message}`);
}
revalidatePath("/tasks");
if (projectId) {
revalidatePath(`/projects/${projectId}`);
}
const id = requiredText(formData.get("id"), "Tamamlanacak görev bulunamadı.");
const projectId = cleanText(formData.get("project_id"));
const { actor, service } = await requireFreelancerBackend();
service.updateTask(actor, id, { status: "done" });
revalidate(projectId);
}
export async function updateTaskStatusRecord(taskId: string, status: string, projectId?: string) {
const { supabase, userId } = await getCurrentUserId();
const nextStatus = readStatus(status);
if (!taskId) {
throw new Error("Durumu güncellenecek görev bulunamadı.");
}
const { error } = await supabase
.from("tasks")
.update({ status: nextStatus })
.eq("id", taskId)
.eq("user_id", userId);
if (error) {
throw new Error(`Görev durumu güncellenemedi: ${error.message}`);
}
revalidatePath("/tasks");
if (projectId) {
revalidatePath(`/projects/${projectId}`);
}
const { actor, service } = await requireFreelancerBackend();
service.updateTask(actor, taskId, { status: enumValue(status, TASK_STATUSES, "todo") });
revalidate(projectId);
}
export async function deleteTaskRecord(formData: FormData) {
const { supabase, userId } = await getCurrentUserId();
const id = cleanText(formData.get("id"));
const projectId = cleanRelationId(formData.get("project_id"));
if (!id) {
throw new Error("Silinecek görev bulunamadı.");
}
const { error } = await supabase
.from("tasks")
.delete()
.eq("id", id)
.eq("user_id", userId);
if (error) {
throw new Error(`Görev silinemedi: ${error.message}`);
}
revalidatePath("/tasks");
if (projectId) {
revalidatePath(`/projects/${projectId}`);
}
const id = requiredText(formData.get("id"), "Silinecek görev bulunamadı.");
const projectId = cleanText(formData.get("project_id"));
const { actor, service } = await requireFreelancerBackend();
service.deleteTask(actor, id);
revalidate(projectId);
}
+31 -87
View File
@@ -1,93 +1,37 @@
import {
TasksClient,
type TaskListItem,
type TaskRelationOption,
} from "@/app/(dashboard)/tasks/tasks-client";
import { createClient } from "@/lib/supabase/server";
type TaskRow = {
id: string;
title: string;
description: string | null;
status: "todo" | "in_progress" | "done";
priority: "low" | "medium" | "high" | "urgent";
due_at: string | null;
estimated_minutes: number | null;
actual_minutes: number | null;
client_id: string | null;
project_id: string | null;
created_at: string;
clients: { name: string } | { name: string }[] | null;
projects: { name: string } | { name: string }[] | null;
};
import { TasksClient, type TaskListItem, type TaskRelationOption } from "@/app/(dashboard)/tasks/tasks-client";
import { requireFreelancerBackend } from "@/server/web/freelancer";
export default async function TasksPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
const { actor, service } = await requireFreelancerBackend();
const taskRows = service.listTasks(actor);
const clientRows = service.listClients(actor);
const projectRows = service.listProjects(actor);
const clientNames = new Map(clientRows.map((item) => [item.id, item.name]));
const projectNames = new Map(projectRows.map((item) => [item.id, item.name]));
if (!user) {
return null;
}
const [{ data: taskRows }, { data: clientRows }, { data: projectRows }] =
await Promise.all([
supabase
.from("tasks")
.select(
"id, title, description, status, priority, due_at, estimated_minutes, actual_minutes, client_id, project_id, created_at, clients(name), projects(name)",
)
.eq("user_id", user.id)
.order("created_at", { 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 clients = (clientRows || []) as TaskRelationOption[];
const projects = (projectRows || []) as TaskRelationOption[];
const tasks: TaskListItem[] = ((taskRows || []) as unknown as TaskRow[]).map((task) => ({
id: task.id,
title: task.title,
description: task.description,
status: normalizeStatus(task.status),
priority: normalizePriority(task.priority),
due_at: task.due_at,
estimated_minutes: task.estimated_minutes,
actual_minutes: task.actual_minutes,
client_id: task.client_id,
clientName: getRelationName(task.clients),
project_id: task.project_id,
projectName: getRelationName(task.projects),
created_at: task.created_at,
}));
const tasks: TaskListItem[] = taskRows
.filter((task) => task.status !== "cancelled")
.map((task) => ({
id: task.id,
title: task.title,
description: task.description,
status: task.status as TaskListItem["status"],
priority: task.priority,
due_at: task.dueAt?.toISOString() ?? null,
estimated_minutes: task.estimatedMinutes,
actual_minutes: task.actualMinutes,
client_id: task.clientId,
clientName: task.clientId ? clientNames.get(task.clientId) ?? null : null,
project_id: task.projectId,
projectName: task.projectId ? projectNames.get(task.projectId) ?? null : null,
created_at: task.createdAt.toISOString(),
}));
const clients: TaskRelationOption[] = clientRows
.filter((client) => client.status !== "archived")
.map(({ id, name }) => ({ id, name }));
const projects: TaskRelationOption[] = projectRows
.filter((project) => project.status !== "cancelled")
.map(({ id, name, clientId }) => ({ id, name, client_id: clientId }));
return <TasksClient tasks={tasks} clients={clients} projects={projects} />;
}
function getRelationName(relation: TaskRow["clients"] | TaskRow["projects"]) {
if (!relation) return null;
return Array.isArray(relation) ? relation[0]?.name || null : relation.name;
}
function normalizeStatus(status: string): TaskListItem["status"] {
return status === "in_progress" || status === "done" ? status : "todo";
}
function normalizePriority(priority: string): TaskListItem["priority"] {
if (priority === "low" || priority === "high" || priority === "urgent") {
return priority;
}
return "medium";
}