added apiClient class handling api auth and validation response from backend

This commit is contained in:
2025-11-01 13:47:29 +01:00
parent 29c66f0228
commit 41ddad6fa9
7 changed files with 267 additions and 468 deletions

View File

@@ -51,39 +51,26 @@ if (isDevelopment) {
}
const configureStaticFiles = () => {
const possiblePaths = [
// Production path (Docker)
'/app/frontend-build',
// Development paths
path.resolve(__dirname, '../../frontend/dist'),
path.resolve(__dirname, '../frontend/dist'), // Added for monorepo
path.resolve(process.cwd(), 'frontend/dist'), // Current directory
path.resolve(process.cwd(), '../frontend/dist'), // Parent directory
// Vite dev server fallback
...(isDevelopment ? [path.resolve(__dirname, '../../frontend')] : [])
];
const staticConfig = {
maxAge: '1y',
etag: false,
immutable: true,
index: false
};
for (const testPath of possiblePaths) {
try {
if (fs.existsSync(testPath)) {
// In development, check for dist or direct source
if (fs.existsSync(path.join(testPath, 'index.html'))) {
console.log('✅ Found frontend at:', testPath);
app.use(express.static(testPath));
return testPath;
}
// For Vite dev server in development
else if (isDevelopment && fs.existsSync(path.join(testPath, 'index.html'))) {
console.log('🔧 Development: Serving frontend source from:', testPath);
app.use(express.static(testPath));
return testPath;
}
}
} catch (error) {
// Silent catch
}
// Serve frontend build
const frontendPath = '/app/frontend-build';
if (fs.existsSync(frontendPath)) {
console.log('✅ Serving frontend from:', frontendPath);
app.use(express.static(frontendPath, staticConfig));
}
// Serve premium assets if available
const premiumPath = '/app/premium-dist';
if (fs.existsSync(premiumPath)) {
console.log('✅ Serving premium assets from:', premiumPath);
app.use('/premium-assets', express.static(premiumPath, staticConfig));
}
return null;
};
// Security configuration
@@ -225,7 +212,7 @@ const findFrontendBuildPath = (): string | null => {
};
const frontendBuildPath = findFrontendBuildPath();
const staticPath = configureStaticFiles();
configureStaticFiles();
if (frontendBuildPath) {
app.use(express.static(frontendBuildPath));
@@ -264,32 +251,26 @@ app.get('/', async (req, res) => {
});
// Client-side routing fallback
app.get('*', async (req, res, next) => {
app.get('*', (req, res, next) => {
// Skip API routes
if (req.path.startsWith('/api/')) {
return next(); // API routes handled normally
return next();
}
// Vite dev server handling
if (vite) {
try {
const template = fs.readFileSync(
path.resolve(__dirname, '../../frontend/index.html'),
'utf-8'
);
const html = await vite.transformIndexHtml(req.url, template);
return res.send(html);
} catch (error) {
console.error('Vite transformation error:', error);
}
// Skip file extensions (assets)
if (req.path.match(/\.[a-z0-9]+$/i)) {
return next();
}
// Static file fallback
if (staticPath) {
const indexPath = path.join(staticPath, 'index.html');
return res.sendFile(indexPath);
}
// Serve React app for all other routes
const frontendPath = '/app/frontend-build';
const indexPath = path.join(frontendPath, 'index.html');
res.status(500).send('Frontend not available');
if (fs.existsSync(indexPath)) {
res.sendFile(indexPath);
} else {
res.status(404).send('Frontend not available');
}
});
// Error handling

View File

@@ -0,0 +1,130 @@
import { ValidationError, ErrorService } from './errorService';
export class ApiError extends Error {
public validationErrors: ValidationError[];
public statusCode: number;
public originalError?: any;
constructor(message: string, validationErrors: ValidationError[] = [], statusCode: number = 0, originalError?: any) {
super(message);
this.name = 'ApiError';
this.validationErrors = validationErrors;
this.statusCode = statusCode;
this.originalError = originalError;
}
}
export class ApiClient {
private baseURL: string;
constructor() {
this.baseURL = import.meta.env.VITE_API_URL || '/api';
}
private getAuthHeaders(): HeadersInit {
const token = localStorage.getItem('token');
return token ? { 'Authorization': `Bearer ${token}` } : {};
}
private async handleApiResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
let errorData;
try {
// Try to parse error response as JSON
const responseText = await response.text();
errorData = responseText ? JSON.parse(responseText) : {};
} catch {
// If not JSON, create a generic error object
errorData = { error: `HTTP ${response.status}: ${response.statusText}` };
}
// Extract validation errors using your existing ErrorService
const validationErrors = ErrorService.extractValidationErrors(errorData);
if (validationErrors.length > 0) {
// Throw error with validationErrors property for useBackendValidation hook
throw new ApiError(
errorData.error || 'Validation failed',
validationErrors,
response.status,
errorData
);
}
// Throw regular error for non-validation errors
throw new ApiError(
errorData.error || errorData.message || `HTTP error! status: ${response.status}`,
[],
response.status,
errorData
);
}
// For successful responses, try to parse as JSON
try {
const responseText = await response.text();
return responseText ? JSON.parse(responseText) : {} as T;
} catch (error) {
// If response is not JSON but request succeeded (e.g., 204 No Content)
return {} as T;
}
}
async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseURL}${endpoint}`;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
return await this.handleApiResponse<T>(response);
} catch (error) {
// Re-throw the error to be caught by useBackendValidation
if (error instanceof ApiError) {
throw error;
}
// Wrap non-ApiError errors
throw new ApiError(
error instanceof Error ? error.message : 'Unknown error occurred',
[],
0,
error
);
}
}
// Standardized HTTP methods
get = <T>(endpoint: string) => this.request<T>(endpoint);
post = <T>(endpoint: string, data?: any) =>
this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined
});
put = <T>(endpoint: string, data?: any) =>
this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined
});
patch = <T>(endpoint: string, data?: any) =>
this.request<T>(endpoint, {
method: 'PATCH',
body: data ? JSON.stringify(data) : undefined
});
delete = <T>(endpoint: string) =>
this.request<T>(endpoint, { method: 'DELETE' });
}
export const apiClient = new ApiClient();

View File

@@ -1,8 +1,5 @@
// frontend/src/services/authService.ts - UPDATED
import { Employee } from '../models/Employee';
import { ErrorService } from './errorService';
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
import { apiClient } from './apiClient';
export interface LoginRequest {
email: string;
@@ -25,31 +22,8 @@ export interface AuthResponse {
class AuthService {
private token: string | null = null;
private async handleApiResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const validationErrors = ErrorService.extractValidationErrors(errorData);
if (validationErrors.length > 0) {
const error = new Error('Validation failed');
(error as any).validationErrors = validationErrors;
throw error;
}
throw new Error(errorData.error || errorData.message || 'Authentication failed');
}
return response.json();
}
async login(credentials: LoginRequest): Promise<AuthResponse> {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const data = await this.handleApiResponse<AuthResponse>(response);
const data = await apiClient.post<AuthResponse>('/auth/login', credentials);
this.token = data.token;
localStorage.setItem('token', data.token);
localStorage.setItem('employee', JSON.stringify(data.employee));
@@ -57,13 +31,7 @@ class AuthService {
}
async register(userData: RegisterRequest): Promise<AuthResponse> {
const response = await fetch(`${API_BASE_URL}/employees`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
const data = await this.handleApiResponse<AuthResponse>(response);
await apiClient.post('/employees', userData);
return this.login({
email: userData.email,
password: userData.password
@@ -77,28 +45,16 @@ class AuthService {
async fetchCurrentEmployee(): Promise<Employee | null> {
const token = this.getToken();
if (!token) {
return null;
}
if (!token) return null;
try {
const response = await fetch(`${API_BASE_URL}/auth/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
const user = data.user;
localStorage.setItem('user', JSON.stringify(user));
return user;
}
const data = await apiClient.get<{ user: Employee }>('/auth/me');
localStorage.setItem('user', JSON.stringify(data.user));
return data.user;
} catch (error) {
console.error('Error fetching current user:', error);
return null;
}
return null;
}
logout(): void {

View File

@@ -1,138 +1,58 @@
// frontend/src/services/employeeService.ts
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest, EmployeeAvailability } from '../models/Employee';
import { ErrorService, ValidationError } from './errorService';
const API_BASE_URL = '/api';
const getAuthHeaders = () => {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
};
import { apiClient } from './apiClient';
export class EmployeeService {
private async handleApiResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const validationErrors = ErrorService.extractValidationErrors(errorData);
if (validationErrors.length > 0) {
const error = new Error('Validation failed');
(error as any).validationErrors = validationErrors;
throw error;
}
throw new Error(errorData.error || errorData.message || `HTTP error! status: ${response.status}`);
}
return response.json();
}
async getEmployees(includeInactive: boolean = false): Promise<Employee[]> {
console.log('🔄 Fetching employees from API...');
const token = localStorage.getItem('token');
console.log('🔑 Token exists:', !!token);
const response = await fetch(`${API_BASE_URL}/employees?includeInactive=${includeInactive}`, {
headers: getAuthHeaders(),
});
console.log('📡 Response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('❌ API Error:', errorText);
throw new Error('Failed to fetch employees');
try {
const employees = await apiClient.get<Employee[]>(`/employees?includeInactive=${includeInactive}`);
console.log('✅ Employees received:', employees.length);
return employees;
} catch (error) {
console.error('❌ Error fetching employees:', error);
throw error; // Let useBackendValidation handle this
}
const employees = await response.json();
console.log('✅ Employees received:', employees.length);
return employees;
}
async getEmployee(id: string): Promise<Employee> {
const response = await fetch(`${API_BASE_URL}/employees/${id}`, {
headers: getAuthHeaders(),
});
return this.handleApiResponse<Employee>(response);
return apiClient.get<Employee>(`/employees/${id}`);
}
async createEmployee(employee: CreateEmployeeRequest): Promise<Employee> {
const response = await fetch(`${API_BASE_URL}/employees`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(employee),
});
return this.handleApiResponse<Employee>(response);
return apiClient.post<Employee>('/employees', employee);
}
async updateEmployee(id: string, employee: UpdateEmployeeRequest): Promise<Employee> {
const response = await fetch(`${API_BASE_URL}/employees/${id}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(employee),
});
return this.handleApiResponse<Employee>(response);
return apiClient.put<Employee>(`/employees/${id}`, employee);
}
async deleteEmployee(id: string): Promise<void> {
const response = await fetch(`${API_BASE_URL}/employees/${id}`, {
method: 'DELETE',
headers: getAuthHeaders(),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to delete employee');
}
await apiClient.delete(`/employees/${id}`);
}
async getAvailabilities(employeeId: string): Promise<EmployeeAvailability[]> {
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/availabilities`, {
headers: getAuthHeaders(),
});
return this.handleApiResponse<EmployeeAvailability[]>(response);
return apiClient.get<EmployeeAvailability[]>(`/employees/${employeeId}/availabilities`);
}
async updateAvailabilities(employeeId: string, data: { planId: string, availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[] }): Promise<EmployeeAvailability[]> {
async updateAvailabilities(
employeeId: string,
data: { planId: string, availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[] }
): Promise<EmployeeAvailability[]> {
console.log('🔄 Updating availabilities for employee:', employeeId);
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/availabilities`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return this.handleApiResponse<EmployeeAvailability[]>(response);
return apiClient.put<EmployeeAvailability[]>(`/employees/${employeeId}/availabilities`, data);
}
async changePassword(id: string, data: { currentPassword: string, newPassword: string, confirmPassword: string }): Promise<void> {
const response = await fetch(`${API_BASE_URL}/employees/${id}/password`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return this.handleApiResponse<void>(response);
async changePassword(
id: string,
data: { currentPassword: string, newPassword: string, confirmPassword: string }
): Promise<void> {
return apiClient.put<void>(`/employees/${id}/password`, data);
}
async updateLastLogin(employeeId: string): Promise<void> {
try {
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/last-login`, {
method: 'PATCH',
headers: getAuthHeaders(),
});
if (!response.ok) {
throw new Error('Failed to update last login');
}
await apiClient.patch(`/employees/${employeeId}/last-login`);
} catch (error) {
console.error('Error updating last login:', error);
throw error;

View File

@@ -1,65 +1,15 @@
// frontend/src/services/shiftAssignmentService.ts - WEEKLY PATTERN VERSION
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../models/Employee';
import { authService } from './authService';
import { AssignmentResult, ScheduleRequest } from '../models/scheduling';
const API_BASE_URL = '/api';
// Helper function to get auth headers
const getAuthHeaders = () => {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` })
};
};
import { apiClient } from './apiClient';
export class ShiftAssignmentService {
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
try {
//console.log('🔄 Updating scheduled shift via API:', { id, updates });
console.log('🔄 Updating scheduled shift via API:', { id, updates });
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(updates)
});
// First, check if we got any response
if (!response.ok) {
// Try to get error message from response
const responseText = await response.text();
console.error('❌ Server response:', responseText);
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
// Try to parse as JSON if possible
try {
const errorData = JSON.parse(responseText);
errorMessage = errorData.error || errorMessage;
} catch (e) {
// If not JSON, use the text as is
errorMessage = responseText || errorMessage;
}
throw new Error(errorMessage);
}
// Try to parse successful response
const responseText = await response.text();
let result;
try {
result = responseText ? JSON.parse(responseText) : {};
} catch (e) {
console.warn('⚠️ Response was not JSON, but request succeeded');
result = { message: 'Update successful' };
}
console.log('✅ Scheduled shift updated successfully:', result);
await apiClient.put(`/scheduled-shifts/${id}`, updates);
console.log('✅ Scheduled shift updated successfully');
} catch (error) {
console.error('❌ Error updating scheduled shift:', error);
@@ -69,48 +19,16 @@ export class ShiftAssignmentService {
async getScheduledShift(id: string): Promise<any> {
try {
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/${id}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
const responseText = await response.text();
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
try {
const errorData = JSON.parse(responseText);
errorMessage = errorData.error || errorMessage;
} catch (e) {
errorMessage = responseText || errorMessage;
}
throw new Error(errorMessage);
}
const responseText = await response.text();
return responseText ? JSON.parse(responseText) : {};
return await apiClient.get(`/scheduled-shifts/${id}`);
} catch (error) {
console.error('Error fetching scheduled shift:', error);
throw error;
}
}
// New method to get all scheduled shifts for a plan
async getScheduledShiftsForPlan(planId: string): Promise<ScheduledShift[]> {
try {
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/plan/${planId}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch scheduled shifts: ${response.status}`);
}
const shifts = await response.json();
const shifts = await apiClient.get<ScheduledShift[]>(`/scheduled-shifts/plan/${planId}`);
// DEBUG: Check the structure of returned shifts
console.log('🔍 SCHEDULED SHIFTS STRUCTURE:', shifts.slice(0, 3));
@@ -132,21 +50,7 @@ export class ShiftAssignmentService {
}
private async callSchedulingAPI(request: ScheduleRequest): Promise<AssignmentResult> {
const response = await fetch(`${API_BASE_URL}/scheduling/generate-schedule`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(request)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Scheduling failed');
}
return response.json();
return await apiClient.post<AssignmentResult>('/scheduling/generate-schedule', request);
}
async assignShifts(

View File

@@ -1,199 +1,115 @@
// frontend/src/services/shiftPlanService.ts
import { authService } from './authService';
import { ShiftPlan, CreateShiftPlanRequest } from '../models/ShiftPlan';
import { TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults';
const API_BASE_URL = '/api/shift-plans';
// Helper function to get auth headers
const getAuthHeaders = () => {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` })
};
};
// Helper function to handle responses
const handleResponse = async (response: Response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
return response.json();
};
import { apiClient } from './apiClient';
export const shiftPlanService = {
async getShiftPlans(): Promise<ShiftPlan[]> {
const response = await fetch(API_BASE_URL, {
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
try {
const plans = await apiClient.get<ShiftPlan[]>('/shift-plans');
if (!response.ok) {
if (response.status === 401) {
authService.logout();
// Ensure scheduledShifts is always an array
return plans.map((plan: any) => ({
...plan,
scheduledShifts: plan.scheduledShifts || []
}));
} catch (error: any) {
if (error.statusCode === 401) {
// You might want to import and use authService here if needed
localStorage.removeItem('token');
localStorage.removeItem('employee');
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Laden der Schichtpläne');
}
const plans = await response.json();
// Ensure scheduledShifts is always an array
return plans.map((plan: any) => ({
...plan,
scheduledShifts: plan.scheduledShifts || []
}));
},
async getShiftPlan(id: string): Promise<ShiftPlan> {
const response = await fetch(`${API_BASE_URL}/${id}`, {
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
try {
return await apiClient.get<ShiftPlan>(`/shift-plans/${id}`);
} catch (error: any) {
if (error.statusCode === 401) {
localStorage.removeItem('token');
localStorage.removeItem('employee');
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Schichtplan nicht gefunden');
}
return await response.json();
},
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
const response = await fetch(API_BASE_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(plan)
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
try {
return await apiClient.post<ShiftPlan>('/shift-plans', plan);
} catch (error: any) {
if (error.statusCode === 401) {
localStorage.removeItem('token');
localStorage.removeItem('employee');
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Erstellen des Schichtplans');
}
return response.json();
},
async updateShiftPlan(id: string, plan: Partial<ShiftPlan>): Promise<ShiftPlan> {
const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(plan)
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
try {
return await apiClient.put<ShiftPlan>(`/shift-plans/${id}`, plan);
} catch (error: any) {
if (error.statusCode === 401) {
localStorage.removeItem('token');
localStorage.removeItem('employee');
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Aktualisieren des Schichtplans');
}
return response.json();
},
async deleteShiftPlan(id: string): Promise<void> {
const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
try {
await apiClient.delete(`/shift-plans/${id}`);
} catch (error: any) {
if (error.statusCode === 401) {
localStorage.removeItem('token');
localStorage.removeItem('employee');
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Löschen des Schichtplans');
}
},
// Get specific template or plan
getTemplate: async (id: string): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE_URL}/${id}`, {
headers: getAuthHeaders()
});
return handleResponse(response);
async getTemplate(id: string): Promise<ShiftPlan> {
return await apiClient.get<ShiftPlan>(`/shift-plans/${id}`);
},
async regenerateScheduledShifts(planId: string):Promise<void> {
async regenerateScheduledShifts(planId: string): Promise<void> {
try {
console.log('🔄 Attempting to regenerate scheduled shifts...');
// You'll need to add this API endpoint to your backend
const response = await fetch(`${API_BASE_URL}/${planId}/regenerate-shifts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (response.ok) {
console.log('✅ Scheduled shifts regenerated');
} else {
console.error('❌ Failed to regenerate shifts');
}
console.log('🔄 Attempting to regenerate scheduled shifts...');
await apiClient.post(`/shift-plans/${planId}/regenerate-shifts`);
console.log('✅ Scheduled shifts regenerated');
} catch (error) {
console.error('❌ Error regenerating shifts:', error);
console.error('❌ Error regenerating shifts:', error);
throw error;
}
},
// Create new plan
createPlan: async (data: CreateShiftPlanRequest): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE_URL}`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return handleResponse(response);
async createPlan(data: CreateShiftPlanRequest): Promise<ShiftPlan> {
return await apiClient.post<ShiftPlan>('/shift-plans', data);
},
createFromPreset: async (data: {
async createFromPreset(data: {
presetName: string;
name: string;
startDate: string;
endDate: string;
isTemplate?: boolean;
}): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE_URL}/from-preset`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}): Promise<ShiftPlan> {
try {
return await apiClient.post<ShiftPlan>('/shift-plans/from-preset', data);
} catch (error: any) {
throw new Error(error.message || `HTTP error! status: ${error.statusCode}`);
}
return response.json();
},
getTemplatePresets: async (): Promise<{name: string, label: string, description: string}[]> => {
// name = label
return Object.entries(TEMPLATE_PRESETS).map(([key, preset]) => ({
async getTemplatePresets(): Promise<{name: string, label: string, description: string}[]> {
return Object.entries(TEMPLATE_PRESETS).map(([key, preset]) => ({
name: key,
label: preset.name,
description: preset.description
@@ -203,22 +119,8 @@ export const shiftPlanService = {
async clearAssignments(planId: string): Promise<void> {
try {
console.log('🔄 Clearing assignments for plan:', planId);
const response = await fetch(`${API_BASE_URL}/${planId}/clear-assignments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `Failed to clear assignments: ${response.status}`);
}
await apiClient.post(`/shift-plans/${planId}/clear-assignments`);
console.log('✅ Assignments cleared successfully');
} catch (error) {
console.error('❌ Error clearing assignments:', error);
throw error;

View File

@@ -59,7 +59,13 @@ export default defineConfig(({ mode }) => {
resolve: {
alias: {
'@': resolve(__dirname, './src'),
// ... other aliases
'@/components': resolve(__dirname, './src/components'),
'@/pages': resolve(__dirname, './src/pages'),
'@/contexts': resolve(__dirname, './src/contexts'),
'@/models': resolve(__dirname, './src/models'),
'@/utils': resolve(__dirname, './src/utils'),
'@/services': resolve(__dirname, './src/services'),
'@/design': resolve(__dirname, './src/design')
}
},