mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
added apiClient class handling api auth and validation response from backend
This commit is contained in:
@@ -51,39 +51,26 @@ if (isDevelopment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const configureStaticFiles = () => {
|
const configureStaticFiles = () => {
|
||||||
const possiblePaths = [
|
const staticConfig = {
|
||||||
// Production path (Docker)
|
maxAge: '1y',
|
||||||
'/app/frontend-build',
|
etag: false,
|
||||||
// Development paths
|
immutable: true,
|
||||||
path.resolve(__dirname, '../../frontend/dist'),
|
index: false
|
||||||
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')] : [])
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const testPath of possiblePaths) {
|
// Serve frontend build
|
||||||
try {
|
const frontendPath = '/app/frontend-build';
|
||||||
if (fs.existsSync(testPath)) {
|
if (fs.existsSync(frontendPath)) {
|
||||||
// In development, check for dist or direct source
|
console.log('✅ Serving frontend from:', frontendPath);
|
||||||
if (fs.existsSync(path.join(testPath, 'index.html'))) {
|
app.use(express.static(frontendPath, staticConfig));
|
||||||
console.log('✅ Found frontend at:', testPath);
|
}
|
||||||
app.use(express.static(testPath));
|
|
||||||
return testPath;
|
// Serve premium assets if available
|
||||||
}
|
const premiumPath = '/app/premium-dist';
|
||||||
// For Vite dev server in development
|
if (fs.existsSync(premiumPath)) {
|
||||||
else if (isDevelopment && fs.existsSync(path.join(testPath, 'index.html'))) {
|
console.log('✅ Serving premium assets from:', premiumPath);
|
||||||
console.log('🔧 Development: Serving frontend source from:', testPath);
|
app.use('/premium-assets', express.static(premiumPath, staticConfig));
|
||||||
app.use(express.static(testPath));
|
|
||||||
return testPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Silent catch
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Security configuration
|
// Security configuration
|
||||||
@@ -225,7 +212,7 @@ const findFrontendBuildPath = (): string | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const frontendBuildPath = findFrontendBuildPath();
|
const frontendBuildPath = findFrontendBuildPath();
|
||||||
const staticPath = configureStaticFiles();
|
configureStaticFiles();
|
||||||
|
|
||||||
if (frontendBuildPath) {
|
if (frontendBuildPath) {
|
||||||
app.use(express.static(frontendBuildPath));
|
app.use(express.static(frontendBuildPath));
|
||||||
@@ -264,32 +251,26 @@ app.get('/', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Client-side routing fallback
|
// Client-side routing fallback
|
||||||
app.get('*', async (req, res, next) => {
|
app.get('*', (req, res, next) => {
|
||||||
|
// Skip API routes
|
||||||
if (req.path.startsWith('/api/')) {
|
if (req.path.startsWith('/api/')) {
|
||||||
return next(); // API routes handled normally
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vite dev server handling
|
// Skip file extensions (assets)
|
||||||
if (vite) {
|
if (req.path.match(/\.[a-z0-9]+$/i)) {
|
||||||
try {
|
return next();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static file fallback
|
// Serve React app for all other routes
|
||||||
if (staticPath) {
|
const frontendPath = '/app/frontend-build';
|
||||||
const indexPath = path.join(staticPath, 'index.html');
|
const indexPath = path.join(frontendPath, 'index.html');
|
||||||
return res.sendFile(indexPath);
|
|
||||||
|
if (fs.existsSync(indexPath)) {
|
||||||
|
res.sendFile(indexPath);
|
||||||
|
} else {
|
||||||
|
res.status(404).send('Frontend not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(500).send('Frontend not available');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
|
|||||||
130
frontend/src/services/apiClient.ts
Normal file
130
frontend/src/services/apiClient.ts
Normal 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();
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
// frontend/src/services/authService.ts - UPDATED
|
|
||||||
import { Employee } from '../models/Employee';
|
import { Employee } from '../models/Employee';
|
||||||
import { ErrorService } from './errorService';
|
import { apiClient } from './apiClient';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
|
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -25,31 +22,8 @@ export interface AuthResponse {
|
|||||||
class AuthService {
|
class AuthService {
|
||||||
private token: string | null = null;
|
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> {
|
async login(credentials: LoginRequest): Promise<AuthResponse> {
|
||||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
const data = await apiClient.post<AuthResponse>('/auth/login', credentials);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(credentials)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await this.handleApiResponse<AuthResponse>(response);
|
|
||||||
this.token = data.token;
|
this.token = data.token;
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
localStorage.setItem('employee', JSON.stringify(data.employee));
|
localStorage.setItem('employee', JSON.stringify(data.employee));
|
||||||
@@ -57,13 +31,7 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
||||||
const response = await fetch(`${API_BASE_URL}/employees`, {
|
await apiClient.post('/employees', userData);
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(userData)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await this.handleApiResponse<AuthResponse>(response);
|
|
||||||
return this.login({
|
return this.login({
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
password: userData.password
|
password: userData.password
|
||||||
@@ -77,28 +45,16 @@ class AuthService {
|
|||||||
|
|
||||||
async fetchCurrentEmployee(): Promise<Employee | null> {
|
async fetchCurrentEmployee(): Promise<Employee | null> {
|
||||||
const token = this.getToken();
|
const token = this.getToken();
|
||||||
if (!token) {
|
if (!token) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/auth/me`, {
|
const data = await apiClient.get<{ user: Employee }>('/auth/me');
|
||||||
headers: {
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
'Authorization': `Bearer ${token}`
|
return data.user;
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
const user = data.user;
|
|
||||||
localStorage.setItem('user', JSON.stringify(user));
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching current user:', error);
|
console.error('Error fetching current user:', error);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): void {
|
logout(): void {
|
||||||
|
|||||||
@@ -1,138 +1,58 @@
|
|||||||
// frontend/src/services/employeeService.ts
|
|
||||||
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest, EmployeeAvailability } from '../models/Employee';
|
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest, EmployeeAvailability } from '../models/Employee';
|
||||||
import { ErrorService, ValidationError } from './errorService';
|
import { apiClient } from './apiClient';
|
||||||
|
|
||||||
const API_BASE_URL = '/api';
|
|
||||||
|
|
||||||
const getAuthHeaders = () => {
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
return {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export class EmployeeService {
|
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[]> {
|
async getEmployees(includeInactive: boolean = false): Promise<Employee[]> {
|
||||||
console.log('🔄 Fetching employees from API...');
|
console.log('🔄 Fetching employees from API...');
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
try {
|
||||||
console.log('🔑 Token exists:', !!token);
|
const employees = await apiClient.get<Employee[]>(`/employees?includeInactive=${includeInactive}`);
|
||||||
|
console.log('✅ Employees received:', employees.length);
|
||||||
const response = await fetch(`${API_BASE_URL}/employees?includeInactive=${includeInactive}`, {
|
return employees;
|
||||||
headers: getAuthHeaders(),
|
} catch (error) {
|
||||||
});
|
console.error('❌ Error fetching employees:', error);
|
||||||
|
throw error; // Let useBackendValidation handle this
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const employees = await response.json();
|
|
||||||
console.log('✅ Employees received:', employees.length);
|
|
||||||
|
|
||||||
return employees;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEmployee(id: string): Promise<Employee> {
|
async getEmployee(id: string): Promise<Employee> {
|
||||||
const response = await fetch(`${API_BASE_URL}/employees/${id}`, {
|
return apiClient.get<Employee>(`/employees/${id}`);
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.handleApiResponse<Employee>(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createEmployee(employee: CreateEmployeeRequest): Promise<Employee> {
|
async createEmployee(employee: CreateEmployeeRequest): Promise<Employee> {
|
||||||
const response = await fetch(`${API_BASE_URL}/employees`, {
|
return apiClient.post<Employee>('/employees', employee);
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(employee),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.handleApiResponse<Employee>(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateEmployee(id: string, employee: UpdateEmployeeRequest): Promise<Employee> {
|
async updateEmployee(id: string, employee: UpdateEmployeeRequest): Promise<Employee> {
|
||||||
const response = await fetch(`${API_BASE_URL}/employees/${id}`, {
|
return apiClient.put<Employee>(`/employees/${id}`, employee);
|
||||||
method: 'PUT',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(employee),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.handleApiResponse<Employee>(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteEmployee(id: string): Promise<void> {
|
async deleteEmployee(id: string): Promise<void> {
|
||||||
const response = await fetch(`${API_BASE_URL}/employees/${id}`, {
|
await apiClient.delete(`/employees/${id}`);
|
||||||
method: 'DELETE',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json();
|
|
||||||
throw new Error(error.error || 'Failed to delete employee');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailabilities(employeeId: string): Promise<EmployeeAvailability[]> {
|
async getAvailabilities(employeeId: string): Promise<EmployeeAvailability[]> {
|
||||||
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/availabilities`, {
|
return apiClient.get<EmployeeAvailability[]>(`/employees/${employeeId}/availabilities`);
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.handleApiResponse<EmployeeAvailability[]>(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
console.log('🔄 Updating availabilities for employee:', employeeId);
|
||||||
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/availabilities`, {
|
return apiClient.put<EmployeeAvailability[]>(`/employees/${employeeId}/availabilities`, data);
|
||||||
method: 'PUT',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.handleApiResponse<EmployeeAvailability[]>(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async changePassword(id: string, data: { currentPassword: string, newPassword: string, confirmPassword: string }): Promise<void> {
|
async changePassword(
|
||||||
const response = await fetch(`${API_BASE_URL}/employees/${id}/password`, {
|
id: string,
|
||||||
method: 'PUT',
|
data: { currentPassword: string, newPassword: string, confirmPassword: string }
|
||||||
headers: getAuthHeaders(),
|
): Promise<void> {
|
||||||
body: JSON.stringify(data),
|
return apiClient.put<void>(`/employees/${id}/password`, data);
|
||||||
});
|
|
||||||
|
|
||||||
return this.handleApiResponse<void>(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLastLogin(employeeId: string): Promise<void> {
|
async updateLastLogin(employeeId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/last-login`, {
|
await apiClient.patch(`/employees/${employeeId}/last-login`);
|
||||||
method: 'PATCH',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to update last login');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating last login:', error);
|
console.error('Error updating last login:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,65 +1,15 @@
|
|||||||
// frontend/src/services/shiftAssignmentService.ts - WEEKLY PATTERN VERSION
|
|
||||||
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
|
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
|
||||||
import { Employee, EmployeeAvailability } from '../models/Employee';
|
import { Employee, EmployeeAvailability } from '../models/Employee';
|
||||||
import { authService } from './authService';
|
|
||||||
import { AssignmentResult, ScheduleRequest } from '../models/scheduling';
|
import { AssignmentResult, ScheduleRequest } from '../models/scheduling';
|
||||||
|
import { apiClient } from './apiClient';
|
||||||
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}` })
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ShiftAssignmentService {
|
export class ShiftAssignmentService {
|
||||||
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
|
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
|
||||||
try {
|
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}`, {
|
await apiClient.put(`/scheduled-shifts/${id}`, updates);
|
||||||
method: 'PUT',
|
console.log('✅ Scheduled shift updated successfully');
|
||||||
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);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error updating scheduled shift:', error);
|
console.error('❌ Error updating scheduled shift:', error);
|
||||||
@@ -69,48 +19,16 @@ export class ShiftAssignmentService {
|
|||||||
|
|
||||||
async getScheduledShift(id: string): Promise<any> {
|
async getScheduledShift(id: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/${id}`, {
|
return await apiClient.get(`/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) : {};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching scheduled shift:', error);
|
console.error('Error fetching scheduled shift:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New method to get all scheduled shifts for a plan
|
|
||||||
async getScheduledShiftsForPlan(planId: string): Promise<ScheduledShift[]> {
|
async getScheduledShiftsForPlan(planId: string): Promise<ScheduledShift[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/plan/${planId}`, {
|
const shifts = await apiClient.get<ScheduledShift[]>(`/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();
|
|
||||||
|
|
||||||
// DEBUG: Check the structure of returned shifts
|
// DEBUG: Check the structure of returned shifts
|
||||||
console.log('🔍 SCHEDULED SHIFTS STRUCTURE:', shifts.slice(0, 3));
|
console.log('🔍 SCHEDULED SHIFTS STRUCTURE:', shifts.slice(0, 3));
|
||||||
@@ -132,21 +50,7 @@ export class ShiftAssignmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async callSchedulingAPI(request: ScheduleRequest): Promise<AssignmentResult> {
|
private async callSchedulingAPI(request: ScheduleRequest): Promise<AssignmentResult> {
|
||||||
const response = await fetch(`${API_BASE_URL}/scheduling/generate-schedule`, {
|
return await apiClient.post<AssignmentResult>('/scheduling/generate-schedule', request);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async assignShifts(
|
async assignShifts(
|
||||||
|
|||||||
@@ -1,199 +1,115 @@
|
|||||||
// frontend/src/services/shiftPlanService.ts
|
|
||||||
import { authService } from './authService';
|
|
||||||
import { ShiftPlan, CreateShiftPlanRequest } from '../models/ShiftPlan';
|
import { ShiftPlan, CreateShiftPlanRequest } from '../models/ShiftPlan';
|
||||||
import { TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults';
|
import { TEMPLATE_PRESETS } from '../models/defaults/shiftPlanDefaults';
|
||||||
|
import { apiClient } from './apiClient';
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const shiftPlanService = {
|
export const shiftPlanService = {
|
||||||
async getShiftPlans(): Promise<ShiftPlan[]> {
|
async getShiftPlans(): Promise<ShiftPlan[]> {
|
||||||
const response = await fetch(API_BASE_URL, {
|
try {
|
||||||
headers: {
|
const plans = await apiClient.get<ShiftPlan[]>('/shift-plans');
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...authService.getAuthHeaders()
|
// Ensure scheduledShifts is always an array
|
||||||
}
|
return plans.map((plan: any) => ({
|
||||||
});
|
...plan,
|
||||||
|
scheduledShifts: plan.scheduledShifts || []
|
||||||
if (!response.ok) {
|
}));
|
||||||
if (response.status === 401) {
|
} catch (error: any) {
|
||||||
authService.logout();
|
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('Nicht authorisiert - bitte erneut anmelden');
|
||||||
}
|
}
|
||||||
throw new Error('Fehler beim Laden der Schichtpläne');
|
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> {
|
async getShiftPlan(id: string): Promise<ShiftPlan> {
|
||||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
try {
|
||||||
headers: {
|
return await apiClient.get<ShiftPlan>(`/shift-plans/${id}`);
|
||||||
'Content-Type': 'application/json',
|
} catch (error: any) {
|
||||||
...authService.getAuthHeaders()
|
if (error.statusCode === 401) {
|
||||||
}
|
localStorage.removeItem('token');
|
||||||
});
|
localStorage.removeItem('employee');
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 401) {
|
|
||||||
authService.logout();
|
|
||||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
}
|
}
|
||||||
throw new Error('Schichtplan nicht gefunden');
|
throw new Error('Schichtplan nicht gefunden');
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
|
async createShiftPlan(plan: CreateShiftPlanRequest): Promise<ShiftPlan> {
|
||||||
const response = await fetch(API_BASE_URL, {
|
try {
|
||||||
method: 'POST',
|
return await apiClient.post<ShiftPlan>('/shift-plans', plan);
|
||||||
headers: {
|
} catch (error: any) {
|
||||||
'Content-Type': 'application/json',
|
if (error.statusCode === 401) {
|
||||||
...authService.getAuthHeaders()
|
localStorage.removeItem('token');
|
||||||
},
|
localStorage.removeItem('employee');
|
||||||
body: JSON.stringify(plan)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 401) {
|
|
||||||
authService.logout();
|
|
||||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
}
|
}
|
||||||
throw new Error('Fehler beim Erstellen des Schichtplans');
|
throw new Error('Fehler beim Erstellen des Schichtplans');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateShiftPlan(id: string, plan: Partial<ShiftPlan>): Promise<ShiftPlan> {
|
async updateShiftPlan(id: string, plan: Partial<ShiftPlan>): Promise<ShiftPlan> {
|
||||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
try {
|
||||||
method: 'PUT',
|
return await apiClient.put<ShiftPlan>(`/shift-plans/${id}`, plan);
|
||||||
headers: {
|
} catch (error: any) {
|
||||||
'Content-Type': 'application/json',
|
if (error.statusCode === 401) {
|
||||||
...authService.getAuthHeaders()
|
localStorage.removeItem('token');
|
||||||
},
|
localStorage.removeItem('employee');
|
||||||
body: JSON.stringify(plan)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 401) {
|
|
||||||
authService.logout();
|
|
||||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
}
|
}
|
||||||
throw new Error('Fehler beim Aktualisieren des Schichtplans');
|
throw new Error('Fehler beim Aktualisieren des Schichtplans');
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteShiftPlan(id: string): Promise<void> {
|
async deleteShiftPlan(id: string): Promise<void> {
|
||||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
try {
|
||||||
method: 'DELETE',
|
await apiClient.delete(`/shift-plans/${id}`);
|
||||||
headers: {
|
} catch (error: any) {
|
||||||
'Content-Type': 'application/json',
|
if (error.statusCode === 401) {
|
||||||
...authService.getAuthHeaders()
|
localStorage.removeItem('token');
|
||||||
}
|
localStorage.removeItem('employee');
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
if (response.status === 401) {
|
|
||||||
authService.logout();
|
|
||||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||||
}
|
}
|
||||||
throw new Error('Fehler beim Löschen des Schichtplans');
|
throw new Error('Fehler beim Löschen des Schichtplans');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get specific template or plan
|
async getTemplate(id: string): Promise<ShiftPlan> {
|
||||||
getTemplate: async (id: string): Promise<ShiftPlan> => {
|
return await apiClient.get<ShiftPlan>(`/shift-plans/${id}`);
|
||||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
});
|
|
||||||
return handleResponse(response);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async regenerateScheduledShifts(planId: string): Promise<void> {
|
||||||
async regenerateScheduledShifts(planId: string):Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Attempting to regenerate scheduled shifts...');
|
console.log('🔄 Attempting to regenerate scheduled shifts...');
|
||||||
|
await apiClient.post(`/shift-plans/${planId}/regenerate-shifts`);
|
||||||
// You'll need to add this API endpoint to your backend
|
console.log('✅ Scheduled shifts regenerated');
|
||||||
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');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error regenerating shifts:', error);
|
console.error('❌ Error regenerating shifts:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create new plan
|
async createPlan(data: CreateShiftPlanRequest): Promise<ShiftPlan> {
|
||||||
createPlan: async (data: CreateShiftPlanRequest): Promise<ShiftPlan> => {
|
return await apiClient.post<ShiftPlan>('/shift-plans', data);
|
||||||
const response = await fetch(`${API_BASE_URL}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
return handleResponse(response);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createFromPreset: async (data: {
|
async createFromPreset(data: {
|
||||||
presetName: string;
|
presetName: string;
|
||||||
name: string;
|
name: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
}): Promise<ShiftPlan> => {
|
}): Promise<ShiftPlan> {
|
||||||
const response = await fetch(`${API_BASE_URL}/from-preset`, {
|
try {
|
||||||
method: 'POST',
|
return await apiClient.post<ShiftPlan>('/shift-plans/from-preset', data);
|
||||||
headers: getAuthHeaders(),
|
} catch (error: any) {
|
||||||
body: JSON.stringify(data),
|
throw new Error(error.message || `HTTP error! status: ${error.statusCode}`);
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getTemplatePresets: async (): Promise<{name: string, label: string, description: string}[]> => {
|
async getTemplatePresets(): Promise<{name: string, label: string, description: string}[]> {
|
||||||
// name = label
|
return Object.entries(TEMPLATE_PRESETS).map(([key, preset]) => ({
|
||||||
return Object.entries(TEMPLATE_PRESETS).map(([key, preset]) => ({
|
|
||||||
name: key,
|
name: key,
|
||||||
label: preset.name,
|
label: preset.name,
|
||||||
description: preset.description
|
description: preset.description
|
||||||
@@ -203,22 +119,8 @@ export const shiftPlanService = {
|
|||||||
async clearAssignments(planId: string): Promise<void> {
|
async clearAssignments(planId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Clearing assignments for plan:', planId);
|
console.log('🔄 Clearing assignments for plan:', planId);
|
||||||
|
await apiClient.post(`/shift-plans/${planId}/clear-assignments`);
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Assignments cleared successfully');
|
console.log('✅ Assignments cleared successfully');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error clearing assignments:', error);
|
console.error('❌ Error clearing assignments:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -59,7 +59,13 @@ export default defineConfig(({ mode }) => {
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': resolve(__dirname, './src'),
|
'@': 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')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user