stupa-pdf-api/frontend/src/store/applicationStore.ts

1573 lines
46 KiB
TypeScript

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import {
ApplicationDetails,
ApplicationListItem,
FormData,
Variant,
CreateResponse,
UpdateResponse,
RootPayload,
} from "../types/api";
import { apiClient, isSuccessResponse, getErrorMessage } from "../api/client";
interface ApplicationState {
// Current application
currentApplication: ApplicationDetails | null;
applicationList: ApplicationListItem[];
// Authentication
paId: string | null;
paKey: string | null;
masterKey: string | null;
isAdmin: boolean;
// UI State
isLoading: boolean;
isSubmitting: boolean;
error: string | null;
successMessage: string | null;
// Form state
formData: Partial<FormData>;
isDirty: boolean;
// Pagination
currentPage: number;
totalItems: number;
itemsPerPage: number;
// Filters
statusFilter: string | null;
variantFilter: string | null;
searchQuery: string | null;
// Sorting
sortBy: string;
sortOrder: "asc" | "desc";
}
interface ApplicationActions {
// Authentication
setApplicationCredentials: (paId: string, paKey: string) => void;
setMasterKey: (masterKey: string) => void;
validateMasterKey: () => Promise<boolean>;
clearCredentials: () => void;
// Application CRUD
createApplication: (
pdf?: File,
formData?: Partial<FormData>,
variant?: Variant,
) => Promise<boolean>;
updateApplication: (
pdf?: File,
formData?: Partial<FormData>,
variant?: Variant,
) => Promise<boolean>;
loadApplication: (paId?: string, paKey?: string) => Promise<boolean>;
deleteApplication: (paId?: string, paKey?: string) => Promise<boolean>;
downloadApplicationPdf: (paId?: string, paKey?: string) => Promise<boolean>;
// Application list management
loadApplicationList: (refresh?: boolean) => Promise<boolean>;
searchApplications: (query: string) => Promise<boolean>;
searchApplicationsAdvanced: (params: any) => Promise<boolean>;
setStatusFilter: (status: string | null) => void;
setVariantFilter: (variant: string | null) => void;
setPage: (page: number) => void;
setSorting: (sortBy: string, sortOrder: "asc" | "desc") => void;
// Admin operations
loadApplicationAdmin: (paId: string) => Promise<boolean>;
loadApplicationListAdmin: () => Promise<boolean>;
updateApplicationStatusAdmin: (
paId: string,
status: string,
) => Promise<boolean>;
deleteApplicationAdmin: (paId: string) => Promise<boolean>;
downloadApplicationPdfAdmin: (paId: string) => Promise<boolean>;
updateApplicationAdmin: (
paId: string,
pdf?: File,
formData?: Partial<FormData>,
variant?: Variant,
) => Promise<boolean>;
resetApplicationCredentials: (
paId: string,
) => Promise<{ pa_id: string; pa_key: string } | null>;
// Bulk operations
bulkDeleteApplications: (paIds: string[]) => Promise<boolean>;
bulkApproveApplications: (paIds: string[]) => Promise<boolean>;
bulkRejectApplications: (paIds: string[]) => Promise<boolean>;
bulkSetInReviewApplications: (paIds: string[]) => Promise<boolean>;
bulkSetNewApplications: (paIds: string[]) => Promise<boolean>;
// Form management
updateFormData: (data: Partial<FormData>) => void;
resetFormData: () => void;
setFormData: (data: Partial<FormData>) => void;
markFormDirty: () => void;
markFormClean: () => void;
// UI state management
setLoading: (loading: boolean) => void;
setSubmitting: (submitting: boolean) => void;
setError: (error: string | null) => void;
setSuccessMessage: (message: string | null) => void;
clearMessages: () => void;
// Utility
reset: () => void;
isCurrentUserApplication: (paId: string) => boolean;
}
const initialFormData: Partial<FormData> = {
applicantType: "person",
institutionType: "-",
institutionName: "",
firstName: "",
lastName: "",
email: "",
phone: "",
course: "-",
role: "-",
projectName: "",
startDate: "",
endDate: "",
participants: undefined,
description: "",
participatingFaculties: {
inf: false,
esb: false,
ls: false,
tec: false,
tex: false,
nxt: false,
open: false,
},
costs: [],
requestedAmountEur: 0,
variant: "VSM",
};
const initialState: ApplicationState = {
currentApplication: null,
applicationList: [],
paId: null,
paKey: null,
masterKey: null,
isAdmin: false,
isLoading: false,
isSubmitting: false,
error: null,
successMessage: null,
formData: initialFormData,
isDirty: false,
currentPage: 0,
totalItems: 0,
itemsPerPage: 50,
statusFilter: null,
variantFilter: null,
searchQuery: null,
// Sorting
sortBy: "created_at",
sortOrder: "desc",
};
export const useApplicationStore = create<
ApplicationState & ApplicationActions
>()(
persist(
(set, get) => ({
...initialState,
// Authentication
setApplicationCredentials: (paId: string, paKey: string) => {
set({ paId, paKey, isAdmin: false });
apiClient.clearMasterKey();
},
setMasterKey: (masterKey: string) => {
set({ masterKey, isAdmin: true });
apiClient.setMasterKey(masterKey);
},
validateMasterKey: async (): Promise<boolean> => {
const { masterKey } = get();
if (!masterKey) {
set({ error: "Master key is required" });
return false;
}
set({ isLoading: true, error: null });
try {
// Try to fetch applications list to validate master key
const response = await apiClient.listApplicationsAdmin({
limit: 1,
offset: 0,
});
if (isSuccessResponse(response)) {
set({ isLoading: false, isAdmin: true });
return true;
} else {
set({
error: "Invalid master key",
isLoading: false,
isAdmin: false,
masterKey: null,
});
apiClient.clearMasterKey();
return false;
}
} catch (error) {
set({
error: "Invalid master key",
isLoading: false,
isAdmin: false,
masterKey: null,
});
apiClient.clearMasterKey();
return false;
}
},
clearCredentials: () => {
set({
paId: null,
paKey: null,
masterKey: null,
isAdmin: false,
currentApplication: null,
applicationList: [],
});
apiClient.clearMasterKey();
},
// Application CRUD
createApplication: async (
pdf?: File,
formData?: Partial<FormData>,
variant?: Variant,
) => {
set({ isSubmitting: true, error: null });
try {
const request = {
pdf,
variant,
return_format: "json" as const,
form_json_b64: formData
? createFormJsonFromData(formData)
: undefined,
};
const response = await apiClient.createApplication(request);
if (isSuccessResponse(response)) {
const data = response.data as CreateResponse;
// Validate response data
if (!data.pa_id || !data.pa_key) {
set({
error:
"Unvollständige Antwort vom Server - ID oder Schlüssel fehlt",
isSubmitting: false,
});
return false;
}
set({
paId: data.pa_id,
paKey: data.pa_key,
successMessage: `Antrag erfolgreich erstellt! ID: ${data.pa_id}`,
isSubmitting: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isSubmitting: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isSubmitting: false,
});
return false;
}
},
updateApplication: async (
pdf?: File,
formData?: Partial<FormData>,
variant?: Variant,
) => {
const { paId, paKey } = get();
if (!paId || !paKey) {
set({ error: "Keine Anmeldedaten verfügbar" });
return false;
}
set({ isSubmitting: true, error: null });
try {
const request = {
pdf,
variant,
return_format: "json" as const,
form_json_b64: formData
? createFormJsonFromData(formData)
: undefined,
};
const response = await apiClient.updateApplication(
paId,
paKey,
request,
);
if (isSuccessResponse(response)) {
const data = response.data as UpdateResponse;
set({
successMessage: `Antrag erfolgreich aktualisiert: ${data.pa_id}`,
isSubmitting: false,
isDirty: false,
});
// Reload current application
await get().loadApplication();
return true;
} else {
set({
error: getErrorMessage(response),
isSubmitting: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isSubmitting: false,
});
return false;
}
},
loadApplication: async (paId?: string, paKey?: string) => {
const state = get();
const id = paId || state.paId;
const key = paKey || state.paKey;
if (!id || !key) {
set({ error: "Keine Anmeldedaten verfügbar" });
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.getApplication(id, key, "json");
if (isSuccessResponse(response)) {
const application = response.data as ApplicationDetails;
// Convert application payload to form data
const convertedFormData = convertPayloadToFormData(
application.payload,
);
set({
currentApplication: application,
formData: { ...initialFormData, ...convertedFormData },
isLoading: false,
isDirty: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isLoading: false,
});
return false;
}
},
deleteApplication: async (paId?: string, paKey?: string) => {
const state = get();
const id = paId || state.paId;
const key = paKey || state.paKey;
if (!id || !key) {
set({ error: "Keine Anmeldedaten verfügbar" });
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.deleteApplication(id, key);
if (isSuccessResponse(response)) {
set({
currentApplication: null,
successMessage: "Antrag erfolgreich gelöscht",
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isLoading: false,
});
return false;
}
},
downloadApplicationPdf: async (paId?: string, paKey?: string) => {
const state = get();
const id = paId || state.paId;
const key = paKey || state.paKey;
console.log("Starting PDF download for application:", id);
if (!id || !key) {
console.error("PDF download failed: Missing credentials", {
id,
key: key ? "***" : null,
});
set({ error: "Keine Anmeldedaten verfügbar" });
return false;
}
set({ isLoading: true, error: null });
try {
console.log("Making API call for PDF download...");
const response = await apiClient.getApplication(id, key, "pdf");
if (isSuccessResponse(response)) {
const blob = response.data as Blob;
console.log("Received PDF blob:", {
size: blob?.size,
type: blob?.type,
});
// Check if blob is valid
if (!blob || blob.size === 0) {
set({
error:
"PDF konnte nicht generiert werden - leere Antwort vom Server",
isLoading: false,
});
return false;
}
// Check for browser support
if (!window.URL || !window.URL.createObjectURL) {
set({
error:
"Ihr Browser unterstützt das Herunterladen von Dateien nicht",
isLoading: false,
});
return false;
}
try {
console.log("Creating download link for PDF...");
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `antrag-${id}.pdf`;
// Make link invisible and add to DOM
link.style.display = "none";
document.body.appendChild(link);
// Trigger download
console.log("Triggering PDF download...");
link.click();
// Cleanup
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 100);
set({
successMessage: "PDF wird heruntergeladen...",
isLoading: false,
});
return true;
} catch (downloadError) {
console.error("Download error:", downloadError);
set({
error:
"Fehler beim Herunterladen der PDF-Datei. Bitte versuchen Sie es erneut.",
isLoading: false,
});
return false;
}
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
console.error("PDF download error:", error);
let errorMessage = "Unbekannter Fehler beim Herunterladen";
if (error instanceof Error) {
if (error.message.includes("404")) {
errorMessage = "Antrag nicht gefunden";
} else if (error.message.includes("403")) {
errorMessage = "Keine Berechtigung zum Herunterladen";
} else if (error.message.includes("500")) {
errorMessage = "Server-Fehler beim Generieren der PDF";
} else {
errorMessage = `Download-Fehler: ${error.message}`;
}
}
set({
error: errorMessage,
isLoading: false,
});
return false;
}
},
// Application list management
loadApplicationList: async (refresh = false) => {
const {
paId,
paKey,
isAdmin,
currentPage,
itemsPerPage,
statusFilter,
variantFilter,
} = get();
if (!isAdmin && (!paId || !paKey)) {
set({ error: "Keine Anmeldedaten verfügbar" });
return false;
}
set({ isLoading: true, error: null });
try {
let response;
if (isAdmin) {
// Ensure master key is set before calling admin API
if (!get().masterKey) {
set({
error: "Master key not available. Please login again.",
isLoading: false,
});
return false;
}
response = await apiClient.listApplicationsAdmin({
limit: itemsPerPage,
offset: currentPage * itemsPerPage,
status: statusFilter || undefined,
variant: variantFilter || undefined,
order_by: get().sortBy,
order: get().sortOrder,
});
} else {
response = await apiClient.listApplications(paId!, paKey!, {
limit: itemsPerPage,
offset: currentPage * itemsPerPage,
status: statusFilter || undefined,
variant: variantFilter || undefined,
order_by: get().sortBy,
order: get().sortOrder,
});
}
if (isSuccessResponse(response)) {
const applications = response.data;
set({
applicationList: refresh
? applications
: [...get().applicationList, ...applications],
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isLoading: false,
});
return false;
}
},
searchApplications: async (query: string) => {
if (!get().isAdmin) {
set({ error: "Suche nur für Administratoren verfügbar" });
return false;
}
// Ensure master key is set before calling admin API
if (!get().masterKey) {
set({
error: "Master key not available. Please login again.",
isLoading: false,
});
return false;
}
set({ isLoading: true, error: null, searchQuery: query });
try {
const response = await apiClient.searchApplications({
q: query,
limit: get().itemsPerPage,
offset: 0,
status: get().statusFilter || undefined,
variant: get().variantFilter || undefined,
order_by: get().sortBy,
order: get().sortOrder,
});
if (isSuccessResponse(response)) {
set({
applicationList: response.data,
currentPage: 0,
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isLoading: false,
});
return false;
}
},
searchApplicationsAdvanced: async (params: any) => {
if (!get().isAdmin) {
set({ error: "Suche nur für Administratoren verfügbar" });
return false;
}
if (!get().masterKey) {
set({
error: "Master key not available. Please login again.",
isLoading: false,
});
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.searchApplications({
q: params.q,
status: params.status,
variant: params.variant,
amount_min: params.amount_min,
amount_max: params.amount_max,
date_from: params.date_from,
date_to: params.date_to,
created_by: params.created_by,
has_attachments: params.has_attachments,
limit: params.limit || get().itemsPerPage,
offset: params.offset || 0,
order_by: get().sortBy,
order: get().sortOrder,
});
if (isSuccessResponse(response)) {
set({
applicationList: response.data,
currentPage: 0,
isLoading: false,
searchQuery: params.q || null,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error: any) {
set({
error: error.message || "Suche fehlgeschlagen",
isLoading: false,
});
return false;
}
},
setStatusFilter: (status) => {
set({ statusFilter: status, currentPage: 0 });
},
setVariantFilter: (variant) => {
set({ variantFilter: variant, currentPage: 0 });
},
setPage: (page) => {
set({ currentPage: page });
},
setSorting: (sortBy, sortOrder) => {
set({ sortBy, sortOrder, currentPage: 0 });
},
// Admin operations
loadApplicationAdmin: async (paId: string) => {
if (!get().isAdmin) {
set({ error: "Admin-Berechtigung erforderlich" });
return false;
}
// Ensure master key is set before calling admin API
if (!get().masterKey) {
set({
error: "Master key not available. Please login again.",
isLoading: false,
});
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.getApplicationAdmin(paId, "json");
if (isSuccessResponse(response)) {
const application = response.data as ApplicationDetails;
const convertedFormData = convertPayloadToFormData(
application.payload,
);
set({
currentApplication: application,
formData: { ...initialFormData, ...convertedFormData },
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isLoading: false,
});
return false;
}
},
loadApplicationListAdmin: async () => {
return get().loadApplicationList(true);
},
updateApplicationStatusAdmin: async (paId: string, status: string) => {
if (!get().isAdmin) {
set({ error: "Admin-Berechtigung erforderlich" });
return false;
}
// Ensure master key is set before calling admin API
if (!get().masterKey) {
set({
error: "Master key not available. Please login again.",
isLoading: false,
});
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.setApplicationStatusAdmin(
paId,
status,
);
if (isSuccessResponse(response)) {
// Update the application in the list
const updatedList = get().applicationList.map((app) =>
app.pa_id === paId ? { ...app, status } : app,
);
// Also update current application if it matches
const currentApp = get().currentApplication;
if (currentApp && currentApp.pa_id === paId) {
set({
currentApplication: { ...currentApp, status },
applicationList: updatedList,
successMessage: `Status erfolgreich auf "${status}" geändert`,
isLoading: false,
});
} else {
set({
applicationList: updatedList,
successMessage: `Status erfolgreich auf "${status}" geändert`,
isLoading: false,
});
}
return true;
} else {
const errorMsg = getErrorMessage(response);
console.error(`Status update failed for ${paId}:`, errorMsg);
set({
error: `Fehler beim Ändern des Status: ${errorMsg}`,
isLoading: false,
});
return false;
}
} catch (error) {
console.error(`Status update error for ${paId}:`, error);
let errorMessage = "Unbekannter Fehler beim Ändern des Status";
if (error instanceof Error) {
if (error.message.includes("500")) {
errorMessage =
"Server-Fehler beim Ändern des Status. Bitte versuchen Sie es später erneut.";
} else if (error.message.includes("403")) {
errorMessage = "Keine Berechtigung zum Ändern des Status";
} else if (error.message.includes("404")) {
errorMessage = "Antrag nicht gefunden";
} else {
errorMessage = `Fehler beim Ändern des Status: ${error.message}`;
}
}
set({
error: errorMessage,
isLoading: false,
});
return false;
}
},
downloadApplicationPdfAdmin: async (paId: string) => {
console.log("Starting admin PDF download for application:", paId);
if (!get().isAdmin) {
console.error("Admin PDF download failed: Not an admin");
set({ error: "Admin-Berechtigung erforderlich" });
return false;
}
set({ isLoading: true, error: null });
try {
console.log("Making admin API call for PDF download...");
const response = await apiClient.getApplicationAdmin(paId, "pdf");
if (isSuccessResponse(response)) {
const blob = response.data as Blob;
console.log("Received admin PDF blob:", {
size: blob?.size,
type: blob?.type,
});
// Check if blob is valid
if (!blob || blob.size === 0) {
set({
error:
"PDF konnte nicht generiert werden - leere Antwort vom Server",
isLoading: false,
});
return false;
}
// Check for browser support
if (!window.URL || !window.URL.createObjectURL) {
set({
error:
"Ihr Browser unterstützt das Herunterladen von Dateien nicht",
isLoading: false,
});
return false;
}
try {
console.log("Creating admin download link for PDF...");
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `antrag-${paId}.pdf`;
// Make link invisible and add to DOM
link.style.display = "none";
document.body.appendChild(link);
// Trigger download
console.log("Triggering admin PDF download...");
link.click();
// Cleanup
setTimeout(() => {
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}, 100);
set({
successMessage: "PDF wird heruntergeladen...",
isLoading: false,
});
return true;
} catch (downloadError) {
console.error("Admin download error:", downloadError);
set({
error:
"Fehler beim Herunterladen der PDF-Datei. Bitte versuchen Sie es erneut.",
isLoading: false,
});
return false;
}
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
console.error("Admin PDF download error:", error);
let errorMessage = "Unbekannter Fehler beim Herunterladen";
if (error instanceof Error) {
if (error.message.includes("404")) {
errorMessage = "Antrag nicht gefunden";
} else if (error.message.includes("403")) {
errorMessage = "Keine Berechtigung zum Herunterladen";
} else if (error.message.includes("500")) {
errorMessage = "Server-Fehler beim Generieren der PDF";
} else {
errorMessage = `Download-Fehler: ${error.message}`;
}
}
set({
error: errorMessage,
isLoading: false,
});
return false;
}
},
deleteApplicationAdmin: async (paId: string) => {
if (!get().isAdmin) {
set({ error: "Admin-Berechtigung erforderlich" });
return false;
}
// Ensure master key is set before calling admin API
if (!get().masterKey) {
set({
error: "Master key not available. Please login again.",
isLoading: false,
});
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.deleteApplicationAdmin(paId);
if (isSuccessResponse(response)) {
// Remove from the list
const updatedList = get().applicationList.filter(
(app) => app.pa_id !== paId,
);
set({
applicationList: updatedList,
successMessage: "Antrag erfolgreich gelöscht",
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isLoading: false,
});
return false;
}
},
updateApplicationAdmin: async (
paId: string,
pdf?: File,
formData?: Partial<FormData>,
variant?: Variant,
) => {
if (!get().isAdmin) {
set({ error: "Admin-Berechtigung erforderlich" });
return false;
}
set({ isSubmitting: true, error: null });
try {
const request = {
pdf,
variant,
return_format: "json" as const,
form_json_b64: formData
? createFormJsonFromData(formData)
: undefined,
};
const response = await apiClient.updateApplicationAdmin(
paId,
request,
);
if (isSuccessResponse(response)) {
const data = response.data as UpdateResponse;
set({
successMessage: `Antrag erfolgreich aktualisiert: ${data.pa_id}`,
isSubmitting: false,
isDirty: false,
});
// Reload current application
await get().loadApplicationAdmin(paId);
return true;
} else {
set({
error: getErrorMessage(response),
isSubmitting: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Unbekannter Fehler",
isSubmitting: false,
});
return false;
}
},
resetApplicationCredentials: async (paId: string) => {
if (!get().isAdmin) {
set({ error: "Admin-Berechtigung erforderlich" });
return null;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.resetApplicationCredentials(paId);
if (!response.success) {
throw new Error(response.error.detail);
}
set({
successMessage: `Neue Zugangsdaten generiert für Antrag ${paId}`,
isLoading: false,
});
return {
pa_id: response.data.pa_id,
pa_key: response.data.pa_key,
};
} catch (error) {
set({
error:
error instanceof Error
? error.message
: "Fehler beim Zurücksetzen der Zugangsdaten",
isLoading: false,
});
return null;
}
},
// Form management
updateFormData: (data) => {
set((state) => ({
formData: { ...state.formData, ...data },
isDirty: true,
}));
},
resetFormData: () => {
set({ formData: initialFormData, isDirty: false });
},
setFormData: (data) => {
set({ formData: { ...initialFormData, ...data }, isDirty: false });
},
markFormDirty: () => {
set({ isDirty: true });
},
markFormClean: () => {
set({ isDirty: false });
},
// UI state management
setLoading: (loading) => set({ isLoading: loading }),
setSubmitting: (submitting) => set({ isSubmitting: submitting }),
setError: (error) => set({ error }),
setSuccessMessage: (message) => set({ successMessage: message }),
clearMessages: () => {
set({ error: null, successMessage: null });
},
// Utility
reset: () => {
set(initialState);
apiClient.clearMasterKey();
},
isCurrentUserApplication: (paId: string) => {
const { paId: currentPaId, isAdmin } = get();
return isAdmin || currentPaId === paId;
},
// Bulk operations
bulkDeleteApplications: async (paIds: string[]) => {
if (!get().isAdmin || !get().masterKey) {
set({ error: "Admin access required" });
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.bulkOperation({
pa_ids: paIds,
operation: "delete",
});
if (isSuccessResponse(response)) {
// Reload application list to reflect changes
await get().loadApplicationListAdmin();
set({
successMessage: `${response.data.success.length} Anträge erfolgreich gelöscht`,
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Fehler beim Löschen",
isLoading: false,
});
return false;
}
},
bulkApproveApplications: async (paIds: string[]) => {
if (!get().isAdmin || !get().masterKey) {
set({ error: "Admin access required" });
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.bulkOperation({
pa_ids: paIds,
operation: "approve",
});
if (isSuccessResponse(response)) {
await get().loadApplicationListAdmin();
set({
successMessage: `${response.data.success.length} Anträge erfolgreich genehmigt`,
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Fehler beim Genehmigen",
isLoading: false,
});
return false;
}
},
bulkRejectApplications: async (paIds: string[]) => {
if (!get().isAdmin || !get().masterKey) {
set({ error: "Admin access required" });
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.bulkOperation({
pa_ids: paIds,
operation: "reject",
});
if (isSuccessResponse(response)) {
await get().loadApplicationListAdmin();
set({
successMessage: `${response.data.success.length} Anträge erfolgreich abgelehnt`,
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error ? error.message : "Fehler beim Ablehnen",
isLoading: false,
});
return false;
}
},
bulkSetInReviewApplications: async (paIds: string[]) => {
if (!get().isAdmin || !get().masterKey) {
set({ error: "Admin access required" });
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.bulkOperation({
pa_ids: paIds,
operation: "set_in_review",
});
if (isSuccessResponse(response)) {
await get().loadApplicationListAdmin();
set({
successMessage: `${response.data.success.length} Anträge für Bearbeitung gesperrt`,
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error
? error.message
: "Fehler beim Status-Update",
isLoading: false,
});
return false;
}
},
bulkSetNewApplications: async (paIds: string[]) => {
if (!get().isAdmin || !get().masterKey) {
set({ error: "Admin access required" });
return false;
}
set({ isLoading: true, error: null });
try {
const response = await apiClient.bulkOperation({
pa_ids: paIds,
operation: "set_new",
});
if (isSuccessResponse(response)) {
await get().loadApplicationListAdmin();
set({
successMessage: `${response.data.success.length} Anträge auf "Beantragt" gesetzt`,
isLoading: false,
});
return true;
} else {
set({
error: getErrorMessage(response),
isLoading: false,
});
return false;
}
} catch (error) {
set({
error:
error instanceof Error
? error.message
: "Fehler beim Status-Update",
isLoading: false,
});
return false;
}
},
}),
{
name: "stupa-application-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
paId: state.paId,
paKey: state.paKey,
masterKey: state.masterKey,
isAdmin: state.isAdmin,
}),
onRehydrateStorage: () => (state) => {
// Immediately set master key in API client after rehydration
if (state?.masterKey && state?.isAdmin) {
apiClient.setMasterKey(state.masterKey);
}
},
},
),
);
// Helper functions
function createFormJsonFromData(formData: Partial<FormData>): string {
const formJson: Record<string, { "/V": any }> = {};
// Map form data to PDF field names based on the mapping
const fieldMappings: Record<string, string> = {
// Meta
paId: "pa-id",
paKey: "pa-key",
// Applicant
applicantType: "pa-applicant-type",
institutionType: "pa-institution-type",
institutionName: "pa-institution",
firstName: "pa-first-name",
lastName: "pa-last-name",
email: "pa-email",
phone: "pa-phone",
course: "pa-course",
role: "pa-role",
// Project
projectName: "pa-project-name",
startDate: "pa-start-date",
endDate: "pa-end-date",
participants: "pa-participants",
description: "pa-project-description",
// Totals
requestedAmountEur: "pa-requested-amount-euro-sum",
};
// Add basic fields
for (const [formKey, pdfKey] of Object.entries(fieldMappings)) {
const value = (formData as any)[formKey];
if (value !== undefined && value !== null && value !== "") {
formJson[pdfKey] = { "/V": value };
}
}
// Add participating faculties
if (formData.participatingFaculties) {
const faculties = formData.participatingFaculties;
formJson["pa-participating-faculties-inf"] = { "/V": faculties.inf };
formJson["pa-participating-faculties-esb"] = { "/V": faculties.esb };
formJson["pa-participating-faculties-ls"] = { "/V": faculties.ls };
formJson["pa-participating-faculties-tec"] = { "/V": faculties.tec };
formJson["pa-participating-faculties-tex"] = { "/V": faculties.tex };
formJson["pa-participating-faculties-nxt"] = { "/V": faculties.nxt };
formJson["pa-participating-faculties-open"] = { "/V": faculties.open };
}
// Add costs
if (formData.costs) {
formData.costs.forEach((cost, index) => {
const i = index + 1; // 1-based indexing
if (cost.name) {
formJson[`pa-cost-${i}-name`] = { "/V": cost.name };
}
if (cost.amountEur !== undefined) {
formJson[`pa-cost-${i}-amount-euro`] = { "/V": cost.amountEur };
}
});
}
// Add variant-specific fields
if (formData.variant === "QSM") {
if (formData.financingCode || formData.qsmFinancing) {
formJson["pa-qsm-financing"] = {
"/V": formData.financingCode || formData.qsmFinancing,
};
}
if (formData.qsmFlags) {
formJson["pa-qsm-stellenfinanzierungen"] = {
"/V": formData.qsmFlags.stellenfinanzierungen,
};
formJson["pa-qsm-studierende"] = { "/V": formData.qsmFlags.studierende };
formJson["pa-qsm-individuell"] = { "/V": formData.qsmFlags.individuell };
formJson["pa-qsm-exkursion-genehmigt"] = {
"/V": formData.qsmFlags.exkursionGenehmigt,
};
formJson["pa-qsm-exkursion-bezuschusst"] = {
"/V": formData.qsmFlags.exkursionBezuschusst,
};
}
} else if (formData.variant === "VSM") {
if (formData.financingCode || formData.vsmFinancing) {
formJson["pa-vsm-financing"] = {
"/V": formData.financingCode || formData.vsmFinancing,
};
}
if (formData.vsmFlags) {
formJson["pa-vsm-aufgaben"] = { "/V": formData.vsmFlags.aufgaben };
formJson["pa-vsm-individuell"] = { "/V": formData.vsmFlags.individuell };
}
}
const jsonString = JSON.stringify(formJson);
return btoa(unescape(encodeURIComponent(jsonString))); // Base64 encode with UTF-8 support
}
function convertPayloadToFormData(payload: RootPayload): Partial<FormData> {
const pa = payload.pa;
return {
paId: pa.meta.id,
paKey: pa.meta.key,
applicantType: pa.applicant.type as any,
institutionType: pa.applicant.institution.type as any,
institutionName: pa.applicant.institution.name || "",
firstName: pa.applicant.name.first || "",
lastName: pa.applicant.name.last || "",
email: pa.applicant.contact.email || "",
phone: pa.applicant.contact.phone || "",
course: pa.applicant.course as any,
role: pa.applicant.role as any,
projectName: pa.project.name || "",
startDate: pa.project.dates.start || "",
endDate: pa.project.dates.end || "",
participants: pa.project.participants,
description: pa.project.description || "",
participatingFaculties: {
inf: pa.project.participation.faculties.inf || false,
esb: pa.project.participation.faculties.esb || false,
ls: pa.project.participation.faculties.ls || false,
tec: pa.project.participation.faculties.tec || false,
tex: pa.project.participation.faculties.tex || false,
nxt: pa.project.participation.faculties.nxt || false,
open: pa.project.participation.faculties.open || false,
},
costs: (pa.project.costs || []).map((cost) => ({
name: cost.name || "",
amountEur: cost.amountEur || 0,
})),
variant: (pa.project.financing.vsm?.code
? "VSM"
: pa.project.financing.qsm?.code
? "QSM"
: "COMMON") as Variant,
financingCode:
pa.project.financing.qsm?.code || pa.project.financing.vsm?.code || "",
qsmFinancing: pa.project.financing.qsm?.code as any,
qsmFlags: pa.project.financing.qsm?.flags
? {
stellenfinanzierungen:
pa.project.financing.qsm.flags.stellenfinanzierungen || false,
studierende: pa.project.financing.qsm.flags.studierende || false,
individuell: pa.project.financing.qsm.flags.individuell || false,
exkursionGenehmigt:
pa.project.financing.qsm.flags.exkursionGenehmigt || false,
exkursionBezuschusst:
pa.project.financing.qsm.flags.exkursionBezuschusst || false,
}
: undefined,
vsmFinancing: pa.project.financing.vsm?.code as any,
vsmFlags: pa.project.financing.vsm?.flags
? {
aufgaben: pa.project.financing.vsm.flags.aufgaben || false,
individuell: pa.project.financing.vsm.flags.individuell || false,
}
: undefined,
};
}