Frontend an Backend angebunden mit API-Wrapper und Datenmapping

This commit is contained in:
root 2025-03-13 09:17:22 +01:00
parent b0d8d4f915
commit 9ffa70aad1
5 changed files with 398 additions and 150 deletions

View File

@ -1,9 +1,22 @@
import { getPrinters } from "@/server/actions/printers";
import { backendApi } from "@/utils/fetch";
import { mapBackendPrinterToFrontend } from "@/utils/backend-types";
export const dynamic = "force-dynamic";
export async function GET() {
const printers = await getPrinters();
try {
// Hole Drucker vom Backend statt aus der lokalen Datenbank
const backendPrinters = await backendApi.printers.getAll();
// Transformiere die Backend-Daten ins Frontend-Format
const printers = backendPrinters.map(mapBackendPrinterToFrontend);
return Response.json(printers);
return Response.json(printers);
} catch (error) {
console.error("Fehler beim Abrufen der Drucker vom Backend:", error);
return Response.json(
{ error: "Fehler beim Abrufen der Drucker vom Backend" },
{ status: 500 }
);
}
}

View File

@ -30,14 +30,47 @@ export async function createPrintJob(printJob: InferInsertModel<typeof printJobs
}
try {
const result = await db.insert(printJobs).values(printJob).returning({
jobId: printJobs.id,
// Bereite die Daten für das Backend vor
// In unserem Backend wird printerId als socketId bezeichnet
const backendJob = {
socketId: printJob.printerId, // Feldname ändern für Backend
userId: printJob.userId,
durationInMinutes: printJob.durationInMinutes,
comments: printJob.comments || ""
};
// Sende den Job ans Backend
const backendResponse = await fetch(`${process.env.BACKEND_URL || 'http://localhost:5000'}/api/jobs`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(backendJob)
});
return result[0].jobId;
if (!backendResponse.ok) {
throw new Error(`Backend-Fehler: ${backendResponse.status} ${backendResponse.statusText}`);
}
const backendResult = await backendResponse.json();
// Synchronisiere mit lokaler Datenbank
// Wir verwenden hier die Job-ID vom Backend, um Konsistenz zu gewährleisten
if (backendResult.id) {
const result = await db.insert(printJobs).values({
...printJob,
id: backendResult.id // Verwende die vom Backend erzeugte ID
}).returning({
jobId: printJobs.id,
});
return result[0].jobId;
} else {
return backendResult.id;
}
} catch (error) {
console.error("Fehler beim Erstellen des Druckauftrags:", error);
return {
error: "Druckauftrag konnte nicht hinzugefügt werden.",
error: error instanceof Error ? error.message : "Druckauftrag konnte nicht hinzugefügt werden.",
};
}
}
@ -85,48 +118,47 @@ export async function abortPrintJob(jobId: string, reason: string) {
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
try {
// Hole den Druckauftrag vom Backend
const backendResponse = await fetch(`${process.env.BACKEND_URL || 'http://localhost:5000'}/api/jobs/${jobId}/abort`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason })
});
if (!backendResponse.ok) {
const errorData = await backendResponse.json();
throw new Error(errorData.message || `Backend-Fehler: ${backendResponse.status}`);
}
// Hole den lokalen Druckauftrag für die Berechtigungsprüfung
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
if (!printJob) {
if (printJob) {
// Aktualisiere den lokalen Druckauftrag auch
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
await db
.update(printJobs)
.set({
aborted: true,
abortReason: reason,
comments: `${printJob.comments || ""}\n\n---${dbUser?.username}: Druckauftrag abgebrochen`,
})
.where(eq(printJobs.id, jobId));
}
revalidatePath("/");
return { success: true };
} catch (error) {
console.error("Fehler beim Abbrechen des Druckauftrags:", error);
return {
error: "Druckauftrag nicht gefunden",
error: error instanceof Error ? error.message : "Druckauftrag konnte nicht abgebrochen werden.",
};
}
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
return {
error: strings.ERROR.PERMISSION,
};
}
// Get duration in minutes since startAt
const duration = Math.floor((Date.now() - new Date(printJob.startAt).getTime()) / 1000 / 60);
await db
.update(printJobs)
.set({
aborted: true,
abortReason: reason,
durationInMinutes: duration,
comments: `${printJob.comments}\n\n---${dbUser?.username}: Druckauftrag abgebrochen`,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}
export async function earlyFinishPrintJob(jobId: string) {
@ -149,44 +181,48 @@ export async function earlyFinishPrintJob(jobId: string) {
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
try {
// Sende die Anfrage an das Backend
const backendResponse = await fetch(`${process.env.BACKEND_URL || 'http://localhost:5000'}/api/jobs/${jobId}/finish`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (!backendResponse.ok) {
const errorData = await backendResponse.json();
throw new Error(errorData.message || `Backend-Fehler: ${backendResponse.status}`);
}
// Hole den lokalen Druckauftrag für die Synchronisierung
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
if (!printJob) {
return { error: "Druckauftrag nicht gefunden" };
}
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
if (printJob) {
// Aktualisiere den lokalen Druckauftrag auch
// Hole die tatsächliche Dauer vom Backend
const backendData = await backendResponse.json();
const duration = backendData.durationInMinutes || Math.floor((Date.now() - new Date(printJob.startAt).getTime()) / 1000 / 60);
await db
.update(printJobs)
.set({
durationInMinutes: duration,
comments: `${printJob.comments || ""}\n\n---${dbUser?.username}: Druckauftrag vorzeitig abgeschlossen`,
})
.where(eq(printJobs.id, jobId));
}
revalidatePath("/");
return { success: true };
} catch (error) {
console.error("Fehler beim vorzeitigen Beenden des Druckauftrags:", error);
return {
error: strings.ERROR.PERMISSION,
error: error instanceof Error ? error.message : "Druckauftrag konnte nicht vorzeitig beendet werden.",
};
}
// Get duration in minutes since startAt
const duration = Math.floor((Date.now() - new Date(printJob.startAt).getTime()) / 1000 / 60);
await db
.update(printJobs)
.set({
durationInMinutes: duration,
comments: `${printJob.comments}\n\n---${dbUser?.username}: Druckauftrag vorzeitig abgeschlossen`,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}
export async function extendPrintJob(jobId: string, minutes: number, hours: number) {
@ -209,43 +245,47 @@ export async function extendPrintJob(jobId: string, minutes: number, hours: numb
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
try {
// Sende die Anfrage an das Backend
const backendResponse = await fetch(`${process.env.BACKEND_URL || 'http://localhost:5000'}/api/jobs/${jobId}/extend`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ minutes, hours })
});
if (!backendResponse.ok) {
const errorData = await backendResponse.json();
throw new Error(errorData.message || `Backend-Fehler: ${backendResponse.status}`);
}
// Hole den lokalen Druckauftrag für die Synchronisierung
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
if (!printJob) {
return { error: "Druckauftrag nicht gefunden" };
}
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
if (printJob) {
const duration = minutes + hours * 60;
// Aktualisiere den lokalen Druckauftrag auch
await db
.update(printJobs)
.set({
durationInMinutes: printJob.durationInMinutes + duration,
comments: `${printJob.comments || ""}\n\n---${dbUser?.username}: Verlängert um ${hours} Stunden und ${minutes} Minuten`,
})
.where(eq(printJobs.id, jobId));
}
revalidatePath("/");
return { success: true };
} catch (error) {
console.error("Fehler beim Verlängern des Druckauftrags:", error);
return {
error: strings.ERROR.PERMISSION,
error: error instanceof Error ? error.message : "Druckauftrag konnte nicht verlängert werden.",
};
}
const duration = minutes + hours * 60;
await db
.update(printJobs)
.set({
durationInMinutes: printJob.durationInMinutes + duration,
comments: `${printJob.comments}\n\n---${dbUser?.username}: Verlängert um ${hours} Stunden und ${minutes} Minuten`,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}
export async function updatePrintComments(jobId: string, comments: string) {
@ -268,38 +308,41 @@ export async function updatePrintComments(jobId: string, comments: string) {
};
}
// Get the print job
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
try {
// Sende die Anfrage an das Backend
const backendResponse = await fetch(`${process.env.BACKEND_URL || 'http://localhost:5000'}/api/jobs/${jobId}/comments`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ comments })
});
if (!backendResponse.ok) {
const errorData = await backendResponse.json();
throw new Error(errorData.message || `Backend-Fehler: ${backendResponse.status}`);
}
if (!printJob) {
return { error: "Druckauftrag nicht gefunden" };
}
// Synchronisiere mit der lokalen Datenbank
const printJob = await db.query.printJobs.findFirst({
where: eq(printJobs.id, jobId),
});
// Check if the print job is already aborted or completed
if (printJob.aborted) {
return { error: "Druckauftrag wurde bereits abgebrochen" };
}
if (new Date(printJob.startAt).getTime() + printJob.durationInMinutes * 60 * 1000 < Date.now()) {
return { error: "Druckauftrag ist bereits abgeschlossen" };
}
// Check if user is the owner of the print job
// biome-ignore lint/style/noNonNullAssertion: guard already checks against null
if (printJob.userId !== dbUser!.id && dbUser!.role !== UserRole.ADMIN) {
if (printJob) {
await db
.update(printJobs)
.set({
comments,
})
.where(eq(printJobs.id, jobId));
}
revalidatePath("/");
return { success: true };
} catch (error) {
console.error("Fehler beim Aktualisieren der Kommentare:", error);
return {
error: strings.ERROR.PERMISSION,
error: error instanceof Error ? error.message : "Kommentare konnten nicht aktualisiert werden.",
};
}
await db
.update(printJobs)
.set({
comments,
})
.where(eq(printJobs.id, jobId));
revalidatePath("/");
}

View File

@ -7,6 +7,8 @@ import { IS_NOT, guard } from "@/utils/guard";
import strings from "@/utils/strings";
import { type InferInsertModel, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { backendApi } from "@/utils/fetch";
import { mapBackendPrinterToFrontend, mapFrontendPrinterToBackend } from "@/utils/backend-types";
export async function createPrinter(printer: InferInsertModel<typeof printers>) {
const { user } = await validateRequest();
@ -35,8 +37,16 @@ export async function createPrinter(printer: InferInsertModel<typeof printers>)
}
try {
// Transformiere die Daten ins Backend-Format
const backendPrinter = mapFrontendPrinterToBackend(printer);
// Rufe das Backend API auf
await backendApi.printers.create(backendPrinter);
// Lokale Datenbank bleibt synchronisiert (optional, wenn du weiterhin lokale Daten brauchst)
await db.insert(printers).values(printer);
} catch (error) {
console.error("Fehler beim Erstellen des Druckers:", error);
return {
error: "Drucker konnte nicht hinzugefügt werden.",
};
@ -72,10 +82,18 @@ export async function updatePrinter(id: string, data: InferInsertModel<typeof pr
}
try {
// Transformiere die Daten ins Backend-Format
const backendPrinter = mapFrontendPrinterToBackend(data);
// Rufe das Backend API auf
await backendApi.printers.update(id, backendPrinter);
// Lokale Datenbank bleibt synchronisiert (optional)
await db.update(printers).set(data).where(eq(printers.id, id));
} catch (error) {
console.error("Fehler beim Aktualisieren des Druckers:", error);
return {
error: "Druckerdaten sind erforderlich.",
error: "Drucker konnte nicht aktualisiert werden.",
};
}
@ -103,8 +121,13 @@ export async function deletePrinter(id: string) {
}
try {
// Rufe das Backend API auf
await backendApi.printers.delete(id);
// Lokale Datenbank bleibt synchronisiert (optional)
await db.delete(printers).where(eq(printers.id, id));
} catch (error) {
console.error("Fehler beim Löschen des Druckers:", error);
if (error instanceof Error) {
return {
error: error.message,
@ -119,12 +142,23 @@ export async function deletePrinter(id: string) {
}
export async function getPrinters() {
return await db.query.printers.findMany({
with: {
printJobs: {
limit: 1,
orderBy: (printJobs, { desc }) => [desc(printJobs.startAt)],
try {
// Rufe die Drucker vom Backend ab
const backendPrinters = await backendApi.printers.getAll();
// Transformiere die Backend-Daten ins Frontend-Format
return backendPrinters.map(mapBackendPrinterToFrontend);
} catch (error) {
console.error("Fehler beim Abrufen der Drucker:", error);
// Fallback zur lokalen Datenbank, falls das Backend nicht erreichbar ist
return await db.query.printers.findMany({
with: {
printJobs: {
limit: 1,
orderBy: (printJobs, { desc }) => [desc(printJobs.startAt)],
},
},
},
});
});
}
}

View File

@ -0,0 +1,85 @@
// TypeScript-Definitionen für die Backend-API-Responses
// Steckdosenmodell (entspricht dem Backend socket)
export interface BackendPrinter {
id: string;
name: string;
description: string;
status: number; // 0=available, 1=busy
ipAddress?: string;
connectionStatus?: string;
lastSeen?: string;
uptimeInfo?: {
offline_since?: string;
offline_duration?: number;
offline_duration_formatted?: string;
};
latestJob?: BackendJob | null;
waitingJobs?: BackendJob[];
}
// Druckauftrag (entspricht dem Backend job)
export interface BackendJob {
id: string;
socketId: string; // Backend nennt es socketId statt printerId
userId: string;
startAt: string;
durationInMinutes: number;
comments: string | null;
aborted: boolean;
abortReason: string | null;
waitingApproval?: boolean;
remainingMinutes?: number;
}
// Für die Kartierung zwischen Frontend und Backend
export const mapBackendPrinterToFrontend = (printer: BackendPrinter) => {
return {
id: printer.id,
name: printer.name,
description: printer.description,
status: printer.status,
// Weitere Felder für Frontend-Anpassungen
connectionStatus: printer.connectionStatus || 'unknown',
uptimeInfo: printer.uptimeInfo,
// Transformiere das aktuelle Job-Format
printJobs: printer.latestJob ? [mapBackendJobToFrontend(printer.latestJob)] : [],
// Weitere wartende Jobs
waitingJobs: printer.waitingJobs?.map(mapBackendJobToFrontend) || [],
};
};
export const mapFrontendPrinterToBackend = (printer: any) => {
return {
id: printer.id,
name: printer.name,
description: printer.description,
status: printer.status,
// Frontend hat keine IP-Adresse, diese wird vom Backend verwaltet
};
};
export const mapBackendJobToFrontend = (job: BackendJob) => {
return {
id: job.id,
printerId: job.socketId, // Anpassung des Feldnamens
userId: job.userId,
startAt: new Date(job.startAt),
durationInMinutes: job.durationInMinutes,
comments: job.comments || '',
aborted: job.aborted,
abortReason: job.abortReason || '',
waitingApproval: job.waitingApproval || false,
remainingMinutes: job.remainingMinutes || 0,
};
};
export const mapFrontendJobToBackend = (job: any) => {
return {
printerId: job.printerId, // Im Backend als socketId
userId: job.userId,
durationInMinutes: job.durationInMinutes,
comments: job.comments || '',
// Die restlichen Felder werden vom Backend verwaltet
};
};

View File

@ -1 +1,74 @@
// Konfiguration für das Backend
export const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:5000';
// Standard Fetcher für SWR
export const fetcher = (url: string) => fetch(url).then((response) => response.json());
// Backend API Client für direkte API-Calls
export const backendFetcher = async (endpoint: string, options?: RequestInit) => {
const url = `${BACKEND_URL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`Backend API error: ${response.status} ${response.statusText}`);
}
return response.json();
};
// Backend API Wrapper mit spezifischen Endpunkten
export const backendApi = {
// Drucker-Endpunkte
printers: {
getAll: () => backendFetcher('/api/printers'),
getById: (id: string) => backendFetcher(`/api/printers/${id}`),
create: (data: any) => backendFetcher('/api/printers', {
method: 'POST',
body: JSON.stringify(data),
}),
update: (id: string, data: any) => backendFetcher(`/api/printers/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
delete: (id: string) => backendFetcher(`/api/printers/${id}`, {
method: 'DELETE',
}),
},
// Druckaufträge-Endpunkte
jobs: {
getAll: () => backendFetcher('/api/jobs'),
getById: (id: string) => backendFetcher(`/api/jobs/${id}`),
create: (data: any) => backendFetcher('/api/jobs', {
method: 'POST',
body: JSON.stringify(data),
}),
abort: (id: string, reason: string) => backendFetcher(`/api/jobs/${id}/abort`, {
method: 'POST',
body: JSON.stringify({ reason }),
}),
finish: (id: string) => backendFetcher(`/api/jobs/${id}/finish`, {
method: 'POST',
}),
extend: (id: string, minutes: number, hours: number) => backendFetcher(`/api/jobs/${id}/extend`, {
method: 'POST',
body: JSON.stringify({ minutes, hours }),
}),
updateComments: (id: string, comments: string) => backendFetcher(`/api/jobs/${id}/comments`, {
method: 'PUT',
body: JSON.stringify({ comments }),
}),
getRemainingTime: (id: string) => backendFetcher(`/api/job/${id}/remaining-time`),
},
// Statistiken-Endpunkt
stats: {
get: () => backendFetcher('/api/stats'),
},
};