Files
neta/app/(dashboard)/projects/actions.ts
T

173 lines
8.1 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 server";
import { randomUUID } from "node:crypto";
import { revalidatePath } from "next/cache";
import { getFileService } from "@/server/files/runtime";
import { getSqliteConnection } from "@/server/db/client";
import {
ContentTranslationService,
parseContentTranslationsFromFormData,
} from "@/server/i18n/content";
import { cleanText, decimalToMinor, requiredText } from "@/server/web/form-data";
import { requireFreelancerBackend } from "@/server/web/freelancer";
const PROJECT_TYPES = ["client_project", "side_project"] as const;
const PROJECT_STATUSES = ["planning", "active", "paused", "completed", "cancelled"] as const;
const SECTION_CATEGORIES = ["overview", "problem", "goal", "audience", "scope", "design_system", "color_palette", "typography", "assets", "notes"] as const;
const REVISION_STATUSES = ["pending", "in_progress", "completed", "rejected"] as const;
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 numberValue(value: FormDataEntryValue | null, fallback = 0) {
const parsed = Number(typeof value === "string" ? value.replace(",", ".") : value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function projectPayload(formData: FormData, translations?: Record<string, Record<string, string | null>>, defaultLocale = "tr") {
const type = enumValue(formData.get("type"), PROJECT_TYPES, "client_project");
const localized = translations?.[defaultLocale] ?? {};
return {
name: localized.name ?? requiredText(formData.get("name"), "Proje adı zorunludur."),
type,
clientId: type === "client_project" ? cleanText(formData.get("client_id")) : null,
description: localized.description ?? cleanText(formData.get("description")),
status: enumValue(formData.get("status"), PROJECT_STATUSES, "planning"),
startDate: cleanText(formData.get("start_date")),
dueDate: cleanText(formData.get("due_date")),
budgetAmountMinor: decimalToMinor(formData.get("budget_amount")),
currency: cleanText(formData.get("currency")) ?? "USD",
progress: Math.min(100, Math.max(0, Math.round(numberValue(formData.get("progress"))))),
coverImageAlt: localized.coverImageAlt ?? cleanText(formData.get("cover_image_alt")),
};
}
async function uploadCover(
actor: Parameters<ReturnType<typeof getFileService>["upload"]>[0],
projectId: string,
formData: FormData,
) {
const file = formData.get("cover_image");
if (!(file instanceof File) || file.size === 0) return null;
const stored = getFileService().upload(actor, {
kind: "project_asset",
originalName: file.name,
claimedMimeType: file.type,
bytes: new Uint8Array(await file.arrayBuffer()),
projectId,
portalVisible: true,
});
return `/api/files/${stored.id}`;
}
export async function createProjectRecord(formData: FormData) {
const { actor, service } = await requireFreelancerBackend();
const i18n = new ContentTranslationService(getSqliteConnection().db);
const context = i18n.getLocalizationContext(actor);
const translations = parseContentTranslationsFromFormData(formData, "project", context);
const id = randomUUID();
service.createProject(actor, { id, ...projectPayload(formData, translations, context.defaultLocale), translations });
try {
const cover = await uploadCover(actor, id, formData);
if (cover) service.updateProject(actor, id, { legacyCoverImagePath: cover });
} catch (error) {
service.deleteProject(actor, id);
throw error;
}
revalidatePath("/projects");
}
export async function updateProjectRecord(formData: FormData) {
const { actor, service } = await requireFreelancerBackend();
const i18n = new ContentTranslationService(getSqliteConnection().db);
const context = i18n.getLocalizationContext(actor);
const translations = parseContentTranslationsFromFormData(formData, "project", context);
const id = requiredText(formData.get("id"), "Proje kaydı bulunamadı.");
service.updateProject(actor, id, { ...projectPayload(formData, translations, context.defaultLocale), translations });
const cover = await uploadCover(actor, id, formData);
if (cover) service.updateProject(actor, id, { legacyCoverImagePath: cover });
revalidatePath("/projects");
revalidatePath(`/projects/${id}`);
}
export async function completeProjectRecord(formData: FormData) {
const { actor, service } = await requireFreelancerBackend();
const id = requiredText(formData.get("id"), "Tamamlanacak proje bulunamadı.");
service.updateProject(actor, id, { status: "completed", progress: 100 });
revalidatePath("/projects");
revalidatePath(`/projects/${id}`);
}
function sectionPayload(formData: FormData, translations?: Record<string, Record<string, string | null>>, defaultLocale = "tr") {
const localized = translations?.[defaultLocale] ?? {};
return {
projectId: requiredText(formData.get("project_id"), "Proje zorunludur."),
category: enumValue(formData.get("category"), SECTION_CATEGORIES, "overview"),
title: localized.title ?? requiredText(formData.get("title"), "Planlama başlığı zorunludur."),
content: localized.content ?? cleanText(formData.get("content")),
sortOrder: Math.max(0, Math.round(numberValue(formData.get("sort_order")))),
};
}
export async function createProjectPlanningSectionRecord(formData: FormData) {
const { actor, service } = await requireFreelancerBackend();
const i18n = new ContentTranslationService(getSqliteConnection().db);
const context = i18n.getLocalizationContext(actor);
const translations = parseContentTranslationsFromFormData(formData, "planning_section", context);
const payload = sectionPayload(formData, translations, context.defaultLocale);
service.addPlanningSection(actor, { ...payload, translations });
revalidatePath("/projects");
revalidatePath(`/projects/${payload.projectId}`);
}
export async function updateProjectPlanningSectionRecord(formData: FormData) {
const { actor, service } = await requireFreelancerBackend();
const i18n = new ContentTranslationService(getSqliteConnection().db);
const context = i18n.getLocalizationContext(actor);
const translations = parseContentTranslationsFromFormData(formData, "planning_section", context);
const id = requiredText(formData.get("id"), "Planlama alanı bulunamadı.");
const payload = sectionPayload(formData, translations, context.defaultLocale);
if (!service.listPlanningSections(actor, payload.projectId).some((section) => section.id === id)) {
throw new Error("Planlama alanı bu projeye ait değil.");
}
service.updatePlanningSection(actor, id, {
category: payload.category,
title: payload.title,
content: payload.content,
sortOrder: payload.sortOrder,
translations,
});
revalidatePath("/projects");
revalidatePath(`/projects/${payload.projectId}`);
}
export async function deleteProjectPlanningSectionRecord(formData: FormData) {
const { actor, service } = await requireFreelancerBackend();
const id = requiredText(formData.get("id"), "Silinecek planlama alanı bulunamadı.");
const projectId = requiredText(formData.get("project_id"), "Proje zorunludur.");
if (!service.listPlanningSections(actor, projectId).some((section) => section.id === id)) {
throw new Error("Planlama alanı bu projeye ait değil.");
}
service.deletePlanningSection(actor, id);
revalidatePath("/projects");
revalidatePath(`/projects/${projectId}`);
}
export async function updateRevisionStatus(id: string, projectId: string, status: string) {
const { actor, service } = await requireFreelancerBackend();
service.updateRevisionStatus(actor, id, enumValue(status, REVISION_STATUSES, "pending"), projectId);
revalidatePath(`/projects/${projectId}`);
}
export async function updateProjectSettings(projectId: string, progressType: "manual" | "auto", progress: number, revisionQuota: number) {
const { actor, service } = await requireFreelancerBackend();
service.updateProject(actor, projectId, {
progressType,
progress: Math.min(100, Math.max(0, Math.round(progress))),
revisionQuota: Math.max(0, Math.round(revisionQuota)),
});
revalidatePath("/projects");
revalidatePath(`/projects/${projectId}`);
}