Skip to content

Commit bc1650c

Browse files
Copilotmilljoniaer
andauthored
Refactor: split long service files and centralize exception handling (#35)
* Initial plan * Add GlobalExceptionHandler and clean up ActivityController try-catch boilerplate - Create ResourceNotFoundException for 404 responses - Create GlobalExceptionHandler (@ControllerAdvice) handling ResourceNotFoundException, IllegalArgumentException, IOException, and RuntimeException - Remove try-catch blocks from all ActivityController endpoints (591→434 lines) - Extract reusable parseRequiredDocumentId() and buildFileDownloadResponse() helpers - Remove unused buildCombinedMarkdown() method - Update ActivityService to throw ResourceNotFoundException Co-authored-by: milljoniaer <78978542+milljoniaer@users.noreply.github.com> * Split MarkdownToDocxService into focused helper classes - Extract DocxHeaderFooterHelper (300 lines) - header/footer creation, logo, styled runs, field runs - Extract DocxTableHelper (146 lines) - Artikulationsschema table layout, column widths, cell sizing - MarkdownToDocxService reduced from 632→270 lines (57% reduction) - Update tests to use new constructor with injected helpers Co-authored-by: milljoniaer <78978542+milljoniaer@users.noreply.github.com> * Extract ActivityExtractionService from ActivityService - Move PDF metadata extraction, normalization, and defaults logic to ActivityExtractionService (239 lines) - ActivityService reduced from 673→491 lines (27% reduction) - ActivityService retains CRUD, filtering, recommendation criteria - Update ActivityServiceTest to inject extraction service with mocks Co-authored-by: milljoniaer <78978542+milljoniaer@users.noreply.github.com> * Split apiService.ts into domain-specific API modules - Extract ActivityApi (309 lines) - activity CRUD, PDF/DOCX downloads, recommendations - Extract HistoryApi (105 lines) - search history and favourites - Extract UserApi (77 lines) - user management and profiles - apiService.ts reduced from 521→106 lines (80% reduction) - ApiService class remains as backward-compatible facade - Export ApiRequestMixin for shared request handling Co-authored-by: milljoniaer <78978542+milljoniaer@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: milljoniaer <78978542+milljoniaer@users.noreply.github.com> Co-authored-by: Jonathan Ostertag <jonathan@ostertage.de>
1 parent 5596509 commit bc1650c

File tree

15 files changed

+1504
-1376
lines changed

15 files changed

+1504
-1376
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { authService } from "./authService";
2+
import type {
3+
Activity,
4+
ActivitiesResponse,
5+
ResultsData,
6+
FieldValues,
7+
} from "@/types/activity";
8+
import type {
9+
UploadPdfDraftResponse,
10+
UploadPdfDraftOptions,
11+
ArtikulationsschemaResponse,
12+
ActivityMarkdownsResponse,
13+
CreateActivityRequest,
14+
UpdateActivityRequest,
15+
LessonPlanRequest,
16+
SearchCriteria,
17+
} from "@/types/api";
18+
import { ApiRequestMixin } from "./apiService";
19+
20+
/**
21+
* Activity-related API methods: CRUD, recommendations, PDF/DOCX downloads,
22+
* markdown generation, and field values.
23+
*/
24+
export const ActivityApi = {
25+
/**
26+
* Get activities with pagination
27+
*/
28+
async getActivities(params: SearchCriteria = {}) {
29+
const queryParams = new URLSearchParams();
30+
Object.entries(params).forEach(([key, value]) => {
31+
if (value !== undefined && value !== null && value !== "") {
32+
if (Array.isArray(value)) {
33+
value.forEach((v) => queryParams.append(key, String(v)));
34+
} else {
35+
queryParams.append(key, String(value));
36+
}
37+
}
38+
});
39+
40+
return ApiRequestMixin.request<ActivitiesResponse>(
41+
`/api/activities/?${queryParams.toString()}`,
42+
);
43+
},
44+
45+
/**
46+
* Get activity by ID
47+
*/
48+
async getActivity(id: string) {
49+
return ApiRequestMixin.request<Activity>(`/api/activities/${id}`);
50+
},
51+
52+
/**
53+
* Get multiple activities by IDs
54+
*/
55+
async getActivitiesByIds(ids: string[]) {
56+
const promises = ids.map((id) => this.getActivity(id));
57+
const results = await Promise.all(promises);
58+
return results;
59+
},
60+
61+
/**
62+
* Get recommendations
63+
*/
64+
async getRecommendations(params: string): Promise<ResultsData> {
65+
return ApiRequestMixin.request<ResultsData>(
66+
`/api/activities/recommendations?${params}`,
67+
);
68+
},
69+
70+
/**
71+
* Create activity
72+
*/
73+
async createActivity(data: CreateActivityRequest) {
74+
return ApiRequestMixin.request("/api/activities/create", {
75+
method: "POST",
76+
headers: { "Content-Type": "application/json" },
77+
body: JSON.stringify(data),
78+
});
79+
},
80+
81+
/**
82+
* Delete activity (admin only)
83+
*/
84+
async deleteActivity(activityId: string) {
85+
return ApiRequestMixin.request(`/api/activities/${activityId}`, {
86+
method: "DELETE",
87+
});
88+
},
89+
90+
/**
91+
* Update activity (admin only)
92+
*/
93+
async updateActivity(activityId: string, data: UpdateActivityRequest) {
94+
return ApiRequestMixin.request<Activity>(`/api/activities/${activityId}`, {
95+
method: "PUT",
96+
headers: { "Content-Type": "application/json" },
97+
body: JSON.stringify(data),
98+
});
99+
},
100+
101+
/**
102+
* Generate lesson plan PDF
103+
*/
104+
async generateLessonPlan(data: LessonPlanRequest) {
105+
const response = await authService.makeAuthenticatedRequest(
106+
"/api/activities/lesson-plan",
107+
{
108+
method: "POST",
109+
headers: { "Content-Type": "application/json" },
110+
body: JSON.stringify(data),
111+
},
112+
);
113+
114+
if (!response.ok) {
115+
const errorData = await response.json().catch(() => ({}));
116+
throw new Error(
117+
errorData.message || `HTTP error! status: ${response.status}`,
118+
);
119+
}
120+
121+
return response.blob();
122+
},
123+
124+
/**
125+
* Get PDF by activity ID
126+
*/
127+
async getActivityPdf(activityId: string) {
128+
const response = await authService.makeAuthenticatedRequest(
129+
`/api/activities/${activityId}/pdf`,
130+
);
131+
132+
if (!response.ok) {
133+
throw new Error(`HTTP error! status: ${response.status}`);
134+
}
135+
136+
return response.blob();
137+
},
138+
139+
/**
140+
* Get a stored markdown rendered as PDF by markdown ID
141+
*/
142+
async getMarkdownPdf(markdownId: string) {
143+
const response = await authService.makeAuthenticatedRequest(
144+
`/api/markdowns/${markdownId}/pdf`,
145+
);
146+
147+
if (!response.ok) {
148+
throw new Error(`HTTP error! status: ${response.status}`);
149+
}
150+
151+
return response.blob();
152+
},
153+
154+
/**
155+
* Get a stored markdown rendered as DOCX (Word) by markdown ID
156+
*/
157+
async getMarkdownDocx(markdownId: string) {
158+
const response = await authService.makeAuthenticatedRequest(
159+
`/api/markdowns/${markdownId}/docx`,
160+
);
161+
162+
if (!response.ok) {
163+
throw new Error(`HTTP error! status: ${response.status}`);
164+
}
165+
166+
return response.blob();
167+
},
168+
169+
/**
170+
* Get field values from server
171+
*/
172+
async getFieldValues() {
173+
return ApiRequestMixin.request<FieldValues>("/api/meta/field-values");
174+
},
175+
176+
/**
177+
* Get current environment from server
178+
*/
179+
async getEnvironment() {
180+
return ApiRequestMixin.request<{ environment: string }>(
181+
"/api/meta/environment",
182+
);
183+
},
184+
185+
/**
186+
* Upload PDF for the 2-step activity creation flow.
187+
*/
188+
async uploadPdfDraft(file: File, options: UploadPdfDraftOptions = {}) {
189+
const formData = new FormData();
190+
formData.append("pdf_file", file);
191+
formData.append("extractMetadata", String(options.extractMetadata ?? true));
192+
193+
return ApiRequestMixin.request<UploadPdfDraftResponse>(
194+
"/api/activities/upload-pdf-draft",
195+
{
196+
method: "POST",
197+
body: formData,
198+
},
199+
);
200+
},
201+
202+
async regenerateMetadata(documentId: string) {
203+
return ApiRequestMixin.request<UploadPdfDraftResponse>(
204+
"/api/activities/regenerate-metadata",
205+
{
206+
method: "POST",
207+
headers: { "Content-Type": "application/json" },
208+
body: JSON.stringify({ documentId }),
209+
},
210+
);
211+
},
212+
213+
/**
214+
* Generate Artikulationsschema markdown from an uploaded PDF.
215+
*/
216+
async generateArtikulationsschema(
217+
documentId: string,
218+
metadata?: Record<string, unknown>,
219+
) {
220+
return ApiRequestMixin.request<ArtikulationsschemaResponse>(
221+
"/api/activities/generate-artikulationsschema",
222+
{
223+
method: "POST",
224+
headers: { "Content-Type": "application/json" },
225+
body: JSON.stringify({ documentId: documentId, metadata: metadata }),
226+
},
227+
);
228+
},
229+
230+
/**
231+
* Generate all activity markdowns (Deckblatt, Artikulationsschema, Hintergrundwissen).
232+
*/
233+
async generateActivityMarkdowns(
234+
documentId: string,
235+
metadata?: Record<string, unknown>,
236+
types?: string[],
237+
) {
238+
return ApiRequestMixin.request<ActivityMarkdownsResponse>(
239+
"/api/activities/generate-activity-markdowns",
240+
{
241+
method: "POST",
242+
headers: { "Content-Type": "application/json" },
243+
body: JSON.stringify({
244+
documentId: documentId,
245+
metadata: metadata,
246+
types: types,
247+
}),
248+
},
249+
);
250+
},
251+
252+
/**
253+
* Download combined activity PDF
254+
*/
255+
async downloadActivityPdf(activityId: string) {
256+
const response = await authService.makeAuthenticatedRequest(
257+
`/api/activities/${activityId}/download-pdf`,
258+
);
259+
260+
if (!response.ok) {
261+
throw new Error(`HTTP error! status: ${response.status}`);
262+
}
263+
264+
return response.blob();
265+
},
266+
267+
/**
268+
* Download combined activity DOCX
269+
*/
270+
async downloadActivityDocx(activityId: string) {
271+
const response = await authService.makeAuthenticatedRequest(
272+
`/api/activities/${activityId}/download-docx`,
273+
);
274+
275+
if (!response.ok) {
276+
throw new Error(`HTTP error! status: ${response.status}`);
277+
}
278+
279+
return response.blob();
280+
},
281+
282+
/**
283+
* Render markdown text to a preview PDF.
284+
*/
285+
async previewMarkdownPdf(
286+
markdown: string,
287+
orientation?: "portrait" | "landscape",
288+
activityName?: string,
289+
) {
290+
const response = await authService.makeAuthenticatedRequest(
291+
"/api/markdowns/preview-pdf",
292+
{
293+
method: "POST",
294+
headers: { "Content-Type": "application/json" },
295+
body: JSON.stringify({ markdown, orientation, activityName }),
296+
},
297+
);
298+
299+
if (!response.ok) {
300+
const errorData = await response.json().catch(() => ({}));
301+
throw new Error(
302+
(errorData as Record<string, string>).error ||
303+
`HTTP error! status: ${response.status}`,
304+
);
305+
}
306+
307+
return response.blob();
308+
},
309+
};

0 commit comments

Comments
 (0)