diff --git a/CLOUDINARY_SETUP.md b/CLOUDINARY_SETUP.md new file mode 100644 index 0000000..b7bdceb --- /dev/null +++ b/CLOUDINARY_SETUP.md @@ -0,0 +1,149 @@ +# Cloudinary Setup Guide + +## Overview +This application uses Cloudinary for storing and managing vehicle images. + +## Setup Instructions + +### 1. Create a Cloudinary Account +1. Go to [Cloudinary](https://cloudinary.com/) +2. Sign up for a free account +3. Once logged in, go to your [Dashboard](https://cloudinary.com/console) + +### 2. Get Your Credentials +From your Cloudinary Dashboard, copy: +- **Cloud Name** +- **API Key** +- **API Secret** + +### 3. Configure Environment Variables + +#### Option 1: Using Environment Variables (Recommended for Production) +Set the following environment variables: + +**Windows (PowerShell):** +```powershell +$env:CLOUDINARY_CLOUD_NAME="your-cloud-name" +$env:CLOUDINARY_API_KEY="your-api-key" +$env:CLOUDINARY_API_SECRET="your-api-secret" +``` + +**Windows (Command Prompt):** +```cmd +set CLOUDINARY_CLOUD_NAME=your-cloud-name +set CLOUDINARY_API_KEY=your-api-key +set CLOUDINARY_API_SECRET=your-api-secret +``` + +**Linux/Mac:** +```bash +export CLOUDINARY_CLOUD_NAME="your-cloud-name" +export CLOUDINARY_API_KEY="your-api-key" +export CLOUDINARY_API_SECRET="your-api-secret" +``` + +#### Option 2: Update application.properties (For Development) +Replace the placeholder values in `src/main/resources/application.properties`: +```properties +cloudinary.cloud-name=your-actual-cloud-name +cloudinary.api-key=your-actual-api-key +cloudinary.api-secret=your-actual-api-secret +``` + +⚠️ **Warning:** Never commit your actual credentials to version control! + +### 4. Test the Configuration +Run your application and try uploading a vehicle image using the API endpoints. + +## API Endpoints + +### Create Vehicle with Image +```http +POST /api/vehicles +Content-Type: multipart/form-data + +Parameters: +- vehicle: JSON string of VehicleDTO +- image: Image file (optional) +``` + +### Update Vehicle with Image +```http +PUT /api/vehicles/{id} +Content-Type: multipart/form-data + +Parameters: +- vehicle: JSON string of VehicleDTO +- image: Image file (optional) +``` + +## Example Using Postman + +1. **Create Vehicle with Image:** + - Method: POST + - URL: `http://localhost:8080/api/vehicles` + - Body type: form-data + - Add fields: + - `vehicle` (text): + ```json + { + "customerId": 1, + "model": "Toyota Camry", + "color": "Blue", + "vin": "1HGBH41JXMN109186", + "licensePlate": "ABC-1234", + "year": 2023, + "registrationDate": "2023-01-15" + } + ``` + - `image` (file): Select your vehicle image + +2. **Update Vehicle with Image:** + - Method: PUT + - URL: `http://localhost:8080/api/vehicles/1` + - Body type: form-data + - Add fields: + - `vehicle` (text): VehicleDTO JSON + - `image` (file): New vehicle image + +## Image Specifications + +- **Supported formats:** JPEG, PNG, GIF, WebP +- **Maximum file size:** 10MB (can be configured) +- **Automatic optimization:** Images are automatically resized to max 800x600 pixels +- **Storage location:** Images are stored in folder `ead-automobile/vehicles/` + +## Features + +✅ **Automatic image upload** to Cloudinary +✅ **Image deletion** when vehicle is deleted +✅ **Image update** - old image is automatically deleted +✅ **Image optimization** - automatic resizing and quality optimization +✅ **Secure storage** - images are served over HTTPS + +## Troubleshooting + +### Issue: "Failed to upload image" +- Check your Cloudinary credentials are correct +- Verify your internet connection +- Check Cloudinary dashboard for quota limits + +### Issue: "Invalid API key" +- Ensure environment variables are set correctly +- Restart your application after setting environment variables + +### Issue: "Upload quota exceeded" +- Free tier has monthly limits (25GB storage, 25 credits/month) +- Consider upgrading your Cloudinary plan + +## Security Notes + +🔒 **Never commit credentials to Git** +- Add `.env` to `.gitignore` +- Use environment variables in production +- Rotate API credentials periodically + +## Additional Resources + +- [Cloudinary Documentation](https://cloudinary.com/documentation) +- [Cloudinary Java SDK](https://cloudinary.com/documentation/java_integration) diff --git a/CUSTOMER_PROFILE_API.md b/CUSTOMER_PROFILE_API.md new file mode 100644 index 0000000..af463b6 --- /dev/null +++ b/CUSTOMER_PROFILE_API.md @@ -0,0 +1,415 @@ +# Customer Profile Management API + +## Overview +API endpoints for customers to view and edit their profile information. Customers can update their first name, last name, and phone number. Email and password cannot be changed through these endpoints. + +## Authentication +All endpoints require JWT authentication. Include the JWT token in the Authorization header: +``` +Authorization: Bearer +``` + +--- + +## Endpoints + +### 1. Get My Profile +Get the profile of the currently authenticated customer. + +**Endpoint:** `GET /api/customer/profile/me` + +**Authentication:** Required (Customer role) + +**Response (200 OK):** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phoneNumber": "1234567890" +} +``` + +**Note:** Email is read-only and cannot be updated. + +--- + +### 2. Update My Profile +Update the profile of the currently authenticated customer. + +**Endpoint:** `PUT /api/customer/profile/me` + +**Authentication:** Required (Customer role) + +**Request Body:** +```json +{ + "firstName": "John", + "lastName": "Doe", + "phoneNumber": "1234567890" +} +``` + +**Validation Rules:** +- `firstName`: Required, cannot be blank +- `lastName`: Required, cannot be blank +- `phoneNumber`: Optional, must be 10-15 digits if provided + +**Response (200 OK):** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phoneNumber": "1234567890" +} +``` + +**Error Responses:** + +- **400 Bad Request** - Validation errors +```json +{ + "timestamp": "2024-01-15T10:30:00", + "status": 400, + "error": "Bad Request", + "message": "Validation failed", + "errors": { + "firstName": "must not be blank", + "phoneNumber": "must be between 10 and 15 digits" + } +} +``` + +- **404 Not Found** - Customer profile not found +```json +{ + "timestamp": "2024-01-15T10:30:00", + "status": 404, + "error": "Not Found", + "message": "Customer not found for user ID: 5" +} +``` + +--- + +### 3. Get Profile by User ID (Admin) +Get customer profile by user ID. Typically used by administrators. + +**Endpoint:** `GET /api/customer/profile/user/{userId}` + +**Authentication:** Required (Admin role recommended) + +**Path Parameters:** +- `userId` (Long): The ID of the user + +**Response (200 OK):** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phoneNumber": "1234567890" +} +``` + +--- + +### 4. Get Profile by Customer ID (Admin) +Get customer profile by customer ID. Typically used by administrators. + +**Endpoint:** `GET /api/customer/profile/{customerId}` + +**Authentication:** Required (Admin role recommended) + +**Path Parameters:** +- `customerId` (Long): The ID of the customer + +**Response (200 OK):** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phoneNumber": "1234567890" +} +``` + +--- + +### 5. Update Profile by User ID (Admin) +Update customer profile by user ID. Typically used by administrators. + +**Endpoint:** `PUT /api/customer/profile/user/{userId}` + +**Authentication:** Required (Admin role recommended) + +**Path Parameters:** +- `userId` (Long): The ID of the user + +**Request Body:** +```json +{ + "firstName": "John", + "lastName": "Doe", + "phoneNumber": "1234567890" +} +``` + +**Response (200 OK):** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phoneNumber": "1234567890" +} +``` + +--- + +## Usage Examples + +### Example 1: Customer Views Their Profile + +**Request:** +```http +GET /api/customer/profile/me HTTP/1.1 +Host: localhost:8080 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +``` + +**Response:** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phoneNumber": "1234567890" +} +``` + +--- + +### Example 2: Customer Updates Their Profile + +**Request:** +```http +PUT /api/customer/profile/me HTTP/1.1 +Host: localhost:8080 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +Content-Type: application/json + +{ + "firstName": "John", + "lastName": "Smith", + "phoneNumber": "9876543210" +} +``` + +**Response:** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Smith", + "email": "john.doe@example.com", + "phoneNumber": "9876543210" +} +``` + +--- + +### Example 3: Customer Updates Only Phone Number + +**Request:** +```http +PUT /api/customer/profile/me HTTP/1.1 +Host: localhost:8080 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +Content-Type: application/json + +{ + "firstName": "John", + "lastName": "Doe", + "phoneNumber": "5555555555" +} +``` + +**Response:** +```json +{ + "id": 1, + "userId": 5, + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "phoneNumber": "5555555555" +} +``` + +--- + +## Integration with Frontend + +### JavaScript/TypeScript Example + +```javascript +// Get customer profile +async function getMyProfile() { + const response = await fetch('http://localhost:8080/api/customer/profile/me', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('jwt')}`, + 'Content-Type': 'application/json' + } + }); + return await response.json(); +} + +// Update customer profile +async function updateMyProfile(profileData) { + const response = await fetch('http://localhost:8080/api/customer/profile/me', { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${localStorage.getItem('jwt')}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + firstName: profileData.firstName, + lastName: profileData.lastName, + phoneNumber: profileData.phoneNumber + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Failed to update profile'); + } + + return await response.json(); +} + +// Usage +try { + const profile = await getMyProfile(); + console.log('Current profile:', profile); + + const updated = await updateMyProfile({ + firstName: 'John', + lastName: 'Smith', + phoneNumber: '9876543210' + }); + console.log('Updated profile:', updated); +} catch (error) { + console.error('Error:', error.message); +} +``` + +--- + +## Security Notes + +1. **Authentication Required**: All endpoints require valid JWT authentication +2. **Email Protection**: Email cannot be changed through profile endpoints +3. **Password Protection**: Password cannot be changed through profile endpoints (use password reset flow) +4. **Customer Access**: Customers can only view/edit their own profile through `/me` endpoints +5. **Admin Access**: Admin endpoints (`/user/{userId}`) should be protected with admin role authorization + +--- + +## Common Error Scenarios + +### 1. Missing Authentication +```json +{ + "status": 401, + "error": "Unauthorized", + "message": "Full authentication is required to access this resource" +} +``` + +### 2. Invalid Phone Number Format +```json +{ + "status": 400, + "error": "Bad Request", + "errors": { + "phoneNumber": "must be between 10 and 15 digits" + } +} +``` + +### 3. Missing Required Fields +```json +{ + "status": 400, + "error": "Bad Request", + "errors": { + "firstName": "must not be blank", + "lastName": "must not be blank" + } +} +``` + +### 4. Customer Not Found +```json +{ + "status": 404, + "error": "Not Found", + "message": "Customer not found for email: john.doe@example.com" +} +``` + +--- + +## Testing with Postman/cURL + +### cURL Example - Get Profile +```bash +curl -X GET http://localhost:8080/api/customer/profile/me \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" +``` + +### cURL Example - Update Profile +```bash +curl -X PUT http://localhost:8080/api/customer/profile/me \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "firstName": "John", + "lastName": "Smith", + "phoneNumber": "9876543210" + }' +``` + +--- + +## Related APIs + +- **Password Management**: Use `/api/auth/forgot-password` and `/api/auth/reset-password` endpoints +- **Vehicle Management**: Use `/api/vehicles` endpoints for managing customer vehicles +- **Appointments**: Use `/api/appointments` endpoints for managing customer appointments + +--- + +## Change History + +- **v1.0.0** - Initial release with profile view and edit functionality + - GET /api/customer/profile/me + - PUT /api/customer/profile/me + - Admin endpoints for user/customer ID access diff --git a/VEHICLE_IMAGE_API.md b/VEHICLE_IMAGE_API.md new file mode 100644 index 0000000..b5aaf3b --- /dev/null +++ b/VEHICLE_IMAGE_API.md @@ -0,0 +1,142 @@ +# Vehicle Image Upload API - Quick Reference + +## Setup Required + +### 1. Set Environment Variables +Before running the application, set these environment variables: + +**PowerShell:** +```powershell +$env:CLOUDINARY_CLOUD_NAME="your-cloud-name" +$env:CLOUDINARY_API_KEY="your-api-key" +$env:CLOUDINARY_API_SECRET="your-api-secret" +``` + +**Or update `application.properties` directly (not recommended for production)** + +## API Endpoints + +### 1. Create Vehicle WITHOUT Image (JSON) +```http +POST http://localhost:8080/api/vehicles +Content-Type: application/json + +{ + "customerId": 1, + "model": "Toyota Camry", + "color": "Blue", + "vin": "1HGBH41JXMN109186", + "licensePlate": "ABC-1234", + "year": 2023, + "registrationDate": "2023-01-15" +} +``` + +### 2. Create Vehicle WITH Image (Multipart Form Data) +```http +POST http://localhost:8080/api/vehicles +Content-Type: multipart/form-data + +Form Fields: +- vehicle: { + "customerId": 1, + "model": "Toyota Camry", + "color": "Blue", + "vin": "1HGBH41JXMN109186", + "licensePlate": "ABC-1234", + "year": 2023, + "registrationDate": "2023-01-15" + } +- image: [Select Image File] +``` + +### 3. Update Vehicle WITHOUT Image (JSON) +```http +PUT http://localhost:8080/api/vehicles/1 +Content-Type: application/json + +{ + "customerId": 1, + "model": "Honda Accord", + "color": "Red", + "vin": "1HGBH41JXMN109186", + "licensePlate": "XYZ-5678", + "year": 2024, + "registrationDate": "2024-01-15" +} +``` + +### 4. Update Vehicle WITH Image (Multipart Form Data) +```http +PUT http://localhost:8080/api/vehicles/1 +Content-Type: multipart/form-data + +Form Fields: +- vehicle: { ... vehicle data ... } +- image: [Select New Image File] +``` + +### 5. Get Vehicle by ID (includes imageUrl) +```http +GET http://localhost:8080/api/vehicles/1 +``` + +Response: +```json +{ + "id": 1, + "customerId": 1, + "model": "Toyota Camry", + "color": "Blue", + "vin": "1HGBH41JXMN109186", + "licensePlate": "ABC-1234", + "year": 2023, + "registrationDate": "2023-01-15", + "imageUrl": "https://res.cloudinary.com/your-cloud/image/upload/v1234567890/ead-automobile/vehicles/abc123.jpg" +} +``` + +### 6. Get All Vehicles for a Customer +```http +GET http://localhost:8080/api/vehicles/customer/1 +``` + +### 7. Delete Vehicle (also deletes image from Cloudinary) +```http +DELETE http://localhost:8080/api/vehicles/1 +``` + +## Testing with Postman + +### Create/Update with Image: +1. Select request method (POST or PUT) +2. Enter URL +3. Go to **Body** tab +4. Select **form-data** +5. Add two fields: + - Key: `vehicle` | Type: Text | Value: `{"customerId": 1, "model": "Toyota", ...}` + - Key: `image` | Type: File | Value: Select your image file + +## Features Implemented ✅ + +- ✅ Upload vehicle images to Cloudinary +- ✅ Automatic image optimization (800x600 max, auto quality) +- ✅ Update vehicle image (old image is automatically deleted) +- ✅ Delete vehicle image when vehicle is deleted +- ✅ Secure HTTPS image URLs +- ✅ Images organized in folder: `ead-automobile/vehicles/` +- ✅ Environment variable configuration for security + +## Database Changes + +New columns added to `vehicles` table: +- `image_url` (VARCHAR 500) - Cloudinary image URL +- `image_public_id` (VARCHAR 255) - Cloudinary public ID for deletion + +## Notes + +- Image upload is **optional** for create/update operations +- Old images are automatically deleted when updating or deleting vehicles +- Images are automatically resized to max 800x600 pixels +- Supported formats: JPEG, PNG, GIF, WebP +- Free Cloudinary tier: 25GB storage, 25 credits/month diff --git a/pom.xml b/pom.xml index 0ea99e5..8c5eb38 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,21 @@ org.springframework.boot spring-boot-starter-web - + + + org.springframework.boot + spring-boot-starter-mail + + + + org.springframework.boot + spring-boot-starter-websocket + + + com.h2database + h2 + runtime + org.springframework.boot spring-boot-devtools @@ -58,6 +72,18 @@ postgresql runtime + + org.mapstruct + mapstruct + 1.5.5.Final + + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + provided + org.projectlombok lombok @@ -73,6 +99,34 @@ spring-security-test test + + org.springframework + spring-messaging + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + com.cloudinary + cloudinary-http44 + 1.36.0 + @@ -82,11 +136,25 @@ maven-compiler-plugin + + org.mapstruct + mapstruct-processor + 1.5.5.Final + org.projectlombok lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + -Amapstruct.defaultComponentModel=spring + @@ -104,4 +172,4 @@ - + \ No newline at end of file diff --git a/src/main/java/com/example/ead_backend/EadBackendApplication.java b/src/main/java/com/example/ead_backend/EadBackendApplication.java index 2a3e8c0..3ae246a 100644 --- a/src/main/java/com/example/ead_backend/EadBackendApplication.java +++ b/src/main/java/com/example/ead_backend/EadBackendApplication.java @@ -10,4 +10,4 @@ public static void main(String[] args) { SpringApplication.run(EadBackendApplication.class, args); } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/ead_backend/config/AdminInitializer.java b/src/main/java/com/example/ead_backend/config/AdminInitializer.java new file mode 100644 index 0000000..a5ab1c9 --- /dev/null +++ b/src/main/java/com/example/ead_backend/config/AdminInitializer.java @@ -0,0 +1,58 @@ +package com.example.ead_backend.config; + +import com.example.ead_backend.model.entity.Employee; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.model.enums.Role; +import com.example.ead_backend.repository.EmployeeRepository; +import com.example.ead_backend.repository.UserRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +@Component +public class AdminInitializer implements CommandLineRunner { + + @Autowired + private UserRepo userRepo; + + @Autowired + private EmployeeRepository employeeRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public void run(String... args) throws Exception { + // Check if admin already exists + if (!userRepo.existsByEmail("admin@company.com")) { + // Create user first + User admin = new User(); + admin.setFirstName("System"); + admin.setLastName("Admin"); + admin.setEmail("admin@company.com"); + admin.setPassword(passwordEncoder.encode("admin123")); + + User savedUser = userRepo.save(admin); + + // Create employee record with ADMIN role + Employee adminEmployee = new Employee(); + adminEmployee.setUser(savedUser); + adminEmployee.setRole(Role.ADMIN); + adminEmployee.setJoinedDate(LocalDate.now()); + + employeeRepository.save(adminEmployee); + + System.out.println("====================================="); + System.out.println("Default admin created successfully!"); + System.out.println("====================================="); + System.out.println("Email: admin@company.com"); + System.out.println("Password: admin123"); + System.out.println("====================================="); + } else { + System.out.println("Admin user already exists. Skipping creation."); + } + } +} diff --git a/src/main/java/com/example/ead_backend/config/CloudinaryConfig.java b/src/main/java/com/example/ead_backend/config/CloudinaryConfig.java new file mode 100644 index 0000000..cbbcd16 --- /dev/null +++ b/src/main/java/com/example/ead_backend/config/CloudinaryConfig.java @@ -0,0 +1,30 @@ +package com.example.ead_backend.config; + +import com.cloudinary.Cloudinary; +import com.cloudinary.utils.ObjectUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CloudinaryConfig { + + @Value("${cloudinary.cloud-name}") + private String cloudName; + + @Value("${cloudinary.api-key}") + private String apiKey; + + @Value("${cloudinary.api-secret}") + private String apiSecret; + + @Bean + public Cloudinary cloudinary() { + return new Cloudinary(ObjectUtils.asMap( + "cloud_name", cloudName, + "api_key", apiKey, + "api_secret", apiSecret, + "secure", true + )); + } +} diff --git a/src/main/java/com/example/ead_backend/config/PasswordConfig.java b/src/main/java/com/example/ead_backend/config/PasswordConfig.java new file mode 100644 index 0000000..e1905b3 --- /dev/null +++ b/src/main/java/com/example/ead_backend/config/PasswordConfig.java @@ -0,0 +1,15 @@ +package com.example.ead_backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/example/ead_backend/config/SecurityConfig.java b/src/main/java/com/example/ead_backend/config/SecurityConfig.java new file mode 100644 index 0000000..405a4e7 --- /dev/null +++ b/src/main/java/com/example/ead_backend/config/SecurityConfig.java @@ -0,0 +1,87 @@ +package com.example.ead_backend.config; + +import com.example.ead_backend.filter.JwtAuthenticationFilter; +import com.example.ead_backend.service.impl.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import java.util.List; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Autowired + private UserService userService; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .cors(customizer -> customizer.configurationSource(corsConfigurationSource())) + .csrf(customizer -> customizer.disable()) + .authorizeHttpRequests(request -> request + .requestMatchers("/api/auth/**").permitAll() + // Allow public access only to appointment availability endpoint + .requestMatchers("/api/appointments/availability").permitAll() + // All other appointment endpoints require authentication + .requestMatchers("/api/appointments/**").authenticated() + .requestMatchers("/api/projects/**").authenticated() + .requestMatchers("/api/password/forgot", "/api/password/verify-otp", "/api/password/reset").permitAll() + .requestMatchers("/api/password/change").authenticated() + .requestMatchers("/api/admin/**").hasRole("ADMIN") +// .requestMatchers("/api/employee/**").hasAnyRole("EMPLOYEE", "ADMIN") +// .requestMatchers("/api/customer/**").hasAnyRole("CUSTOMER", "EMPLOYEE", "ADMIN") + .anyRequest().authenticated()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userService); + authProvider.setPasswordEncoder(passwordEncoder); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:3000", "http://localhost:3001")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With", "X-User-Id")); + config.setExposedHeaders(List.of("Authorization", "Content-Type")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/src/main/java/com/example/ead_backend/contoller/admin/AdminAppointmentController.java b/src/main/java/com/example/ead_backend/contoller/admin/AdminAppointmentController.java deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/ead_backend/contoller/admin/AdminDashboardController.java b/src/main/java/com/example/ead_backend/contoller/admin/AdminDashboardController.java deleted file mode 100644 index c3b670f..0000000 --- a/src/main/java/com/example/ead_backend/contoller/admin/AdminDashboardController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.admin; - -public class AdminDashboardController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/admin/AdminServiceController.java b/src/main/java/com/example/ead_backend/contoller/admin/AdminServiceController.java deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/example/ead_backend/contoller/admin/AdminUserController.java b/src/main/java/com/example/ead_backend/contoller/admin/AdminUserController.java deleted file mode 100644 index dd3120e..0000000 --- a/src/main/java/com/example/ead_backend/contoller/admin/AdminUserController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.admin; - -public class AdminUserController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/auth/AuthController.java b/src/main/java/com/example/ead_backend/contoller/auth/AuthController.java deleted file mode 100644 index a942dc7..0000000 --- a/src/main/java/com/example/ead_backend/contoller/auth/AuthController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.auth; - -public class AuthController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/auth/ProfileController.java b/src/main/java/com/example/ead_backend/contoller/auth/ProfileController.java deleted file mode 100644 index be9b276..0000000 --- a/src/main/java/com/example/ead_backend/contoller/auth/ProfileController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.auth; - -public class ProfileController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/customer/CustomerAppointmentController.java b/src/main/java/com/example/ead_backend/contoller/customer/CustomerAppointmentController.java deleted file mode 100644 index d24044a..0000000 --- a/src/main/java/com/example/ead_backend/contoller/customer/CustomerAppointmentController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.customer; - -public class CustomerAppointmentController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/customer/CustomerDashboardController.java b/src/main/java/com/example/ead_backend/contoller/customer/CustomerDashboardController.java deleted file mode 100644 index 1bbd800..0000000 --- a/src/main/java/com/example/ead_backend/contoller/customer/CustomerDashboardController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.customer; - -public class CustomerDashboardController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/customer/ProgressViewController.java b/src/main/java/com/example/ead_backend/contoller/customer/ProgressViewController.java deleted file mode 100644 index 672deed..0000000 --- a/src/main/java/com/example/ead_backend/contoller/customer/ProgressViewController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.customer; - -public class ProgressViewController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/customer/ServiceCatalogController.java b/src/main/java/com/example/ead_backend/contoller/customer/ServiceCatalogController.java deleted file mode 100644 index 638e70f..0000000 --- a/src/main/java/com/example/ead_backend/contoller/customer/ServiceCatalogController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.customer; - -public class ServiceCatalogController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/customer/VehicleController.java b/src/main/java/com/example/ead_backend/contoller/customer/VehicleController.java deleted file mode 100644 index 82d9d0a..0000000 --- a/src/main/java/com/example/ead_backend/contoller/customer/VehicleController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.customer; - -public class VehicleController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/employee/EmployeeAppointmentController.java b/src/main/java/com/example/ead_backend/contoller/employee/EmployeeAppointmentController.java deleted file mode 100644 index 0d9909e..0000000 --- a/src/main/java/com/example/ead_backend/contoller/employee/EmployeeAppointmentController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.employee; - -public class EmployeeAppointmentController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/employee/EmployeeDashboardController.java b/src/main/java/com/example/ead_backend/contoller/employee/EmployeeDashboardController.java deleted file mode 100644 index d74a251..0000000 --- a/src/main/java/com/example/ead_backend/contoller/employee/EmployeeDashboardController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.employee; - -public class EmployeeDashboardController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/employee/ProgressUpdateController.java b/src/main/java/com/example/ead_backend/contoller/employee/ProgressUpdateController.java deleted file mode 100644 index 930a465..0000000 --- a/src/main/java/com/example/ead_backend/contoller/employee/ProgressUpdateController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.employee; - -public class ProgressUpdateController { - -} diff --git a/src/main/java/com/example/ead_backend/contoller/employee/TimeLogController.java b/src/main/java/com/example/ead_backend/contoller/employee/TimeLogController.java deleted file mode 100644 index 7865181..0000000 --- a/src/main/java/com/example/ead_backend/contoller/employee/TimeLogController.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.contoller.employee; - -public class TimeLogController { - -} diff --git a/src/main/java/com/example/ead_backend/controller/AdminController.java b/src/main/java/com/example/ead_backend/controller/AdminController.java new file mode 100644 index 0000000..0bbde96 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/AdminController.java @@ -0,0 +1,49 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.CreateEmployeeRequest; +import com.example.ead_backend.dto.EmployeeCreateDTO; +import com.example.ead_backend.service.AdminService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@CrossOrigin +@RequestMapping("/api/admin") +@PreAuthorize("hasRole('ADMIN')") +public class AdminController { + + @Autowired + private AdminService adminService; + + @PostMapping("/employees") + public ResponseEntity createEmployee(@Valid @RequestBody CreateEmployeeRequest request, + BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + Map errors = new HashMap<>(); + bindingResult.getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + return ResponseEntity.badRequest().body(errors); + } + + try { + EmployeeCreateDTO employee = adminService.createEmployee(request); + return ResponseEntity.status(HttpStatus.CREATED).body(employee); + } catch (IllegalArgumentException e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", "Failed to create employee: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + } + } +} diff --git a/src/main/java/com/example/ead_backend/controller/AppointmentController.java b/src/main/java/com/example/ead_backend/controller/AppointmentController.java new file mode 100644 index 0000000..fc4e200 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/AppointmentController.java @@ -0,0 +1,105 @@ +package com.example.ead_backend.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import com.example.ead_backend.dto.AppointmentDTO; +import com.example.ead_backend.service.AppointmentService; + +import java.util.List; +import java.util.Map; +import java.security.Principal; +import java.time.LocalDate; + +@RestController +@Slf4j +@RequestMapping("/api/appointments") +@RequiredArgsConstructor +public class AppointmentController { + private final AppointmentService appointmentService; + + // Helper to get current user's ID (you can customize this based on your auth setup) + private String getCurrentUserId(Principal principal) { + return principal != null ? principal.getName() : null; + } + + @PostMapping + public AppointmentDTO create(@RequestBody AppointmentDTO dto, Principal principal) { + // Set customerId from authenticated user when creating + String userId = getCurrentUserId(principal); + if (userId != null) { + dto.setCustomerId(userId); + } + return appointmentService.createAppointment(dto); + } + + @GetMapping("/{id}") + public AppointmentDTO getById(@PathVariable String id, Principal principal) { + // Check if the user owns the appointment + AppointmentDTO appointment = appointmentService.getAppointmentById(id); + String userId = getCurrentUserId(principal); + if (userId != null && !userId.equals(appointment.getCustomerId())) { + throw new RuntimeException("Not authorized to view this appointment"); + } + return appointment; + } + + @GetMapping + public List getAll(@RequestParam(required = false) String customerId, Principal principal) { + // If no customerId provided but user is authenticated, use their ID + if (customerId == null && principal != null) { + customerId = getCurrentUserId(principal); + } + + // If customerId is provided (either via param or auth), filter by it + if (customerId != null) { + return appointmentService.getAppointmentsByCustomerId(customerId); + } + + // Otherwise return all appointments (e.g., for admin users) + return appointmentService.getAllAppointments(); + } + + // Availability: return object with 'booked' (HH:mm list) for a given date (YYYY-MM-DD) + @GetMapping("/availability") + public Map getAvailability(@RequestParam("date") String dateStr) { + LocalDate date = LocalDate.parse(dateStr); + List booked = appointmentService.getBookedStartTimes(date); + return Map.of( + "date", dateStr, + "booked", booked + ); + } + + @PutMapping("/{id}") + public AppointmentDTO update(@PathVariable String id, @RequestBody AppointmentDTO dto, Principal principal) { + // Verify user owns the appointment they're trying to update + AppointmentDTO existing = appointmentService.getAppointmentById(id); + String userId = getCurrentUserId(principal); + if (userId != null && !userId.equals(existing.getCustomerId())) { + throw new RuntimeException("Not authorized to update this appointment"); + } + + // Preserve the existing customerId - don't allow it to be changed + dto.setCustomerId(existing.getCustomerId()); + return appointmentService.updateAppointment(id, dto); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable String id, Principal principal) { + // Log incoming delete attempt for debugging + log.info("DELETE /api/appointments/{} called, principal={}", id, principal == null ? "" : principal.getName()); + + // Verify user owns the appointment they're trying to delete + AppointmentDTO existing = appointmentService.getAppointmentById(id); + String userId = getCurrentUserId(principal); + if (userId != null && !userId.equals(existing.getCustomerId())) { + log.warn("User {} not authorized to delete appointment {} owned by {}", userId, id, existing.getCustomerId()); + throw new RuntimeException("Not authorized to delete this appointment"); + } + + appointmentService.deleteAppointment(id); + log.info("Appointment {} deleted successfully", id); + } +} diff --git a/src/main/java/com/example/ead_backend/controller/AuthController.java b/src/main/java/com/example/ead_backend/controller/AuthController.java new file mode 100644 index 0000000..ae733f8 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/AuthController.java @@ -0,0 +1,123 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.AuthResponse; +import com.example.ead_backend.dto.LoginRequest; +import com.example.ead_backend.dto.SignupRequest; +import com.example.ead_backend.model.entity.Customer; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.service.CustomerService; +import com.example.ead_backend.service.impl.UserService; +import com.example.ead_backend.util.JwtUtil; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@CrossOrigin +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired + @Lazy + private AuthenticationManager authenticationManager; + + @Autowired + private UserService userService; + + @Autowired + private CustomerService customerService; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private PasswordEncoder passwordEncoder; + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + Map errors = new HashMap<>(); + bindingResult.getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + return ResponseEntity.badRequest().body(errors); + } + + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()) + ); + + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + String token = jwtUtil.generateToken(userDetails); + User user = (User) userDetails; + + // Determine role from user's customer or employee relationship + String role = "CUSTOMER"; + if (user.getEmployee() != null) { + role = user.getEmployee().getRole().name(); + } + + return ResponseEntity.ok(new AuthResponse(token, user.getEmail(), role)); + } catch (BadCredentialsException e) { + return ResponseEntity.badRequest().body("Invalid email or password"); + } catch (Exception e) { + return ResponseEntity.badRequest().body("Authentication failed: " + e.getMessage()); + } + } + + @PostMapping("/signup") + public ResponseEntity signup(@Valid @RequestBody SignupRequest signupRequest, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + Map errors = new HashMap<>(); + bindingResult.getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + return ResponseEntity.badRequest().body(errors); + } + + try { + // Hash the password before creating user + String encodedPassword = passwordEncoder.encode(signupRequest.getPassword()); + + // Create User entity + User user = userService.createUser( + signupRequest.getFirstName(), + signupRequest.getLastName(), + encodedPassword, + signupRequest.getEmail() + ); + + // Create Customer entity linked to the User + Customer customer = customerService.createCustomer(user, signupRequest.getPhoneNumber()); + + // Set the customer relationship in user + user.setCustomer(customer); + + // Generate JWT token for the newly created user + String token = jwtUtil.generateToken(user); + + return ResponseEntity.ok(new AuthResponse(token, user.getEmail(), "CUSTOMER")); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } catch (Exception e) { + return ResponseEntity.badRequest().body("Registration failed: " + e.getMessage()); + } + } + + @PostMapping("/logout") + public ResponseEntity logout() { + // JWT tokens are stateless, so logout is handled client-side by removing the token + return ResponseEntity.ok("Logout successful"); + } +} diff --git a/src/main/java/com/example/ead_backend/controller/CustomerController.java b/src/main/java/com/example/ead_backend/controller/CustomerController.java new file mode 100644 index 0000000..6e12439 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/CustomerController.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.controller; + +public class CustomerController { + +} diff --git a/src/main/java/com/example/ead_backend/controller/CustomerProfileController.java b/src/main/java/com/example/ead_backend/controller/CustomerProfileController.java new file mode 100644 index 0000000..be6ed58 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/CustomerProfileController.java @@ -0,0 +1,91 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.CustomerProfileDTO; +import com.example.ead_backend.dto.UpdateCustomerProfileRequest; +import com.example.ead_backend.service.CustomerProfileService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +/** + * Controller for customer profile management + * Handles profile viewing and editing for customers + */ +@RestController +@RequestMapping("/api/customer/profile") +@CrossOrigin(origins = "*") +@RequiredArgsConstructor +public class CustomerProfileController { + + private final CustomerProfileService customerProfileService; + + /** + * Get current authenticated customer's profile + * Uses Spring Security Authentication to get the logged-in user + */ + @GetMapping("/me") + public ResponseEntity getMyProfile(Authentication authentication) { + String email = authentication.getName(); // Gets email from authenticated user + CustomerProfileDTO profile = customerProfileService.getCustomerProfileByEmail(email); + return ResponseEntity.ok(profile); + } + + /** + * Get customer profile by user ID + * Useful for admin or other authorized users + */ + @GetMapping("/user/{userId}") + public ResponseEntity getProfileByUserId(@PathVariable Long userId) { + CustomerProfileDTO profile = customerProfileService.getCustomerProfileByUserId(userId); + return ResponseEntity.ok(profile); + } + + /** + * Get customer profile by customer ID + * Useful for admin or other authorized users + */ + @GetMapping("/{customerId}") + public ResponseEntity getProfileByCustomerId(@PathVariable Long customerId) { + CustomerProfileDTO profile = customerProfileService.getCustomerProfileByCustomerId(customerId); + return ResponseEntity.ok(profile); + } + + /** + * Update current authenticated customer's profile + * Email and password cannot be updated through this endpoint + */ + @PutMapping("/me") + public ResponseEntity updateMyProfile( + Authentication authentication, + @Valid @RequestBody UpdateCustomerProfileRequest request) { + + // Get user email from authentication + String email = authentication.getName(); + + // Get user profile to extract userId + CustomerProfileDTO currentProfile = customerProfileService.getCustomerProfileByEmail(email); + + // Update profile + CustomerProfileDTO updatedProfile = customerProfileService.updateCustomerProfile( + currentProfile.getUserId(), + request + ); + + return ResponseEntity.ok(updatedProfile); + } + + /** + * Update customer profile by user ID + * Useful for admin or other authorized users + */ + @PutMapping("/user/{userId}") + public ResponseEntity updateProfileByUserId( + @PathVariable Long userId, + @Valid @RequestBody UpdateCustomerProfileRequest request) { + + CustomerProfileDTO updatedProfile = customerProfileService.updateCustomerProfile(userId, request); + return ResponseEntity.ok(updatedProfile); + } +} diff --git a/src/main/java/com/example/ead_backend/controller/EmployeeController.java b/src/main/java/com/example/ead_backend/controller/EmployeeController.java new file mode 100644 index 0000000..00c6afc --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/EmployeeController.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.controller; + +public class EmployeeController { + +} diff --git a/src/main/java/com/example/ead_backend/controller/PasswordResetController.java b/src/main/java/com/example/ead_backend/controller/PasswordResetController.java new file mode 100644 index 0000000..f534d8c --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/PasswordResetController.java @@ -0,0 +1,119 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.ChangePasswordRequest; +import com.example.ead_backend.dto.ForgotPasswordRequest; +import com.example.ead_backend.dto.ResetPasswordRequest; +import com.example.ead_backend.dto.VerifyOtpRequest; +import com.example.ead_backend.service.impl.PasswordResetService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@CrossOrigin +@RequestMapping("/api/password") +public class PasswordResetController { + + @Autowired + private PasswordResetService passwordResetService; + + @PostMapping("/forgot") + public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + Map errors = new HashMap<>(); + bindingResult.getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + return ResponseEntity.badRequest().body(errors); + } + + try { + passwordResetService.generateAndSendOTP(request.getEmail()); + Map response = new HashMap<>(); + response.put("message", "OTP has been sent to your email address"); + response.put("email", request.getEmail()); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/verify-otp") + public ResponseEntity verifyOTP(@Valid @RequestBody VerifyOtpRequest request, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + Map errors = new HashMap<>(); + bindingResult.getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + return ResponseEntity.badRequest().body(errors); + } + + try { + boolean valid = passwordResetService.verifyOTP(request.getEmail(), request.getOtp()); + if (valid) { + Map response = new HashMap<>(); + response.put("message", "OTP verified successfully"); + return ResponseEntity.ok(response); + } else { + Map error = new HashMap<>(); + error.put("error", "Invalid OTP"); + return ResponseEntity.badRequest().body(error); + } + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/reset") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request, BindingResult bindingResult) { + if (bindingResult.hasErrors()) { + Map errors = new HashMap<>(); + bindingResult.getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + return ResponseEntity.badRequest().body(errors); + } + + try { + passwordResetService.resetPassword(request.getEmail(), request.getOtp(), request.getNewPassword()); + Map response = new HashMap<>(); + response.put("message", "Password has been reset successfully"); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/change") + public ResponseEntity changePassword(@Valid @RequestBody ChangePasswordRequest request, + BindingResult bindingResult, + Authentication authentication) { + if (bindingResult.hasErrors()) { + Map errors = new HashMap<>(); + bindingResult.getFieldErrors().forEach(error -> + errors.put(error.getField(), error.getDefaultMessage())); + return ResponseEntity.badRequest().body(errors); + } + + try { + String userEmail = authentication.getName(); + passwordResetService.changePassword(userEmail, request.getOldPassword(), request.getNewPassword()); + Map response = new HashMap<>(); + response.put("message", "Password changed successfully"); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } +} diff --git a/src/main/java/com/example/ead_backend/controller/ProgressUpdateController.java b/src/main/java/com/example/ead_backend/controller/ProgressUpdateController.java new file mode 100644 index 0000000..988087b --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/ProgressUpdateController.java @@ -0,0 +1,72 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.dto.ProgressUpdateRequest; +import com.example.ead_backend.service.ProgressService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * REST Controller for employee progress update operations. + */ +@RestController +@RequestMapping("/api/employee/progress") +@RequiredArgsConstructor +@Slf4j +@CrossOrigin(origins = "http://localhost:3000") +public class ProgressUpdateController { + + private final ProgressService progressService; + + /** + * Create or update progress for an appointment. + * + * @param appointmentId the appointment ID + * @param request the progress update request + * @param userId the authenticated user ID from header + * @return the created/updated progress response + */ + @PutMapping("/{appointmentId}") + public ResponseEntity updateProgress( + @PathVariable Long appointmentId, + @Valid @RequestBody ProgressUpdateRequest request, + @RequestHeader(value = "X-User-Id", required = false, defaultValue = "1") Long userId) { + + log.info("Received progress update request for appointment {} from user {}", appointmentId, userId); + + ProgressResponse response = progressService.createOrUpdateProgress(appointmentId, request, userId); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + /** + * Update status only for an appointment. + * + * @param appointmentId the appointment ID + * @param status the new status + * @param userId the authenticated user ID from header + * @return success response + */ + @PostMapping("/{appointmentId}/status") + public ResponseEntity updateStatus( + @PathVariable Long appointmentId, + @RequestParam String status, + @RequestHeader(value = "X-User-Id", required = false, defaultValue = "1") Long userId) { + + log.info("Updating status for appointment {} to {} by user {}", appointmentId, status, userId); + + ProgressUpdateRequest request = ProgressUpdateRequest.builder() + .stage(status) + .percentage(0) // Status update without percentage change + .remarks("Status changed to: " + status) + .build(); + + progressService.createOrUpdateProgress(appointmentId, request, userId); + + return ResponseEntity.ok("Status updated successfully"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/ead_backend/controller/ProgressViewController.java b/src/main/java/com/example/ead_backend/controller/ProgressViewController.java new file mode 100644 index 0000000..d64600f --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/ProgressViewController.java @@ -0,0 +1,80 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.service.ProgressService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * REST Controller for customer progress view operations. + */ +@RestController +@RequestMapping("/api/customer/progress") +@RequiredArgsConstructor +@Slf4j +@CrossOrigin(origins = {"http://localhost:3001", "http://localhost:3000"}) +public class ProgressViewController { + + private final ProgressService progressService; + + /** + * Get all progress updates for an appointment. + * + * @param appointmentId the appointment ID + * @return list of progress responses + */ + @GetMapping("/{appointmentId}") + public ResponseEntity> getProgressHistory( + @PathVariable Long appointmentId) { + + log.info("Fetching progress history for appointment {}", appointmentId); + + List progressList = progressService.getProgressForAppointment(appointmentId); + + return ResponseEntity.ok(progressList); + } + + /** + * Get the latest progress update for an appointment. + * + * @param appointmentId the appointment ID + * @return the latest progress response + */ + @GetMapping("/{appointmentId}/latest") + public ResponseEntity getLatestProgress( + @PathVariable Long appointmentId) { + + log.info("Fetching latest progress for appointment {}", appointmentId); + + List progressList = progressService.getProgressForAppointment(appointmentId); + + if (progressList.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + ProgressResponse latest = progressList.get(progressList.size() - 1); + + return ResponseEntity.ok(latest); + } + + /** + * Get the overall progress percentage for an appointment. + * + * @param appointmentId the appointment ID + * @return the progress percentage + */ + @GetMapping("/{appointmentId}/percentage") + public ResponseEntity getProgressPercentage( + @PathVariable Long appointmentId) { + + log.info("Calculating progress percentage for appointment {}", appointmentId); + + int percentage = progressService.calculateProgressPercentage(appointmentId); + + return ResponseEntity.ok(percentage); + } +} diff --git a/src/main/java/com/example/ead_backend/controller/ProjectController.java b/src/main/java/com/example/ead_backend/controller/ProjectController.java new file mode 100644 index 0000000..0018b67 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/ProjectController.java @@ -0,0 +1,77 @@ +package com.example.ead_backend.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import com.example.ead_backend.dto.ProjectDTO; +import com.example.ead_backend.service.ProjectService; + +import java.util.List; +import java.security.Principal; + +@RestController +@Slf4j +@RequestMapping("/api/projects") +@RequiredArgsConstructor +public class ProjectController { + private final ProjectService projectService; + + private String getCurrentUserId(Principal principal) { + return principal != null ? principal.getName() : null; + } + + @PostMapping + public ProjectDTO create(@RequestBody ProjectDTO dto, Principal principal) { + String userId = getCurrentUserId(principal); + if (userId != null) { + dto.setCustomerId(userId); + } + return projectService.createProject(dto); + } + + @GetMapping("/{id}") + public ProjectDTO getById(@PathVariable String id, Principal principal) { + ProjectDTO project = projectService.getProjectById(id); + String userId = getCurrentUserId(principal); + if (userId != null && !userId.equals(project.getCustomerId())) { + throw new RuntimeException("Not authorized to view this project"); + } + return project; + } + + @GetMapping + public List getAll(@RequestParam(required = false) String customerId, Principal principal) { + if (customerId == null && principal != null) { + customerId = getCurrentUserId(principal); + } + if (customerId != null) { + return projectService.getProjectsByCustomerId(customerId); + } + return projectService.getAllProjects(); + } + + @PutMapping("/{id}") + public ProjectDTO update(@PathVariable String id, @RequestBody ProjectDTO dto, Principal principal) { + ProjectDTO existing = projectService.getProjectById(id); + String userId = getCurrentUserId(principal); + if (userId != null && !userId.equals(existing.getCustomerId())) { + throw new RuntimeException("Not authorized to update this project"); + } + dto.setCustomerId(existing.getCustomerId()); + return projectService.updateProject(id, dto); + } + + @DeleteMapping("/{id}") + public void delete(@PathVariable String id, Principal principal) { + log.info("DELETE /api/projects/{} called, principal={}", id, principal == null ? "" : principal.getName()); + ProjectDTO existing = projectService.getProjectById(id); + String userId = getCurrentUserId(principal); + if (userId != null && !userId.equals(existing.getCustomerId())) { + log.warn("User {} not authorized to delete project {} owned by {}", userId, id, existing.getCustomerId()); + throw new RuntimeException("Not authorized to delete this project"); + } + projectService.deleteProject(id); + log.info("Project {} deleted successfully", id); + } +} diff --git a/src/main/java/com/example/ead_backend/controller/TaskController.java b/src/main/java/com/example/ead_backend/controller/TaskController.java new file mode 100644 index 0000000..92f3fd9 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/TaskController.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.controller; + +public class TaskController { + +} diff --git a/src/main/java/com/example/ead_backend/controller/TimeLogController.java b/src/main/java/com/example/ead_backend/controller/TimeLogController.java new file mode 100644 index 0000000..ddef87a --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/TimeLogController.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.controller; + +public class TimeLogController { + +} diff --git a/src/main/java/com/example/ead_backend/controller/VehicleController.java b/src/main/java/com/example/ead_backend/controller/VehicleController.java new file mode 100644 index 0000000..9b3d264 --- /dev/null +++ b/src/main/java/com/example/ead_backend/controller/VehicleController.java @@ -0,0 +1,97 @@ +package com.example.ead_backend.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import com.example.ead_backend.dto.VehicleDTO; +import com.example.ead_backend.service.VehicleService; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.List; + +@RestController +@RequestMapping("/api/vehicles") +@CrossOrigin(origins = "*") +@RequiredArgsConstructor +public class VehicleController { + + private final VehicleService vehicleService; + private final ObjectMapper objectMapper; + + @PostMapping + public VehicleDTO createVehicle(@RequestBody VehicleDTO vehicleDTO) { + return vehicleService.createVehicle(vehicleDTO); + } + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity createVehicleWithImage( + @RequestPart("vehicle") String vehicleDTOJson, + @RequestPart(value = "image", required = false) MultipartFile image) { + try { + VehicleDTO vehicleDTO = objectMapper.readValue(vehicleDTOJson, VehicleDTO.class); + + if (image != null && !image.isEmpty()) { + VehicleDTO created = vehicleService.createVehicleWithImage(vehicleDTO, image); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } else { + VehicleDTO created = vehicleService.createVehicle(vehicleDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(created); + } + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to create vehicle: " + e.getMessage()); + } + } + + @GetMapping("/{id}") + public VehicleDTO getVehicleById(@PathVariable Long id) { + return vehicleService.getVehicleById(id); + } + + @GetMapping + public List getAllVehicles() { + return vehicleService.getAllVehicles(); + } + + @GetMapping("/customer/{customerId}") + public List getVehiclesByCustomerId(@PathVariable Long customerId) { + return vehicleService.getVehiclesByCustomerId(customerId); + } + + @PutMapping("/{id}") + public VehicleDTO updateVehicle(@PathVariable Long id, @RequestBody VehicleDTO vehicleDTO) { + return vehicleService.updateVehicle(id, vehicleDTO); + } + + @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity updateVehicleWithImage( + @PathVariable Long id, + @RequestPart("vehicle") String vehicleDTOJson, + @RequestPart(value = "image", required = false) MultipartFile image) { + try { + VehicleDTO vehicleDTO = objectMapper.readValue(vehicleDTOJson, VehicleDTO.class); + + if (image != null && !image.isEmpty()) { + VehicleDTO updated = vehicleService.updateVehicleWithImage(id, vehicleDTO, image); + return ResponseEntity.ok(updated); + } else { + VehicleDTO updated = vehicleService.updateVehicle(id, vehicleDTO); + return ResponseEntity.ok(updated); + } + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Failed to update vehicle: " + e.getMessage()); + } + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteVehicle(@PathVariable Long id) { + vehicleService.deleteVehicle(id); + return ResponseEntity.ok("Vehicle deleted successfully"); + } +} diff --git a/src/main/java/com/example/ead_backend/dto/AdminDTO.java b/src/main/java/com/example/ead_backend/dto/AdminDTO.java new file mode 100644 index 0000000..198e46f --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/AdminDTO.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.dto; + +public class AdminDTO { + +} diff --git a/src/main/java/com/example/ead_backend/dto/AppointmentDTO.java b/src/main/java/com/example/ead_backend/dto/AppointmentDTO.java new file mode 100644 index 0000000..032e8c4 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/AppointmentDTO.java @@ -0,0 +1,18 @@ +package com.example.ead_backend.dto; + +import java.time.LocalDate; +import com.example.ead_backend.model.enums.AppointmentStatus; +import lombok.Data; + +@Data +public class AppointmentDTO { + private String appointmentId; + private String service; + private String customerId; + private String vehicleId; + private String vehicleNo; + private LocalDate date; + private String startTime; + private String endTime; + private AppointmentStatus status; +} diff --git a/src/main/java/com/example/ead_backend/dto/AuthResponse.java b/src/main/java/com/example/ead_backend/dto/AuthResponse.java new file mode 100644 index 0000000..049ba99 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/AuthResponse.java @@ -0,0 +1,28 @@ +package com.example.ead_backend.dto; + +public class AuthResponse { + private String token; + private String type = "Bearer"; + private String email; + private String role; + + public AuthResponse() {} + + public AuthResponse(String token, String email, String role) { + this.token = token; + this.email = email; + this.role = role; + } + + public String getToken() { return token; } + public void setToken(String token) { this.token = token; } + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } +} diff --git a/src/main/java/com/example/ead_backend/dto/ChangePasswordRequest.java b/src/main/java/com/example/ead_backend/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..6cdaf84 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/ChangePasswordRequest.java @@ -0,0 +1,17 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class ChangePasswordRequest { + + @NotBlank(message = "Old password is required") + private String oldPassword; + + @NotBlank(message = "New password is required") + @Size(min = 6, message = "New password must be at least 6 characters") + private String newPassword; +} + diff --git a/src/main/java/com/example/ead_backend/dto/CreateEmployeeRequest.java b/src/main/java/com/example/ead_backend/dto/CreateEmployeeRequest.java new file mode 100644 index 0000000..071f2c3 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/CreateEmployeeRequest.java @@ -0,0 +1,43 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class CreateEmployeeRequest { + + @NotBlank(message = "First name is required") + private String firstName; + + @NotBlank(message = "Last name is required") + private String lastName; + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 6, message = "Password must be at least 6 characters long") + private String password; + + public CreateEmployeeRequest() {} + + public CreateEmployeeRequest(String firstName, String lastName, String email, String password) { + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.password = password; + } + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } +} diff --git a/src/main/java/com/example/ead_backend/dto/CustomerDTO.java b/src/main/java/com/example/ead_backend/dto/CustomerDTO.java new file mode 100644 index 0000000..8637136 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/CustomerDTO.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.dto; + +public class CustomerDTO { + +} diff --git a/src/main/java/com/example/ead_backend/dto/CustomerProfileDTO.java b/src/main/java/com/example/ead_backend/dto/CustomerProfileDTO.java new file mode 100644 index 0000000..de48fd5 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/CustomerProfileDTO.java @@ -0,0 +1,23 @@ +package com.example.ead_backend.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * DTO for customer profile information + * Excludes password and email (non-editable fields) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CustomerProfileDTO { + private Long id; + private Long userId; + private String firstName; + private String lastName; + private String email; // Read-only, not editable + private String phoneNumber; +} diff --git a/src/main/java/com/example/ead_backend/dto/EmployeeCreateDTO.java b/src/main/java/com/example/ead_backend/dto/EmployeeCreateDTO.java new file mode 100644 index 0000000..5f37521 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/EmployeeCreateDTO.java @@ -0,0 +1,42 @@ +package com.example.ead_backend.dto; + +import java.time.LocalDate; + +public class EmployeeCreateDTO { + + private Long id; + private String firstName; + private String lastName; + private String email; + private String role; + private LocalDate joinedDate; + + public EmployeeCreateDTO() {} + + public EmployeeCreateDTO(Long id, String firstName, String lastName, String email, String role, LocalDate joinedDate) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.role = role; + this.joinedDate = joinedDate; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + + public LocalDate getJoinedDate() { return joinedDate; } + public void setJoinedDate(LocalDate joinedDate) { this.joinedDate = joinedDate; } +} diff --git a/src/main/java/com/example/ead_backend/dto/EmployeeDTO.java b/src/main/java/com/example/ead_backend/dto/EmployeeDTO.java new file mode 100644 index 0000000..188c5dc --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/EmployeeDTO.java @@ -0,0 +1,20 @@ +package com.example.ead_backend.dto; + +import java.io.Serializable; +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public class EmployeeDTO implements Serializable { + private String employeeId; + private String name; + private String email; + private String password; + private String specialization; + private LocalDate joinedDate; +} diff --git a/src/main/java/com/example/ead_backend/dto/ForgotPasswordRequest.java b/src/main/java/com/example/ead_backend/dto/ForgotPasswordRequest.java new file mode 100644 index 0000000..50d4860 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/ForgotPasswordRequest.java @@ -0,0 +1,23 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class ForgotPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + public ForgotPasswordRequest() {} + + public ForgotPasswordRequest(String email) { + this.email = email; + } + +} + diff --git a/src/main/java/com/example/ead_backend/dto/LoginRequest.java b/src/main/java/com/example/ead_backend/dto/LoginRequest.java new file mode 100644 index 0000000..1e1126a --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/LoginRequest.java @@ -0,0 +1,26 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class LoginRequest { + @NotBlank(message = "Email is required") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 6, message = "Password must be at least 6 characters long") + private String password; + + public LoginRequest() {} + + public LoginRequest(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } +} diff --git a/src/main/java/com/example/ead_backend/dto/NotificationDTO.java b/src/main/java/com/example/ead_backend/dto/NotificationDTO.java new file mode 100644 index 0000000..8ea5751 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/NotificationDTO.java @@ -0,0 +1,26 @@ +package com.example.ead_backend.dto; + +import com.example.ead_backend.model.enums.NotificationType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * DTO for notification information. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotificationDTO { + + private Long id; + private Long userId; + private NotificationType type; + private String message; + private Boolean isRead; + private Timestamp createdAt; +} diff --git a/src/main/java/com/example/ead_backend/dto/ProgressResponse.java b/src/main/java/com/example/ead_backend/dto/ProgressResponse.java new file mode 100644 index 0000000..5077f51 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/ProgressResponse.java @@ -0,0 +1,26 @@ +package com.example.ead_backend.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * Response DTO for progress update information. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProgressResponse { + + private Long id; + private Long appointmentId; + private String stage; + private Integer percentage; + private String remarks; + private Long updatedBy; + private Timestamp updatedAt; +} diff --git a/src/main/java/com/example/ead_backend/dto/ProgressUpdateRequest.java b/src/main/java/com/example/ead_backend/dto/ProgressUpdateRequest.java new file mode 100644 index 0000000..8825ae9 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/ProgressUpdateRequest.java @@ -0,0 +1,30 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request DTO for creating or updating progress on an appointment. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProgressUpdateRequest { + + @NotBlank(message = "Stage is required") + private String stage; + + @NotNull(message = "Percentage is required") + @Min(value = 0, message = "Percentage must be at least 0") + @Max(value = 100, message = "Percentage must not exceed 100") + private Integer percentage; + + private String remarks; +} diff --git a/src/main/java/com/example/ead_backend/dto/ProjectDTO.java b/src/main/java/com/example/ead_backend/dto/ProjectDTO.java new file mode 100644 index 0000000..d7acfd9 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/ProjectDTO.java @@ -0,0 +1,16 @@ +package com.example.ead_backend.dto; + +import java.time.LocalDate; +import com.example.ead_backend.model.enums.ProjectStatus; +import lombok.Data; + +@Data +public class ProjectDTO { + private String projectId; + private String name; + private String description; + private String customerId; + private LocalDate startDate; + private LocalDate endDate; + private ProjectStatus status; +} diff --git a/src/main/java/com/example/ead_backend/dto/ResetPasswordRequest.java b/src/main/java/com/example/ead_backend/dto/ResetPasswordRequest.java new file mode 100644 index 0000000..932e09e --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/ResetPasswordRequest.java @@ -0,0 +1,53 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class ResetPasswordRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "OTP is required") + @Size(min = 6, max = 6, message = "OTP must be 6 digits") + private String otp; + + @NotBlank(message = "New password is required") + @Size(min = 6, message = "Password must be at least 6 characters") + private String newPassword; + + public ResetPasswordRequest() {} + + public ResetPasswordRequest(String email, String otp, String newPassword) { + this.email = email; + this.otp = otp; + this.newPassword = newPassword; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getOtp() { + return otp; + } + + public void setOtp(String otp) { + this.otp = otp; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} + diff --git a/src/main/java/com/example/ead_backend/dto/SignupRequest.java b/src/main/java/com/example/ead_backend/dto/SignupRequest.java new file mode 100644 index 0000000..f07681e --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/SignupRequest.java @@ -0,0 +1,56 @@ +package com.example.ead_backend.dto; + +import com.example.ead_backend.model.enums.Role; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class SignupRequest { + @NotBlank(message = "First name is required") + private String firstName; + + @NotBlank(message = "Last name is required") + private String lastName; + + @NotBlank(message = "Password is required") + @Size(min = 6, message = "Password must be at least 6 characters long") + private String password; + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "Phone number is required") + private String phoneNumber; + + private Role role = Role.CUSTOMER; // Default to CUSTOMER + + public SignupRequest() {} + + public SignupRequest(String firstName, String lastName, String password, String email, String phoneNumber, Role role) { + this.firstName = firstName; + this.lastName = lastName; + this.password = password; + this.email = email; + this.phoneNumber = phoneNumber; + this.role = role; + } + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public String getPhoneNumber() { return phoneNumber; } + public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + + public Role getRole() { return role; } + public void setRole(Role role) { this.role = role; } +} diff --git a/src/main/java/com/example/ead_backend/dto/TaskDTO.java b/src/main/java/com/example/ead_backend/dto/TaskDTO.java new file mode 100644 index 0000000..8a67aae --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/TaskDTO.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.dto; + +public class TaskDTO { + +} diff --git a/src/main/java/com/example/ead_backend/dto/TimeLogDTO.java b/src/main/java/com/example/ead_backend/dto/TimeLogDTO.java new file mode 100644 index 0000000..c8d49c5 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/TimeLogDTO.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.dto; + +public class TimeLogDTO { + +} diff --git a/src/main/java/com/example/ead_backend/dto/UpdateCustomerProfileRequest.java b/src/main/java/com/example/ead_backend/dto/UpdateCustomerProfileRequest.java new file mode 100644 index 0000000..a945b9a --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/UpdateCustomerProfileRequest.java @@ -0,0 +1,29 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * DTO for updating customer profile + * Only includes editable fields (firstName, lastName, phoneNumber) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UpdateCustomerProfileRequest { + + @NotBlank(message = "First name is required") + private String firstName; + + @NotBlank(message = "Last name is required") + private String lastName; + + @NotBlank(message = "Phone number is required") + @Pattern(regexp = "^[0-9]{10,15}$", message = "Phone number must be between 10 and 15 digits") + private String phoneNumber; +} diff --git a/src/main/java/com/example/ead_backend/dto/VehicleDTO.java b/src/main/java/com/example/ead_backend/dto/VehicleDTO.java new file mode 100644 index 0000000..c45633e --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/VehicleDTO.java @@ -0,0 +1,21 @@ +package com.example.ead_backend.dto; + +import lombok.*; + +import java.time.LocalDate; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class VehicleDTO { + private Long id; + private Long customerId; + private String model; + private String color; + private String vin; + private String licensePlate; + private int year; + private LocalDate registrationDate; + private String imageUrl; +} diff --git a/src/main/java/com/example/ead_backend/dto/VerifyOtpRequest.java b/src/main/java/com/example/ead_backend/dto/VerifyOtpRequest.java new file mode 100644 index 0000000..83531a9 --- /dev/null +++ b/src/main/java/com/example/ead_backend/dto/VerifyOtpRequest.java @@ -0,0 +1,40 @@ +package com.example.ead_backend.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class VerifyOtpRequest { + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "OTP is required") + @Size(min = 6, max = 6, message = "OTP must be 6 digits") + private String otp; + + public VerifyOtpRequest() {} + + public VerifyOtpRequest(String email, String otp) { + this.email = email; + this.otp = otp; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getOtp() { + return otp; + } + + public void setOtp(String otp) { + this.otp = otp; + } +} + diff --git a/src/main/java/com/example/ead_backend/dto/appointment/AppointmentDetailsResponse.java b/src/main/java/com/example/ead_backend/dto/appointment/AppointmentDetailsResponse.java deleted file mode 100644 index 1854eac..0000000 --- a/src/main/java/com/example/ead_backend/dto/appointment/AppointmentDetailsResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.appointment; - -public class AppointmentDetailsResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/appointment/AppointmentResponse.java b/src/main/java/com/example/ead_backend/dto/appointment/AppointmentResponse.java deleted file mode 100644 index a09854c..0000000 --- a/src/main/java/com/example/ead_backend/dto/appointment/AppointmentResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.appointment; - -public class AppointmentResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/appointment/CreateAppointmentRequest.java b/src/main/java/com/example/ead_backend/dto/appointment/CreateAppointmentRequest.java deleted file mode 100644 index 5207185..0000000 --- a/src/main/java/com/example/ead_backend/dto/appointment/CreateAppointmentRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.appointment; - -public class CreateAppointmentRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/appointment/UpdateAppointmentRequest.java b/src/main/java/com/example/ead_backend/dto/appointment/UpdateAppointmentRequest.java deleted file mode 100644 index 08e97ee..0000000 --- a/src/main/java/com/example/ead_backend/dto/appointment/UpdateAppointmentRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.appointment; - -public class UpdateAppointmentRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/auth/AuthResponse.java b/src/main/java/com/example/ead_backend/dto/auth/AuthResponse.java deleted file mode 100644 index e203a1f..0000000 --- a/src/main/java/com/example/ead_backend/dto/auth/AuthResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.auth; - -public class AuthResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/auth/LoginRequest.java b/src/main/java/com/example/ead_backend/dto/auth/LoginRequest.java deleted file mode 100644 index 74b414b..0000000 --- a/src/main/java/com/example/ead_backend/dto/auth/LoginRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.auth; - -public class LoginRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/auth/PasswordResetRequest.java b/src/main/java/com/example/ead_backend/dto/auth/PasswordResetRequest.java deleted file mode 100644 index 504d1a5..0000000 --- a/src/main/java/com/example/ead_backend/dto/auth/PasswordResetRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.auth; - -public class PasswordResetRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/auth/SignupRequest.java b/src/main/java/com/example/ead_backend/dto/auth/SignupRequest.java deleted file mode 100644 index dc13411..0000000 --- a/src/main/java/com/example/ead_backend/dto/auth/SignupRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.auth; - -public class SignupRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/auth/UserResponse.java b/src/main/java/com/example/ead_backend/dto/auth/UserResponse.java deleted file mode 100644 index 14d8f8d..0000000 --- a/src/main/java/com/example/ead_backend/dto/auth/UserResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.auth; - -public class UserResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/dashboard/AdminDashboardResponse.java b/src/main/java/com/example/ead_backend/dto/dashboard/AdminDashboardResponse.java deleted file mode 100644 index 47b5ef8..0000000 --- a/src/main/java/com/example/ead_backend/dto/dashboard/AdminDashboardResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.dashboard; - -public class AdminDashboardResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/dashboard/CustomerDashboardResponse.java b/src/main/java/com/example/ead_backend/dto/dashboard/CustomerDashboardResponse.java deleted file mode 100644 index cd102ca..0000000 --- a/src/main/java/com/example/ead_backend/dto/dashboard/CustomerDashboardResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.dashboard; - -public class CustomerDashboardResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/dashboard/EmployeeDashboardResponse.java b/src/main/java/com/example/ead_backend/dto/dashboard/EmployeeDashboardResponse.java deleted file mode 100644 index 265fbd3..0000000 --- a/src/main/java/com/example/ead_backend/dto/dashboard/EmployeeDashboardResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.dashboard; - -public class EmployeeDashboardResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/service/CreateServiceRequest.java b/src/main/java/com/example/ead_backend/dto/service/CreateServiceRequest.java deleted file mode 100644 index 09aeea7..0000000 --- a/src/main/java/com/example/ead_backend/dto/service/CreateServiceRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.service; - -public class CreateServiceRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/service/UpdateServiceRequest.java b/src/main/java/com/example/ead_backend/dto/service/UpdateServiceRequest.java deleted file mode 100644 index 9fd8330..0000000 --- a/src/main/java/com/example/ead_backend/dto/service/UpdateServiceRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.service; - -public class UpdateServiceRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/timelog/TimeLogRequest.java b/src/main/java/com/example/ead_backend/dto/timelog/TimeLogRequest.java deleted file mode 100644 index 520e934..0000000 --- a/src/main/java/com/example/ead_backend/dto/timelog/TimeLogRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.timelog; - -public class TimeLogRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/timelog/TimeLogResponse.java b/src/main/java/com/example/ead_backend/dto/timelog/TimeLogResponse.java deleted file mode 100644 index 33dc13d..0000000 --- a/src/main/java/com/example/ead_backend/dto/timelog/TimeLogResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.timelog; - -public class TimeLogResponse { - -} diff --git a/src/main/java/com/example/ead_backend/dto/vehicle/CreateVehicleRequest.java b/src/main/java/com/example/ead_backend/dto/vehicle/CreateVehicleRequest.java deleted file mode 100644 index facb0f5..0000000 --- a/src/main/java/com/example/ead_backend/dto/vehicle/CreateVehicleRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.vehicle; - -public class CreateVehicleRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/vehicle/UpdateVehicleRequest.java b/src/main/java/com/example/ead_backend/dto/vehicle/UpdateVehicleRequest.java deleted file mode 100644 index a0aac33..0000000 --- a/src/main/java/com/example/ead_backend/dto/vehicle/UpdateVehicleRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.vehicle; - -public class UpdateVehicleRequest { - -} diff --git a/src/main/java/com/example/ead_backend/dto/vehicle/VehicleResponse.java b/src/main/java/com/example/ead_backend/dto/vehicle/VehicleResponse.java deleted file mode 100644 index e3cd598..0000000 --- a/src/main/java/com/example/ead_backend/dto/vehicle/VehicleResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.dto.vehicle; - -public class VehicleResponse { - -} diff --git a/src/main/java/com/example/ead_backend/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/ead_backend/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..059a649 --- /dev/null +++ b/src/main/java/com/example/ead_backend/filter/JwtAuthenticationFilter.java @@ -0,0 +1,59 @@ +package com.example.ead_backend.filter; + +import com.example.ead_backend.service.impl.UserService; +import com.example.ead_backend.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + @Lazy + private UserService userService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + final String authHeader = request.getHeader("Authorization"); + final String jwt; + final String email; + + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); + email = jwtUtil.extractUsername(jwt); // This extracts email now + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userService.loadUserByUsername(email); + + if (jwtUtil.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/example/ead_backend/mapper/AppointmentMapper.java b/src/main/java/com/example/ead_backend/mapper/AppointmentMapper.java index ce84051..5fe7b82 100644 --- a/src/main/java/com/example/ead_backend/mapper/AppointmentMapper.java +++ b/src/main/java/com/example/ead_backend/mapper/AppointmentMapper.java @@ -1,5 +1,25 @@ package com.example.ead_backend.mapper; -public class AppointmentMapper { +import org.mapstruct.Mapper; +import org.mapstruct.AfterMapping; +import org.mapstruct.MappingTarget; +import com.example.ead_backend.dto.AppointmentDTO; +import com.example.ead_backend.model.entity.Appointment; + +@Mapper(componentModel = "spring") +public interface AppointmentMapper { + AppointmentDTO toDTO(Appointment entity); + Appointment toEntity(AppointmentDTO dto); + + @AfterMapping + default void setLegacyTime(@MappingTarget Appointment entity, AppointmentDTO dto) { + if (entity == null) return; + String start = dto != null ? dto.getStartTime() : null; + String end = dto != null ? dto.getEndTime() : null; + String derived = (start != null && end != null) + ? start + "-" + end + : (start != null ? start : "00:00"); + entity.setTime(derived); + } } diff --git a/src/main/java/com/example/ead_backend/mapper/AppointmentMapperManual.java b/src/main/java/com/example/ead_backend/mapper/AppointmentMapperManual.java new file mode 100644 index 0000000..fad4041 --- /dev/null +++ b/src/main/java/com/example/ead_backend/mapper/AppointmentMapperManual.java @@ -0,0 +1,49 @@ +package com.example.ead_backend.mapper; + +import com.example.ead_backend.dto.AppointmentDTO; +import com.example.ead_backend.model.entity.Appointment; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +// Fallback manual mapper (not a Spring bean). Kept only to avoid compile issues +// when MapStruct processing is unavailable. MapStruct will generate the real bean. +@Primary +@Component +public class AppointmentMapperManual implements AppointmentMapper { + + @Override + public AppointmentDTO toDTO(Appointment entity) { + if (entity == null) return null; + AppointmentDTO dto = new AppointmentDTO(); + dto.setAppointmentId(entity.getAppointmentId()); + dto.setService(entity.getService()); + dto.setCustomerId(entity.getCustomerId()); + dto.setVehicleId(entity.getVehicleId()); + dto.setVehicleNo(entity.getVehicleNo()); + dto.setDate(entity.getDate()); + dto.setStartTime(entity.getStartTime()); + dto.setEndTime(entity.getEndTime()); + dto.setStatus(entity.getStatus()); + return dto; + } + + @Override + public Appointment toEntity(AppointmentDTO dto) { + if (dto == null) return null; + Appointment entity = new Appointment(); + entity.setAppointmentId(dto.getAppointmentId()); + entity.setService(dto.getService()); + entity.setCustomerId(dto.getCustomerId()); + entity.setVehicleId(dto.getVehicleId()); + entity.setVehicleNo(dto.getVehicleNo()); + entity.setDate(dto.getDate()); + entity.setStartTime(dto.getStartTime()); + entity.setEndTime(dto.getEndTime()); + // Set legacy 'time' column as a derived range to satisfy NOT NULL + String start = dto.getStartTime(); + String end = dto.getEndTime(); + entity.setTime((start != null && end != null) ? start + "-" + end : (start != null ? start : "00:00")); + entity.setStatus(dto.getStatus()); + return entity; + } +} diff --git a/src/main/java/com/example/ead_backend/mapper/NotificationMapper.java b/src/main/java/com/example/ead_backend/mapper/NotificationMapper.java new file mode 100644 index 0000000..49dd7c3 --- /dev/null +++ b/src/main/java/com/example/ead_backend/mapper/NotificationMapper.java @@ -0,0 +1,20 @@ +package com.example.ead_backend.mapper; + +import com.example.ead_backend.dto.NotificationDTO; +import com.example.ead_backend.model.entity.Notification; +import org.mapstruct.Mapper; + +/** + * MapStruct mapper for Notification entity and DTOs. + */ +@Mapper(componentModel = "spring") +public interface NotificationMapper { + + /** + * Convert Notification entity to NotificationDTO. + * + * @param entity the notification entity + * @return the notification DTO + */ + NotificationDTO toDto(Notification entity); +} diff --git a/src/main/java/com/example/ead_backend/mapper/ProgressMapper.java b/src/main/java/com/example/ead_backend/mapper/ProgressMapper.java new file mode 100644 index 0000000..a9832bd --- /dev/null +++ b/src/main/java/com/example/ead_backend/mapper/ProgressMapper.java @@ -0,0 +1,36 @@ +package com.example.ead_backend.mapper; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.dto.ProgressUpdateRequest; +import com.example.ead_backend.model.entity.ProgressUpdate; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * MapStruct mapper for ProgressUpdate entity and DTOs. + */ +@Mapper(componentModel = "spring") +public interface ProgressMapper { + + /** + * Convert ProgressUpdate entity to ProgressResponse DTO. + * + * @param entity the progress update entity + * @return the progress response DTO + */ + ProgressResponse toResponse(ProgressUpdate entity); + + /** + * Convert ProgressUpdateRequest DTO to ProgressUpdate entity. + * Note: appointmentId and updatedBy must be set separately. + * + * @param dto the progress update request + * @return the progress update entity + */ + @Mapping(target = "id", ignore = true) + @Mapping(target = "appointmentId", ignore = true) + @Mapping(target = "updatedBy", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + ProgressUpdate toEntity(ProgressUpdateRequest dto); +} diff --git a/src/main/java/com/example/ead_backend/mapper/ProjectMapper.java b/src/main/java/com/example/ead_backend/mapper/ProjectMapper.java new file mode 100644 index 0000000..d0fb5b3 --- /dev/null +++ b/src/main/java/com/example/ead_backend/mapper/ProjectMapper.java @@ -0,0 +1,12 @@ +package com.example.ead_backend.mapper; + +import org.mapstruct.Mapper; + +import com.example.ead_backend.dto.ProjectDTO; +import com.example.ead_backend.model.entity.Project; + +@Mapper(componentModel = "spring") +public interface ProjectMapper { + ProjectDTO toDTO(Project entity); + Project toEntity(ProjectDTO dto); +} diff --git a/src/main/java/com/example/ead_backend/mapper/ProjectMapperManual.java b/src/main/java/com/example/ead_backend/mapper/ProjectMapperManual.java new file mode 100644 index 0000000..2c60918 --- /dev/null +++ b/src/main/java/com/example/ead_backend/mapper/ProjectMapperManual.java @@ -0,0 +1,41 @@ +package com.example.ead_backend.mapper; + +import com.example.ead_backend.dto.ProjectDTO; +import com.example.ead_backend.model.entity.Project; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +// Fallback manual mapper (not a Spring bean). Kept only to avoid compile issues +// when MapStruct processing is unavailable. MapStruct will generate the real bean. +@Primary +@Component +public class ProjectMapperManual implements ProjectMapper { + + @Override + public ProjectDTO toDTO(Project entity) { + if (entity == null) return null; + ProjectDTO dto = new ProjectDTO(); + dto.setProjectId(entity.getProjectId()); + dto.setName(entity.getName()); + dto.setDescription(entity.getDescription()); + dto.setCustomerId(entity.getCustomerId()); + dto.setStartDate(entity.getStartDate()); + dto.setEndDate(entity.getEndDate()); + dto.setStatus(entity.getStatus()); + return dto; + } + + @Override + public Project toEntity(ProjectDTO dto) { + if (dto == null) return null; + Project entity = new Project(); + entity.setProjectId(dto.getProjectId()); + entity.setName(dto.getName()); + entity.setDescription(dto.getDescription()); + entity.setCustomerId(dto.getCustomerId()); + entity.setStartDate(dto.getStartDate()); + entity.setEndDate(dto.getEndDate()); + entity.setStatus(dto.getStatus()); + return entity; + } +} diff --git a/src/main/java/com/example/ead_backend/mapper/ServiceMapper.java b/src/main/java/com/example/ead_backend/mapper/ServiceMapper.java deleted file mode 100644 index d9b58dd..0000000 --- a/src/main/java/com/example/ead_backend/mapper/ServiceMapper.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.mapper; - -public class ServiceMapper { - -} diff --git a/src/main/java/com/example/ead_backend/mapper/TimeLogMapper.java b/src/main/java/com/example/ead_backend/mapper/TimeLogMapper.java deleted file mode 100644 index e7d2d2c..0000000 --- a/src/main/java/com/example/ead_backend/mapper/TimeLogMapper.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.mapper; - -public class TimeLogMapper { - -} diff --git a/src/main/java/com/example/ead_backend/mapper/UserMapper.java b/src/main/java/com/example/ead_backend/mapper/UserMapper.java deleted file mode 100644 index 967f3ef..0000000 --- a/src/main/java/com/example/ead_backend/mapper/UserMapper.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.mapper; - -public class UserMapper { - -} diff --git a/src/main/java/com/example/ead_backend/mapper/VehicleMapper.java b/src/main/java/com/example/ead_backend/mapper/VehicleMapper.java index 1802afd..b395466 100644 --- a/src/main/java/com/example/ead_backend/mapper/VehicleMapper.java +++ b/src/main/java/com/example/ead_backend/mapper/VehicleMapper.java @@ -1,5 +1,20 @@ package com.example.ead_backend.mapper; -public class VehicleMapper { +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; +import com.example.ead_backend.dto.VehicleDTO; +import com.example.ead_backend.model.entity.Vehicle; + +@Mapper(componentModel = "spring") +public interface VehicleMapper { + VehicleMapper INSTANCE = Mappers.getMapper(VehicleMapper.class); + + @Mapping(source = "customer.id", target = "customerId") + VehicleDTO toDTO(Vehicle vehicle); + + @Mapping(target = "customer", ignore = true) + @Mapping(target = "imagePublicId", ignore = true) + Vehicle toEntity(VehicleDTO dto); } diff --git a/src/main/java/com/example/ead_backend/mapper/VehicleMapperManual.java b/src/main/java/com/example/ead_backend/mapper/VehicleMapperManual.java new file mode 100644 index 0000000..a54b3c4 --- /dev/null +++ b/src/main/java/com/example/ead_backend/mapper/VehicleMapperManual.java @@ -0,0 +1,49 @@ +package com.example.ead_backend.mapper; + +import com.example.ead_backend.dto.VehicleDTO; +import com.example.ead_backend.model.entity.Vehicle; +import com.example.ead_backend.model.entity.Customer; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Primary +@Component +public class VehicleMapperManual implements VehicleMapper { + + @Override + public VehicleDTO toDTO(Vehicle vehicle) { + if (vehicle == null) return null; + VehicleDTO dto = VehicleDTO.builder() + .id(vehicle.getId()) + .model(vehicle.getModel()) + .color(vehicle.getColor()) + .vin(vehicle.getVin()) + .licensePlate(vehicle.getLicensePlate()) + .year(vehicle.getYear()) + .registrationDate(vehicle.getRegistrationDate()) + .imageUrl(vehicle.getImageUrl()) + .build(); + Customer customer = vehicle.getCustomer(); + if (customer != null) { + dto.setCustomerId(customer.getId()); + } + return dto; + } + + @Override + public Vehicle toEntity(VehicleDTO dto) { + if (dto == null) return null; + Vehicle entity = Vehicle.builder() + .id(dto.getId()) + .model(dto.getModel()) + .color(dto.getColor()) + .vin(dto.getVin()) + .licensePlate(dto.getLicensePlate()) + .year(dto.getYear()) + .registrationDate(dto.getRegistrationDate()) + .imageUrl(dto.getImageUrl()) + .build(); + // customer is set in service layer after lookup + return entity; + } +} diff --git a/src/main/java/com/example/ead_backend/model/entity/Appointment.java b/src/main/java/com/example/ead_backend/model/entity/Appointment.java index dc395af..1c576c9 100644 --- a/src/main/java/com/example/ead_backend/model/entity/Appointment.java +++ b/src/main/java/com/example/ead_backend/model/entity/Appointment.java @@ -1,5 +1,52 @@ package com.example.ead_backend.model.entity; +import jakarta.persistence.*; +import lombok.Data; +import java.time.LocalDate; +import com.example.ead_backend.model.enums.AppointmentStatus; +import org.hibernate.annotations.UuidGenerator; + +@Entity +@Table(name = "Appointments") +@Data public class Appointment { + @Id + @UuidGenerator + @Column(columnDefinition = "VARCHAR(255)") + private String appointmentId; + + @Column(nullable = false) + private String service; + + @Column(nullable = true) + private String customerId; + + @Column(nullable = true) + private String vehicleId; + + @Column(nullable = false) + private String vehicleNo; + + @Column(nullable = false) + private LocalDate date; // appointment date + + // Start time (maps to DB column start_time) + @Column(name = "start_time", nullable = false) + private String startTime; + + // Legacy column 'time' kept by existing DB; we persist a derived value to satisfy NOT NULL + @Column(name = "time", nullable = false) + private String time; + + // Some existing databases enforce a NOT NULL on end_time + @Column(name = "end_time", nullable = false) + private String endTime; + + @ManyToOne + @JoinColumn(name = "employee_id") + private Employee employee; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private AppointmentStatus status; } diff --git a/src/main/java/com/example/ead_backend/model/entity/Customer.java b/src/main/java/com/example/ead_backend/model/entity/Customer.java new file mode 100644 index 0000000..cb66a92 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/entity/Customer.java @@ -0,0 +1,39 @@ +package com.example.ead_backend.model.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "customers") +@Data +public class Customer { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "user_id", nullable = false, unique = true) + private User user; + + @Column(nullable = false) + private String phoneNumber; + + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) + private List vehicles; + +// @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) +// private List appointments; + + public Customer(User user, String phoneNumber) { + this.user = user; + this.phoneNumber = phoneNumber; + } +} + diff --git a/src/main/java/com/example/ead_backend/model/entity/Employee.java b/src/main/java/com/example/ead_backend/model/entity/Employee.java new file mode 100644 index 0000000..2d170e4 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/entity/Employee.java @@ -0,0 +1,44 @@ +package com.example.ead_backend.model.entity; + +import java.time.LocalDate; +import java.util.List; + +import com.example.ead_backend.model.enums.Role; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Entity +@Table(name = "employees") +@Data +public class Employee { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "user_id", nullable = false, unique = true) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; // ADMIN or EMPLOYEE + + @Column(nullable = false) + private LocalDate joinedDate; + + @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true) + private List appointments; + + @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true) + private List projects; + + public Employee(User user, Role role, LocalDate joinedDate) { + this.user = user; + this.role = role; + this.joinedDate = joinedDate; + } +} diff --git a/src/main/java/com/example/ead_backend/model/entity/Notification.java b/src/main/java/com/example/ead_backend/model/entity/Notification.java index 69570b4..651bea8 100644 --- a/src/main/java/com/example/ead_backend/model/entity/Notification.java +++ b/src/main/java/com/example/ead_backend/model/entity/Notification.java @@ -1,5 +1,47 @@ package com.example.ead_backend.model.entity; +import com.example.ead_backend.model.enums.NotificationType; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.sql.Timestamp; + +/** + * Entity representing a notification sent to a user. + * Used for tracking progress updates, status changes, and general + * notifications. + */ +@Entity +@Table(name = "notifications") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor public class Notification { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "type", nullable = false, length = 50) + private NotificationType type; + + @Column(name = "message", nullable = false, length = 1000) + private String message; + + @Column(name = "is_read", nullable = false) + @Builder.Default + private Boolean isRead = false; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private Timestamp createdAt; } diff --git a/src/main/java/com/example/ead_backend/model/entity/OTP.java b/src/main/java/com/example/ead_backend/model/entity/OTP.java new file mode 100644 index 0000000..bf5544f --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/entity/OTP.java @@ -0,0 +1,64 @@ +package com.example.ead_backend.model.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "otp") +public class OTP { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String otp; + + @Column(nullable = false) + private LocalDateTime expiryTime; + + public OTP() {} + + public OTP(String email, String otp, LocalDateTime expiryTime) { + this.email = email; + this.otp = otp; + this.expiryTime = expiryTime; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getOtp() { + return otp; + } + + public void setOtp(String otp) { + this.otp = otp; + } + + public LocalDateTime getExpiryTime() { + return expiryTime; + } + + public void setExpiryTime(LocalDateTime expiryTime) { + this.expiryTime = expiryTime; + } +} + diff --git a/src/main/java/com/example/ead_backend/model/entity/ProgressUpdate.java b/src/main/java/com/example/ead_backend/model/entity/ProgressUpdate.java new file mode 100644 index 0000000..d477b92 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/entity/ProgressUpdate.java @@ -0,0 +1,51 @@ +package com.example.ead_backend.model.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.sql.Timestamp; + +/** + * Entity representing a progress update for an appointment. + * Tracks the stage, percentage completion, and remarks for service progress. + */ +@Entity +@Table(name = "progress_updates") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProgressUpdate { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "appointment_id", nullable = false) + private Long appointmentId; + + @Column(name = "stage", nullable = false, length = 100) + private String stage; + + @Column(name = "percentage", nullable = false) + private Integer percentage; + + @Column(name = "remarks", length = 500) + private String remarks; + + @Column(name = "updated_by", nullable = false) + private Long updatedBy; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private Timestamp createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private Timestamp updatedAt; +} diff --git a/src/main/java/com/example/ead_backend/model/entity/Project.java b/src/main/java/com/example/ead_backend/model/entity/Project.java new file mode 100644 index 0000000..f329544 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/entity/Project.java @@ -0,0 +1,40 @@ +package com.example.ead_backend.model.entity; + +import jakarta.persistence.*; +import lombok.Data; +import java.time.LocalDate; +import org.hibernate.annotations.UuidGenerator; +import com.example.ead_backend.model.enums.ProjectStatus; + +@Entity +@Table(name = "Projects") +@Data +public class Project { + @Id + @UuidGenerator + @Column(columnDefinition = "VARCHAR(255)") + private String projectId; + + @Column(nullable = false) + private String name; + + @Column(nullable = true, length = 2000) + private String description; + + @Column(nullable = true) + private String customerId; + + @Column(nullable = false) + private LocalDate startDate; + + @Column(nullable = true) + private LocalDate endDate; + + @ManyToOne + @JoinColumn(name = "employee_id") + private Employee employee; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ProjectStatus status; +} diff --git a/src/main/java/com/example/ead_backend/model/entity/Service.java b/src/main/java/com/example/ead_backend/model/entity/Service.java deleted file mode 100644 index 39ea042..0000000 --- a/src/main/java/com/example/ead_backend/model/entity/Service.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.model.entity; - -public class Service { - -} diff --git a/src/main/java/com/example/ead_backend/model/entity/ServiceCategory.java b/src/main/java/com/example/ead_backend/model/entity/ServiceCategory.java deleted file mode 100644 index 8ad63a6..0000000 --- a/src/main/java/com/example/ead_backend/model/entity/ServiceCategory.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.model.entity; - -public class ServiceCategory { - -} diff --git a/src/main/java/com/example/ead_backend/model/entity/Task.java b/src/main/java/com/example/ead_backend/model/entity/Task.java new file mode 100644 index 0000000..af7223e --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/entity/Task.java @@ -0,0 +1,12 @@ +package com.example.ead_backend.model.entity; + +import jakarta.persistence.*; +import lombok.Data; + +@Entity +@Table(name = "Tasks") +@Data +public class Task { + @Id + private String taskId; +} diff --git a/src/main/java/com/example/ead_backend/model/entity/TimeLog.java b/src/main/java/com/example/ead_backend/model/entity/TimeLog.java index 7f62238..8fca6dd 100644 --- a/src/main/java/com/example/ead_backend/model/entity/TimeLog.java +++ b/src/main/java/com/example/ead_backend/model/entity/TimeLog.java @@ -1,5 +1,28 @@ package com.example.ead_backend.model.entity; +import jakarta.persistence.*; +import lombok.Data; +import org.hibernate.annotations.UuidGenerator; +import java.time.LocalDate; + +@Entity +@Table(name = "TimeLogs") +@Data public class TimeLog { + @Id + @UuidGenerator + @Column(columnDefinition = "VARCHAR(255)") + private String timeLogId; + + @Column(nullable = false) + private LocalDate date; + + @Column(name = "start_time", nullable = false) + private String startTime; // HH:mm + + @Column(name = "end_time", nullable = false) + private String endTime; // HH:mm + @Column(nullable = false) + private String type; // e.g., BLOCKED, MAINTENANCE } diff --git a/src/main/java/com/example/ead_backend/model/entity/User.java b/src/main/java/com/example/ead_backend/model/entity/User.java index d4347de..4e093ba 100644 --- a/src/main/java/com/example/ead_backend/model/entity/User.java +++ b/src/main/java/com/example/ead_backend/model/entity/User.java @@ -1,5 +1,95 @@ package com.example.ead_backend.model.entity; -public class User { +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import java.util.Collection; +import java.util.List; + +@Entity +@Table(name = "users") +public class User implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String firstName; + + @Column(nullable = false) + private String lastName; + + @Column(nullable = false) + private String password; + + @Column(unique = true, nullable = false) + private String email; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Customer customer; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Employee employee; + + public User() {} + + public User(String firstName, String lastName, String password, String email) { + this.firstName = firstName; + this.lastName = lastName; + this.password = password; + this.email = email; + } + + @Override + public Collection getAuthorities() { + // Determine role based on whether customer or employee exists + if (employee != null) { + return List.of(new SimpleGrantedAuthority("ROLE_" + employee.getRole().name())); + } else if (customer != null) { + return List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER")); + } + return List.of(); + } + + @Override + public String getUsername() { + return email; // Use email as username + } + + @Override + public boolean isAccountNonExpired() { return true; } + + @Override + public boolean isAccountNonLocked() { return true; } + + @Override + public boolean isCredentialsNonExpired() { return true; } + + @Override + public boolean isEnabled() { return true; } + + // Getters and setters + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + + public Customer getCustomer() { return customer; } + public void setCustomer(Customer customer) { this.customer = customer; } + + public Employee getEmployee() { return employee; } + public void setEmployee(Employee employee) { this.employee = employee; } } diff --git a/src/main/java/com/example/ead_backend/model/entity/Vehicle.java b/src/main/java/com/example/ead_backend/model/entity/Vehicle.java index 0c40759..b6c0923 100644 --- a/src/main/java/com/example/ead_backend/model/entity/Vehicle.java +++ b/src/main/java/com/example/ead_backend/model/entity/Vehicle.java @@ -1,5 +1,45 @@ package com.example.ead_backend.model.entity; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; + +@Entity +@Table(name = "vehicles") +@Data // Lombok: generates getters, setters, toString, equals, and hashCode +@NoArgsConstructor // Lombok: generates default constructor +@AllArgsConstructor // Lombok: generates constructor with all fields +@Builder // Lombok: allows builder pattern for object creation public class Vehicle { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customer_id", referencedColumnName = "id", nullable = false) + private Customer customer; + + @Column(nullable = false) + private String model; + + @Column(nullable = false) + private String color; + + private String vin; + + private LocalDate registrationDate; + + @Column(nullable = false) + private String licensePlate; + + @Column(nullable = false) + private int year; + + @Column(length = 500) + private String imageUrl; + + @Column(length = 255) + private String imagePublicId; } diff --git a/src/main/java/com/example/ead_backend/model/enums/AppointmentStatus.java b/src/main/java/com/example/ead_backend/model/enums/AppointmentStatus.java index c2dce53..081f44a 100644 --- a/src/main/java/com/example/ead_backend/model/enums/AppointmentStatus.java +++ b/src/main/java/com/example/ead_backend/model/enums/AppointmentStatus.java @@ -1,5 +1,7 @@ package com.example.ead_backend.model.enums; -public class AppointmentStatus { - +public enum AppointmentStatus { + UPCOMING, + COMPLETED, + CANCELLED } diff --git a/src/main/java/com/example/ead_backend/model/enums/NotificationType.java b/src/main/java/com/example/ead_backend/model/enums/NotificationType.java index b9b5a30..7e62695 100644 --- a/src/main/java/com/example/ead_backend/model/enums/NotificationType.java +++ b/src/main/java/com/example/ead_backend/model/enums/NotificationType.java @@ -1,5 +1,21 @@ package com.example.ead_backend.model.enums; -public class NotificationType { +/** + * Enum representing different types of notifications in the system. + */ +public enum NotificationType { + /** + * Notification for progress updates on appointments + */ + PROGRESS_UPDATE, + /** + * Notification for status changes in appointments or tasks + */ + STATUS_CHANGE, + + /** + * General notifications + */ + GENERAL } diff --git a/src/main/java/com/example/ead_backend/model/enums/ProjectStatus.java b/src/main/java/com/example/ead_backend/model/enums/ProjectStatus.java new file mode 100644 index 0000000..ce25a8c --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/enums/ProjectStatus.java @@ -0,0 +1,9 @@ +package com.example.ead_backend.model.enums; + +public enum ProjectStatus { + PLANNED, + IN_PROGRESS, + COMPLETED, + CANCELLED, + ON_HOLD +} diff --git a/src/main/java/com/example/ead_backend/model/enums/Role.java b/src/main/java/com/example/ead_backend/model/enums/Role.java index aceb68a..ce620c3 100644 --- a/src/main/java/com/example/ead_backend/model/enums/Role.java +++ b/src/main/java/com/example/ead_backend/model/enums/Role.java @@ -1,5 +1,7 @@ package com.example.ead_backend.model.enums; -public class Role { - +public enum Role { + CUSTOMER, + EMPLOYEE, + ADMIN } diff --git a/src/main/java/com/example/ead_backend/model/enums/ServiceStatus.java b/src/main/java/com/example/ead_backend/model/enums/ServiceStatus.java deleted file mode 100644 index 9593e8d..0000000 --- a/src/main/java/com/example/ead_backend/model/enums/ServiceStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.model.enums; - -public class ServiceStatus { - -} diff --git a/src/main/java/com/example/ead_backend/model/enums/TaskStatus.java b/src/main/java/com/example/ead_backend/model/enums/TaskStatus.java new file mode 100644 index 0000000..9ee4e05 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/enums/TaskStatus.java @@ -0,0 +1,8 @@ +package com.example.ead_backend.model.enums; + +public enum TaskStatus { + // TODO, + // IN_PROGRESS, + // COMPLETED, + // CANCELLED +} diff --git a/src/main/java/com/example/ead_backend/model/enums/VehicleStatus.java b/src/main/java/com/example/ead_backend/model/enums/VehicleStatus.java new file mode 100644 index 0000000..258fef2 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/enums/VehicleStatus.java @@ -0,0 +1,8 @@ +package com.example.ead_backend.model.enums; + +public enum VehicleStatus { + // ACTIVE, + // IN_SERVICE, + // COMPLETED, + // INACTIVE +} diff --git a/src/main/java/com/example/ead_backend/model/message/NotificationMessage.java b/src/main/java/com/example/ead_backend/model/message/NotificationMessage.java new file mode 100644 index 0000000..13d7098 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/message/NotificationMessage.java @@ -0,0 +1,24 @@ +package com.example.ead_backend.model.message; + +import com.example.ead_backend.model.enums.NotificationType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * WebSocket message for general notifications. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotificationMessage { + + private Long userId; + private NotificationType type; + private String message; + private Timestamp timestamp; +} diff --git a/src/main/java/com/example/ead_backend/model/message/ProgressUpdateMessage.java b/src/main/java/com/example/ead_backend/model/message/ProgressUpdateMessage.java new file mode 100644 index 0000000..58943b0 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/message/ProgressUpdateMessage.java @@ -0,0 +1,25 @@ +package com.example.ead_backend.model.message; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * WebSocket message for progress updates. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProgressUpdateMessage { + + private Long appointmentId; + private String stage; + private Integer percentage; + private String remarks; + private Long updatedBy; + private Timestamp timestamp; +} diff --git a/src/main/java/com/example/ead_backend/model/message/StatusChangeMessage.java b/src/main/java/com/example/ead_backend/model/message/StatusChangeMessage.java new file mode 100644 index 0000000..5e49381 --- /dev/null +++ b/src/main/java/com/example/ead_backend/model/message/StatusChangeMessage.java @@ -0,0 +1,24 @@ +package com.example.ead_backend.model.message; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +/** + * WebSocket message for status changes. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StatusChangeMessage { + + private Long appointmentId; + private String oldStatus; + private String newStatus; + private String changedBy; + private Timestamp timestamp; +} diff --git a/src/main/java/com/example/ead_backend/repository/ServiceRepository.java b/src/main/java/com/example/ead_backend/repository/AdminRepository.java similarity index 54% rename from src/main/java/com/example/ead_backend/repository/ServiceRepository.java rename to src/main/java/com/example/ead_backend/repository/AdminRepository.java index 7720e12..075e2b0 100644 --- a/src/main/java/com/example/ead_backend/repository/ServiceRepository.java +++ b/src/main/java/com/example/ead_backend/repository/AdminRepository.java @@ -1,5 +1,5 @@ package com.example.ead_backend.repository; -public class ServiceRepository { - +public interface AdminRepository { + } diff --git a/src/main/java/com/example/ead_backend/repository/AppointmentRepository.java b/src/main/java/com/example/ead_backend/repository/AppointmentRepository.java index 315c4ce..6584c9a 100644 --- a/src/main/java/com/example/ead_backend/repository/AppointmentRepository.java +++ b/src/main/java/com/example/ead_backend/repository/AppointmentRepository.java @@ -1,5 +1,11 @@ package com.example.ead_backend.repository; -public class AppointmentRepository { +import com.example.ead_backend.model.entity.Appointment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; +@Repository +public interface AppointmentRepository extends JpaRepository { + List findByCustomerId(String customerId); } diff --git a/src/main/java/com/example/ead_backend/repository/CustomerRepository.java b/src/main/java/com/example/ead_backend/repository/CustomerRepository.java new file mode 100644 index 0000000..7611a59 --- /dev/null +++ b/src/main/java/com/example/ead_backend/repository/CustomerRepository.java @@ -0,0 +1,14 @@ +package com.example.ead_backend.repository; + +import com.example.ead_backend.model.entity.Customer; +import com.example.ead_backend.model.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CustomerRepository extends JpaRepository { + Optional findByUser(User user); + Optional findByUserId(Long userId); +} diff --git a/src/main/java/com/example/ead_backend/repository/EmployeeRepository.java b/src/main/java/com/example/ead_backend/repository/EmployeeRepository.java new file mode 100644 index 0000000..41089e1 --- /dev/null +++ b/src/main/java/com/example/ead_backend/repository/EmployeeRepository.java @@ -0,0 +1,14 @@ +package com.example.ead_backend.repository; + +import com.example.ead_backend.model.entity.Employee; +import com.example.ead_backend.model.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface EmployeeRepository extends JpaRepository { + Optional findByUser(User user); + Optional findByUserId(Long userId); +} diff --git a/src/main/java/com/example/ead_backend/repository/NotificationRepository.java b/src/main/java/com/example/ead_backend/repository/NotificationRepository.java index 4d90188..881591e 100644 --- a/src/main/java/com/example/ead_backend/repository/NotificationRepository.java +++ b/src/main/java/com/example/ead_backend/repository/NotificationRepository.java @@ -1,5 +1,23 @@ package com.example.ead_backend.repository; -public class NotificationRepository { +import com.example.ead_backend.model.entity.Notification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +/** + * Repository interface for Notification entity operations. + */ +@Repository +public interface NotificationRepository extends JpaRepository { + + /** + * Find all notifications for a specific user ordered by creation date + * descending. + * + * @param userId the user ID + * @return list of notifications + */ + List findByUserIdOrderByCreatedAtDesc(Long userId); } diff --git a/src/main/java/com/example/ead_backend/repository/OTPRepository.java b/src/main/java/com/example/ead_backend/repository/OTPRepository.java new file mode 100644 index 0000000..842624e --- /dev/null +++ b/src/main/java/com/example/ead_backend/repository/OTPRepository.java @@ -0,0 +1,18 @@ +package com.example.ead_backend.repository; + +import com.example.ead_backend.model.entity.OTP; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Repository +public interface OTPRepository extends JpaRepository { + Optional findByEmailAndOtp(String email, String otp); + Optional findByEmail(String email); + + @Transactional + void deleteByEmail(String email); +} + diff --git a/src/main/java/com/example/ead_backend/repository/ProgressUpdateRepository.java b/src/main/java/com/example/ead_backend/repository/ProgressUpdateRepository.java new file mode 100644 index 0000000..ce2a1ac --- /dev/null +++ b/src/main/java/com/example/ead_backend/repository/ProgressUpdateRepository.java @@ -0,0 +1,23 @@ +package com.example.ead_backend.repository; + +import com.example.ead_backend.model.entity.ProgressUpdate; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * Repository interface for ProgressUpdate entity operations. + */ +@Repository +public interface ProgressUpdateRepository extends JpaRepository { + + /** + * Find all progress updates for a specific appointment ordered by creation + * date. + * + * @param appointmentId the appointment ID + * @return list of progress updates + */ + List findByAppointmentIdOrderByCreatedAtAsc(Long appointmentId); +} diff --git a/src/main/java/com/example/ead_backend/repository/ProjectRepository.java b/src/main/java/com/example/ead_backend/repository/ProjectRepository.java new file mode 100644 index 0000000..654de4d --- /dev/null +++ b/src/main/java/com/example/ead_backend/repository/ProjectRepository.java @@ -0,0 +1,11 @@ +package com.example.ead_backend.repository; + +import com.example.ead_backend.model.entity.Project; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface ProjectRepository extends JpaRepository { + List findByCustomerId(String customerId); +} diff --git a/src/main/java/com/example/ead_backend/repository/ServiceCategoryRepository.java b/src/main/java/com/example/ead_backend/repository/ServiceCategoryRepository.java deleted file mode 100644 index 5018ab9..0000000 --- a/src/main/java/com/example/ead_backend/repository/ServiceCategoryRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.repository; - -public class ServiceCategoryRepository { - -} diff --git a/src/main/java/com/example/ead_backend/repository/TaskRepository.java b/src/main/java/com/example/ead_backend/repository/TaskRepository.java new file mode 100644 index 0000000..5cea183 --- /dev/null +++ b/src/main/java/com/example/ead_backend/repository/TaskRepository.java @@ -0,0 +1,10 @@ +package com.example.ead_backend.repository; + +import com.example.ead_backend.model.entity.Task; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TaskRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/ead_backend/repository/TimeLogRepository.java b/src/main/java/com/example/ead_backend/repository/TimeLogRepository.java index a37256e..fe68562 100644 --- a/src/main/java/com/example/ead_backend/repository/TimeLogRepository.java +++ b/src/main/java/com/example/ead_backend/repository/TimeLogRepository.java @@ -1,5 +1,15 @@ package com.example.ead_backend.repository; -public class TimeLogRepository { +import com.example.ead_backend.model.entity.TimeLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.time.LocalDate; +import java.util.List; + +@Repository +public interface TimeLogRepository extends JpaRepository { + List findByDate(LocalDate date); + + List findByDateAndType(LocalDate date, String type); } diff --git a/src/main/java/com/example/ead_backend/repository/UserRepo.java b/src/main/java/com/example/ead_backend/repository/UserRepo.java new file mode 100644 index 0000000..173cd17 --- /dev/null +++ b/src/main/java/com/example/ead_backend/repository/UserRepo.java @@ -0,0 +1,13 @@ +package com.example.ead_backend.repository; + +import com.example.ead_backend.model.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepo extends JpaRepository { + Optional findByEmail(String email); + boolean existsByEmail(String email); +} diff --git a/src/main/java/com/example/ead_backend/repository/UserRepository.java b/src/main/java/com/example/ead_backend/repository/UserRepository.java deleted file mode 100644 index 3e2a049..0000000 --- a/src/main/java/com/example/ead_backend/repository/UserRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.repository; - -public class UserRepository { - -} diff --git a/src/main/java/com/example/ead_backend/repository/VehicleRepository.java b/src/main/java/com/example/ead_backend/repository/VehicleRepository.java index 1f22c12..7b98bb2 100644 --- a/src/main/java/com/example/ead_backend/repository/VehicleRepository.java +++ b/src/main/java/com/example/ead_backend/repository/VehicleRepository.java @@ -1,5 +1,13 @@ package com.example.ead_backend.repository; -public class VehicleRepository { +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import com.example.ead_backend.model.entity.Vehicle; + +import java.util.List; + +@Repository +public interface VehicleRepository extends JpaRepository { + List findByCustomerId(Long customerId); } diff --git a/src/main/java/com/example/ead_backend/security/JwtTokenProvider.java b/src/main/java/com/example/ead_backend/security/JwtTokenProvider.java deleted file mode 100644 index ea83d30..0000000 --- a/src/main/java/com/example/ead_backend/security/JwtTokenProvider.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.security; - -public class JwtTokenProvider { - -} diff --git a/src/main/java/com/example/ead_backend/service/AdminService.java b/src/main/java/com/example/ead_backend/service/AdminService.java new file mode 100644 index 0000000..71a080c --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/AdminService.java @@ -0,0 +1,8 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.dto.CreateEmployeeRequest; +import com.example.ead_backend.dto.EmployeeCreateDTO; + +public interface AdminService { + EmployeeCreateDTO createEmployee(CreateEmployeeRequest request); +} diff --git a/src/main/java/com/example/ead_backend/service/AppointmentService.java b/src/main/java/com/example/ead_backend/service/AppointmentService.java new file mode 100644 index 0000000..9d483fe --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/AppointmentService.java @@ -0,0 +1,19 @@ +package com.example.ead_backend.service; + +import java.util.List; + +import com.example.ead_backend.dto.AppointmentDTO; + +public interface AppointmentService { + AppointmentDTO createAppointment(AppointmentDTO dto); + + AppointmentDTO getAppointmentById(String id); + + List getAllAppointments(); + + List getAppointmentsByCustomerId(String customerId); + + AppointmentDTO updateAppointment(String id, AppointmentDTO dto); + + void deleteAppointment(String id); +} diff --git a/src/main/java/com/example/ead_backend/service/CloudinaryService.java b/src/main/java/com/example/ead_backend/service/CloudinaryService.java new file mode 100644 index 0000000..70c6d2d --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/CloudinaryService.java @@ -0,0 +1,94 @@ +package com.example.ead_backend.service; + +import com.cloudinary.Cloudinary; +import com.cloudinary.utils.ObjectUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CloudinaryService { + + private final Cloudinary cloudinary; + + /** + * Upload an image to Cloudinary + * + * @param file the image file to upload + * @return Map containing the upload result with url and public_id + * @throws IOException if upload fails + */ + public Map uploadImage(MultipartFile file) throws IOException { + try { + log.info("Uploading image to Cloudinary: {}", file.getOriginalFilename()); + + // Generate a unique public ID for the image + String publicId = "vehicles/" + UUID.randomUUID().toString(); + + Map uploadResult = cloudinary.uploader().upload( + file.getBytes(), + ObjectUtils.asMap( + "public_id", publicId, + "folder", "ead-automobile/vehicles", + "resource_type", "image", + "transformation", ObjectUtils.asMap( + "width", 800, + "height", 600, + "crop", "limit", + "quality", "auto" + ) + ) + ); + + log.info("Image uploaded successfully. Public ID: {}", uploadResult.get("public_id")); + return uploadResult; + } catch (IOException e) { + log.error("Failed to upload image to Cloudinary: {}", e.getMessage(), e); + throw new IOException("Failed to upload image: " + e.getMessage()); + } + } + + /** + * Delete an image from Cloudinary + * + * @param publicId the public ID of the image to delete + * @throws IOException if deletion fails + */ + public void deleteImage(String publicId) throws IOException { + try { + if (publicId != null && !publicId.isEmpty()) { + log.info("Deleting image from Cloudinary: {}", publicId); + Map result = cloudinary.uploader().destroy(publicId, ObjectUtils.emptyMap()); + log.info("Image deleted successfully. Result: {}", result.get("result")); + } + } catch (IOException e) { + log.error("Failed to delete image from Cloudinary: {}", e.getMessage(), e); + throw new IOException("Failed to delete image: " + e.getMessage()); + } + } + + /** + * Update an image (delete old one and upload new one) + * + * @param file the new image file + * @param oldPublicId the public ID of the old image to delete + * @return Map containing the upload result + * @throws IOException if operation fails + */ + public Map updateImage(MultipartFile file, String oldPublicId) throws IOException { + // Delete old image if exists + if (oldPublicId != null && !oldPublicId.isEmpty()) { + deleteImage(oldPublicId); + } + + // Upload new image + return uploadImage(file); + } +} diff --git a/src/main/java/com/example/ead_backend/service/CustomerProfileService.java b/src/main/java/com/example/ead_backend/service/CustomerProfileService.java new file mode 100644 index 0000000..cae2517 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/CustomerProfileService.java @@ -0,0 +1,36 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.dto.CustomerProfileDTO; +import com.example.ead_backend.dto.UpdateCustomerProfileRequest; + +public interface CustomerProfileService { + + /** + * Get customer profile by user ID + * @param userId the user ID + * @return CustomerProfileDTO + */ + CustomerProfileDTO getCustomerProfileByUserId(Long userId); + + /** + * Get customer profile by customer ID + * @param customerId the customer ID + * @return CustomerProfileDTO + */ + CustomerProfileDTO getCustomerProfileByCustomerId(Long customerId); + + /** + * Get customer profile by email + * @param email the user email + * @return CustomerProfileDTO + */ + CustomerProfileDTO getCustomerProfileByEmail(String email); + + /** + * Update customer profile + * @param userId the user ID + * @param request the update request + * @return updated CustomerProfileDTO + */ + CustomerProfileDTO updateCustomerProfile(Long userId, UpdateCustomerProfileRequest request); +} diff --git a/src/main/java/com/example/ead_backend/service/CustomerService.java b/src/main/java/com/example/ead_backend/service/CustomerService.java new file mode 100644 index 0000000..1f8d1d4 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/CustomerService.java @@ -0,0 +1,9 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.model.entity.Customer; +import com.example.ead_backend.model.entity.User; + +public interface CustomerService { + Customer createCustomer(User user, String phoneNumber); + Customer findByUserId(Long userId); +} diff --git a/src/main/java/com/example/ead_backend/service/EmailService.java b/src/main/java/com/example/ead_backend/service/EmailService.java new file mode 100644 index 0000000..425e061 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/EmailService.java @@ -0,0 +1,103 @@ +package com.example.ead_backend.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +/** + * Service for sending email notifications. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class EmailService { + + private final JavaMailSender mailSender; + + @Value("${spring.mail.from:noreply@automobile.com}") + private String fromAddress; + + /** + * Send a progress update email to a user. + * + * @param toEmail the recipient email address + * @param subject the email subject + * @param body the email body + */ + public void sendProgressEmail(String toEmail, String subject, String body) { + try { + log.info("Sending progress email to {}", toEmail); + + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromAddress); + message.setTo(toEmail); + message.setSubject(subject); + message.setText(body); + + mailSender.send(message); + log.info("Email sent successfully to {}", toEmail); + } catch (Exception e) { + log.error("Failed to send email to {}: {}", toEmail, e.getMessage(), e); + // Don't throw exception - email failure shouldn't break the flow + } + } + + /** + * Send a progress update email using appointment details. + * + * @param toEmail the recipient email + * @param appointmentId the appointment ID + * @param stage the current stage + * @param percentage the progress percentage + * @param remarks additional remarks + */ + public void sendProgressUpdateNotification(String toEmail, Long appointmentId, String stage, Integer percentage, + String remarks) { + String subject = "Progress Update - Appointment #" + appointmentId; + String body = String.format( + "Dear Customer,\n\n" + + "Your appointment #%d has been updated:\n\n" + + "Stage: %s\n" + + "Progress: %d%%\n" + + "Remarks: %s\n\n" + + "Thank you for choosing our service.\n\n" + + "Best regards,\n" + + "Automobile Service Team", + appointmentId, stage, percentage, remarks != null ? remarks : "N/A"); + + sendProgressEmail(toEmail, subject, body); + } + + /** + * Send OTP email for password reset. + * + * @param to the recipient email address + * @param otp the OTP code + */ + public void sendOTP(String to, String otp) { + try { + log.info("Sending OTP email to {}", to); + + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromAddress); + message.setTo(to); + message.setSubject("Password Reset OTP - Auto Mobile"); + message.setText("Dear User,\n\n" + + "You have requested to reset your password.\n\n" + + "Your OTP for password reset is: " + otp + "\n\n" + + "This OTP is valid for 10 minutes.\n\n" + + "If you did not request this, please ignore this email.\n\n" + + "Best regards,\n" + + "Auto Mobile Team"); + + mailSender.send(message); + log.info("OTP email sent successfully to {}", to); + } catch (Exception e) { + log.error("Failed to send OTP email to {}: {}", to, e.getMessage(), e); + throw new RuntimeException("Failed to send email: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/example/ead_backend/service/EmployeeService.java b/src/main/java/com/example/ead_backend/service/EmployeeService.java new file mode 100644 index 0000000..ec48467 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/EmployeeService.java @@ -0,0 +1,12 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.model.entity.Employee; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.model.enums.Role; + +import java.time.LocalDate; + +public interface EmployeeService { + Employee createEmployee(User user, Role role, LocalDate joinedDate); + Employee findByUserId(Long userId); +} diff --git a/src/main/java/com/example/ead_backend/service/NotificationService.java b/src/main/java/com/example/ead_backend/service/NotificationService.java new file mode 100644 index 0000000..b1745d5 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/NotificationService.java @@ -0,0 +1,37 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.dto.NotificationDTO; +import com.example.ead_backend.model.enums.NotificationType; + +import java.util.List; + +/** + * Service interface for managing notifications. + */ +public interface NotificationService { + + /** + * Create and save a notification for a user. + * + * @param userId the user ID + * @param type the notification type + * @param message the notification message + * @return the created notification DTO + */ + NotificationDTO createNotification(Long userId, NotificationType type, String message); + + /** + * Get all notifications for a user. + * + * @param userId the user ID + * @return list of notification DTOs + */ + List getNotificationsForUser(Long userId); + + /** + * Mark a notification as read. + * + * @param notificationId the notification ID + */ + void markAsRead(Long notificationId); +} diff --git a/src/main/java/com/example/ead_backend/service/ProgressCalculationService.java b/src/main/java/com/example/ead_backend/service/ProgressCalculationService.java new file mode 100644 index 0000000..565ea6b --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/ProgressCalculationService.java @@ -0,0 +1,61 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.model.entity.ProgressUpdate; +import com.example.ead_backend.repository.ProgressUpdateRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * Service for calculating progress percentages based on recorded updates. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class ProgressCalculationService { + + private final ProgressUpdateRepository progressUpdateRepository; + + /** + * Calculate the average progress percentage for an appointment. + * Uses the average of all recorded percentage entries. + * + * @param appointmentId the appointment ID + * @return the calculated average percentage + */ + public int calculateAverageProgress(Long appointmentId) { + List updates = progressUpdateRepository.findByAppointmentIdOrderByCreatedAtAsc(appointmentId); + + if (updates.isEmpty()) { + log.debug("No progress updates found for appointment {}", appointmentId); + return 0; + } + + int sum = updates.stream() + .mapToInt(ProgressUpdate::getPercentage) + .sum(); + + int average = sum / updates.size(); + log.debug("Calculated average progress for appointment {}: {}%", appointmentId, average); + + return average; + } + + /** + * Get the latest progress percentage for an appointment. + * + * @param appointmentId the appointment ID + * @return the latest percentage or 0 if no updates exist + */ + public int getLatestProgress(Long appointmentId) { + List updates = progressUpdateRepository.findByAppointmentIdOrderByCreatedAtAsc(appointmentId); + + if (updates.isEmpty()) { + return 0; + } + + return updates.get(updates.size() - 1).getPercentage(); + } +} diff --git a/src/main/java/com/example/ead_backend/service/ProgressService.java b/src/main/java/com/example/ead_backend/service/ProgressService.java new file mode 100644 index 0000000..40b6053 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/ProgressService.java @@ -0,0 +1,39 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.dto.ProgressUpdateRequest; + +import java.util.List; + +/** + * Service interface for managing progress updates on appointments. + */ +public interface ProgressService { + + /** + * Create or update progress for an appointment. + * + * @param appointmentId the appointment ID + * @param request the progress update request + * @param updatedBy the ID of the user making the update + * @return the created/updated progress response + */ + ProgressResponse createOrUpdateProgress(Long appointmentId, ProgressUpdateRequest request, Long updatedBy); + + /** + * Get all progress updates for an appointment. + * + * @param appointmentId the appointment ID + * @return list of progress responses + */ + List getProgressForAppointment(Long appointmentId); + + /** + * Calculate the overall progress percentage for an appointment. + * Uses ProgressCalculationService for computation. + * + * @param appointmentId the appointment ID + * @return the calculated progress percentage + */ + int calculateProgressPercentage(Long appointmentId); +} diff --git a/src/main/java/com/example/ead_backend/service/ProjectService.java b/src/main/java/com/example/ead_backend/service/ProjectService.java new file mode 100644 index 0000000..2626772 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/ProjectService.java @@ -0,0 +1,19 @@ +package com.example.ead_backend.service; + +import java.util.List; + +import com.example.ead_backend.dto.ProjectDTO; + +public interface ProjectService { + ProjectDTO createProject(ProjectDTO dto); + + ProjectDTO getProjectById(String id); + + List getAllProjects(); + + List getProjectsByCustomerId(String customerId); + + ProjectDTO updateProject(String id, ProjectDTO dto); + + void deleteProject(String id); +} diff --git a/src/main/java/com/example/ead_backend/service/TaskService.java b/src/main/java/com/example/ead_backend/service/TaskService.java new file mode 100644 index 0000000..255dba4 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/TaskService.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.service; + +public interface TaskService { + +} diff --git a/src/main/java/com/example/ead_backend/service/TimeLogService.java b/src/main/java/com/example/ead_backend/service/TimeLogService.java new file mode 100644 index 0000000..3e2affd --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/TimeLogService.java @@ -0,0 +1,5 @@ +package com.example.ead_backend.service; + +public interface TimeLogService { + +} diff --git a/src/main/java/com/example/ead_backend/service/VehicleService.java b/src/main/java/com/example/ead_backend/service/VehicleService.java new file mode 100644 index 0000000..5f57651 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/VehicleService.java @@ -0,0 +1,26 @@ +package com.example.ead_backend.service; + +import java.io.IOException; +import java.util.List; + +import org.springframework.web.multipart.MultipartFile; + +import com.example.ead_backend.dto.VehicleDTO; + +public interface VehicleService { + VehicleDTO createVehicle(VehicleDTO vehicleDTO); + + VehicleDTO createVehicleWithImage(VehicleDTO vehicleDTO, MultipartFile image) throws IOException; + + VehicleDTO getVehicleById(Long id); + + List getAllVehicles(); + + List getVehiclesByCustomerId(Long customerId); + + VehicleDTO updateVehicle(Long id, VehicleDTO vehicleDTO); + + VehicleDTO updateVehicleWithImage(Long id, VehicleDTO vehicleDTO, MultipartFile image) throws IOException; + + void deleteVehicle(Long id); +} diff --git a/src/main/java/com/example/ead_backend/service/WebSocketNotificationService.java b/src/main/java/com/example/ead_backend/service/WebSocketNotificationService.java new file mode 100644 index 0000000..e7f90b6 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/WebSocketNotificationService.java @@ -0,0 +1,70 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.model.entity.Notification; +import com.example.ead_backend.model.message.NotificationMessage; +import com.example.ead_backend.model.message.ProgressUpdateMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +import java.sql.Timestamp; +import java.time.Instant; + +/** + * Service for broadcasting notifications via WebSocket. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class WebSocketNotificationService { + + private final SimpMessagingTemplate messagingTemplate; + + /** + * Broadcast a progress update to all subscribers of a specific appointment. + * + * @param appointmentId the appointment ID + * @param response the progress response + */ + public void broadcastProgressUpdate(Long appointmentId, ProgressResponse response) { + log.info("Broadcasting progress update for appointment {}", appointmentId); + + ProgressUpdateMessage message = ProgressUpdateMessage.builder() + .appointmentId(appointmentId) + .stage(response.getStage()) + .percentage(response.getPercentage()) + .remarks(response.getRemarks()) + .updatedBy(response.getUpdatedBy()) + .timestamp(Timestamp.from(Instant.now())) + .build(); + + String destination = "/topic/progress." + appointmentId; + messagingTemplate.convertAndSend(destination, message); + + log.debug("Progress update broadcasted to {}", destination); + } + + /** + * Send a notification to a specific user. + * + * @param userId the user ID + * @param notification the notification entity + */ + public void notifyUser(Long userId, Notification notification) { + log.info("Sending notification to user {}", userId); + + NotificationMessage message = NotificationMessage.builder() + .userId(userId) + .type(notification.getType()) + .message(notification.getMessage()) + .timestamp(notification.getCreatedAt()) + .build(); + + String destination = "/topic/notifications." + userId; + messagingTemplate.convertAndSend(destination, message); + + log.debug("Notification sent to {}", destination); + } +} diff --git a/src/main/java/com/example/ead_backend/service/appointment/AppointmentService.java b/src/main/java/com/example/ead_backend/service/appointment/AppointmentService.java deleted file mode 100644 index 7516103..0000000 --- a/src/main/java/com/example/ead_backend/service/appointment/AppointmentService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.appointment; - -public class AppointmentService { - -} diff --git a/src/main/java/com/example/ead_backend/service/appointment/AppointmentServiceImpl.java b/src/main/java/com/example/ead_backend/service/appointment/AppointmentServiceImpl.java deleted file mode 100644 index 34cbcd9..0000000 --- a/src/main/java/com/example/ead_backend/service/appointment/AppointmentServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.appointment; - -public class AppointmentServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/auth/AuthService.java b/src/main/java/com/example/ead_backend/service/auth/AuthService.java deleted file mode 100644 index 5b9b3df..0000000 --- a/src/main/java/com/example/ead_backend/service/auth/AuthService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.auth; - -public class AuthService { - -} diff --git a/src/main/java/com/example/ead_backend/service/auth/AuthServiceImpl.java b/src/main/java/com/example/ead_backend/service/auth/AuthServiceImpl.java deleted file mode 100644 index bf21562..0000000 --- a/src/main/java/com/example/ead_backend/service/auth/AuthServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.auth; - -public class AuthServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/auth/TokenService.java b/src/main/java/com/example/ead_backend/service/auth/TokenService.java deleted file mode 100644 index a8bc72e..0000000 --- a/src/main/java/com/example/ead_backend/service/auth/TokenService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.auth; - -public class TokenService { - -} diff --git a/src/main/java/com/example/ead_backend/service/auth/UserDetailsServiceImpl.java b/src/main/java/com/example/ead_backend/service/auth/UserDetailsServiceImpl.java deleted file mode 100644 index 7b73022..0000000 --- a/src/main/java/com/example/ead_backend/service/auth/UserDetailsServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.auth; - -public class UserDetailsServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/dashboard/AdminDashboardService.java b/src/main/java/com/example/ead_backend/service/dashboard/AdminDashboardService.java deleted file mode 100644 index bd29de6..0000000 --- a/src/main/java/com/example/ead_backend/service/dashboard/AdminDashboardService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.dashboard; - -public class AdminDashboardService { - -} diff --git a/src/main/java/com/example/ead_backend/service/dashboard/CustomerDashboardService.java b/src/main/java/com/example/ead_backend/service/dashboard/CustomerDashboardService.java deleted file mode 100644 index 5d2efb1..0000000 --- a/src/main/java/com/example/ead_backend/service/dashboard/CustomerDashboardService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.dashboard; - -public class CustomerDashboardService { - -} diff --git a/src/main/java/com/example/ead_backend/service/dashboard/EmployeeDashboardService.java b/src/main/java/com/example/ead_backend/service/dashboard/EmployeeDashboardService.java deleted file mode 100644 index 4ed72d8..0000000 --- a/src/main/java/com/example/ead_backend/service/dashboard/EmployeeDashboardService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.dashboard; - -public class EmployeeDashboardService { - -} diff --git a/src/main/java/com/example/ead_backend/service/impl/AdminServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/AdminServiceImpl.java new file mode 100644 index 0000000..d08ff45 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/AdminServiceImpl.java @@ -0,0 +1,56 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.dto.CreateEmployeeRequest; +import com.example.ead_backend.dto.EmployeeCreateDTO; +import com.example.ead_backend.model.entity.Employee; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.model.enums.Role; +import com.example.ead_backend.service.AdminService; +import com.example.ead_backend.service.EmployeeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@Service +public class AdminServiceImpl implements AdminService { + + @Autowired + private UserService userService; + + @Autowired + private EmployeeService employeeService; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + @Transactional + public EmployeeCreateDTO createEmployee(CreateEmployeeRequest request) { + // Hash the password before creating user + String encodedPassword = passwordEncoder.encode(request.getPassword()); + + // Create user first + User user = userService.createUser( + request.getFirstName(), + request.getLastName(), + encodedPassword, + request.getEmail() + ); + + // Create employee record with EMPLOYEE role and today's date as joined date + Employee employee = employeeService.createEmployee(user, Role.EMPLOYEE, LocalDate.now()); + + // Convert to DTO + return new EmployeeCreateDTO( + employee.getId(), + user.getFirstName(), + user.getLastName(), + user.getEmail(), + employee.getRole().name(), + employee.getJoinedDate() + ); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/AppointmentServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/AppointmentServiceImpl.java new file mode 100644 index 0000000..c5feb1e --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/AppointmentServiceImpl.java @@ -0,0 +1,161 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.service.AppointmentService; +import com.example.ead_backend.dto.AppointmentDTO; +import com.example.ead_backend.model.entity.Appointment; +import com.example.ead_backend.model.entity.TimeLog; +import com.example.ead_backend.model.enums.AppointmentStatus; +import com.example.ead_backend.repository.AppointmentRepository; +import com.example.ead_backend.repository.TimeLogRepository; +import com.example.ead_backend.mapper.AppointmentMapper; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.time.LocalDate; +import java.time.LocalTime; + +@Service +@RequiredArgsConstructor +public class AppointmentServiceImpl implements AppointmentService { + + private final AppointmentRepository appointmentRepository; + private final TimeLogRepository timeLogRepository; + private final AppointmentMapper appointmentMapper; + + @Override + public AppointmentDTO createAppointment(AppointmentDTO dto) { + // Enforce unique slot per date (by start time). This is global; can be extended per-employee later. + LocalDate date = dto.getDate(); + String start = dto.getStartTime(); + if (date != null && start != null && appointmentRepository.existsByDateAndStartTime(date, start)) { + throw new IllegalStateException("Time slot already booked for this date"); + } + Appointment entity = appointmentMapper.toEntity(dto); + // ensure new fields are copied (mapper should handle this if configured) + entity.setCustomerId(dto.getCustomerId()); + entity.setVehicleId(dto.getVehicleId()); + Appointment saved = appointmentRepository.save(entity); + return appointmentMapper.toDTO(saved); + } + + @Override + public AppointmentDTO getAppointmentById(String id) { + Appointment entity = appointmentRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Appointment not found with id " + id)); + return appointmentMapper.toDTO(entity); + } + + @Override + public List getAllAppointments() { + return appointmentRepository.findAll() + .stream() + .map(appointmentMapper::toDTO) + .collect(Collectors.toList()); + } + + @Override + public List getAppointmentsByCustomerId(String customerId) { + return appointmentRepository.findByCustomerId(customerId) + .stream() + .map(appointmentMapper::toDTO) + .collect(Collectors.toList()); + } + + @Override + public AppointmentDTO updateAppointment(String id, AppointmentDTO dto) { + Appointment existing = appointmentRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Appointment not found with id " + id)); + + // If changing date/startTime, enforce uniqueness + LocalDate newDate = dto.getDate(); + String newStart = dto.getStartTime(); + if (newDate != null && newStart != null) { + boolean slotTaken = appointmentRepository.existsByDateAndStartTime(newDate, newStart); + // allow updating to same slot the record already has + boolean sameSlot = newDate.equals(existing.getDate()) && newStart.equals(existing.getStartTime()); + if (slotTaken && !sameSlot) { + throw new IllegalStateException("Time slot already booked for this date"); + } + } + + existing.setService(dto.getService()); + existing.setCustomerId(dto.getCustomerId()); + existing.setVehicleId(dto.getVehicleId()); + existing.setVehicleNo(dto.getVehicleNo()); + existing.setDate(dto.getDate()); + existing.setStartTime(dto.getStartTime()); + existing.setEndTime(dto.getEndTime()); + // Update legacy 'time' to satisfy NOT NULL constraint + String start = dto.getStartTime(); + String end = dto.getEndTime(); + String derived = (start != null && end != null) ? start + "-" + end : (start != null ? start : "00:00"); + existing.setTime(derived); + existing.setStatus(dto.getStatus()); + + Appointment updated = appointmentRepository.save(existing); + return appointmentMapper.toDTO(updated); + } + + @Override + public void deleteAppointment(String id) { + appointmentRepository.deleteById(id); + } + + @Override + public List getBookedStartTimes(LocalDate date) { + // 1) Appointment-based bookings (exclude CANCELLED) + List bookedFromAppointments = appointmentRepository.findByDate(date) + .stream() + .filter(a -> a.getStatus() != AppointmentStatus.CANCELLED) + .map(Appointment::getStartTime) + .collect(Collectors.toList()); + + // 2) TimeLog-based blocked intervals expanded into 30-minute slot starts + List logs = timeLogRepository.findByDate(date); + Set bookedFromLogs = new HashSet<>(); + for (TimeLog log : logs) { + LocalTime start = LocalTime.parse(safeHHMM(log.getStartTime())); + LocalTime end = LocalTime.parse(safeHHMM(log.getEndTime())); + for (LocalTime t = start; !t.isAfter(end.minusMinutes(30)); t = t.plusMinutes(30)) { + bookedFromLogs.add(toHHMM(t)); + } + } + + // 3) Merge and return unique HH:mm values + Set all = new HashSet<>(bookedFromAppointments); + all.addAll(bookedFromLogs); + return new ArrayList<>(all); + } + + private static String toHHMM(LocalTime t) { + return String.format("%02d:%02d", t.getHour(), t.getMinute()); + } + + private static String safeHHMM(String t) { + if (t == null) return "00:00"; + String s = t.trim(); + if (s.contains("-")) s = s.split("-")[0].trim(); + if (s.matches("^\\d{1,2}:\\d{2}\\s*[AaPp][Mm]$")) { + // convert 12h to 24h + String[] parts = s.split(" "); + String[] hm = parts[0].split(":"); + int h = Integer.parseInt(hm[0]); + String m = hm[1]; + String mer = parts[1].toUpperCase(); + if (mer.equals("PM") && h != 12) h += 12; + if (mer.equals("AM") && h == 12) h = 0; + return String.format("%02d:%s", h, m); + } + // strip seconds if any + if (s.matches("^\\d{2}:\\d{2}:\\d{2}$")) return s.substring(0,5); + // pad hour if needed + if (s.matches("^\\d{1}:\\d{2}$")) return "0" + s; + return s; + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/CustomerProfileServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/CustomerProfileServiceImpl.java new file mode 100644 index 0000000..9220035 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/CustomerProfileServiceImpl.java @@ -0,0 +1,101 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.dto.CustomerProfileDTO; +import com.example.ead_backend.dto.UpdateCustomerProfileRequest; +import com.example.ead_backend.model.entity.Customer; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.repository.CustomerRepository; +import com.example.ead_backend.repository.UserRepo; +import com.example.ead_backend.service.CustomerProfileService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class CustomerProfileServiceImpl implements CustomerProfileService { + + private final CustomerRepository customerRepository; + private final UserRepo userRepository; + + @Override + public CustomerProfileDTO getCustomerProfileByUserId(Long userId) { + log.info("Fetching customer profile for user ID: {}", userId); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found with ID: " + userId)); + + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("Customer not found for user ID: " + userId)); + + return mapToDTO(customer, user); + } + + @Override + public CustomerProfileDTO getCustomerProfileByCustomerId(Long customerId) { + log.info("Fetching customer profile for customer ID: {}", customerId); + + Customer customer = customerRepository.findById(customerId) + .orElseThrow(() -> new RuntimeException("Customer not found with ID: " + customerId)); + + return mapToDTO(customer, customer.getUser()); + } + + @Override + public CustomerProfileDTO getCustomerProfileByEmail(String email) { + log.info("Fetching customer profile for email: {}", email); + + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found with email: " + email)); + + Customer customer = customerRepository.findByUserId(user.getId()) + .orElseThrow(() -> new RuntimeException("Customer not found for email: " + email)); + + return mapToDTO(customer, user); + } + + @Override + @Transactional + public CustomerProfileDTO updateCustomerProfile(Long userId, UpdateCustomerProfileRequest request) { + log.info("Updating customer profile for user ID: {}", userId); + + // Fetch user + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found with ID: " + userId)); + + // Fetch customer + Customer customer = customerRepository.findByUserId(userId) + .orElseThrow(() -> new RuntimeException("Customer not found for user ID: " + userId)); + + // Update user fields (firstName and lastName) + user.setFirstName(request.getFirstName()); + user.setLastName(request.getLastName()); + + // Update customer fields (phoneNumber) + customer.setPhoneNumber(request.getPhoneNumber()); + + // Save changes + userRepository.save(user); + customerRepository.save(customer); + + log.info("Customer profile updated successfully for user ID: {}", userId); + + return mapToDTO(customer, user); + } + + /** + * Helper method to map Customer and User entities to CustomerProfileDTO + */ + private CustomerProfileDTO mapToDTO(Customer customer, User user) { + return CustomerProfileDTO.builder() + .id(customer.getId()) + .userId(user.getId()) + .firstName(user.getFirstName()) + .lastName(user.getLastName()) + .email(user.getEmail()) + .phoneNumber(customer.getPhoneNumber()) + .build(); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/CustomerServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..216c0bf --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/CustomerServiceImpl.java @@ -0,0 +1,28 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.model.entity.Customer; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.repository.CustomerRepository; +import com.example.ead_backend.service.CustomerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CustomerServiceImpl implements CustomerService { + + @Autowired + private CustomerRepository customerRepository; + + @Override + @Transactional + public Customer createCustomer(User user, String phoneNumber) { + Customer customer = new Customer(user, phoneNumber); + return customerRepository.save(customer); + } + + @Override + public Customer findByUserId(Long userId) { + return customerRepository.findByUserId(userId).orElse(null); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/EmployeeServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/EmployeeServiceImpl.java new file mode 100644 index 0000000..918da41 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/EmployeeServiceImpl.java @@ -0,0 +1,34 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.model.entity.Employee; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.model.enums.Role; +import com.example.ead_backend.repository.EmployeeRepository; +import com.example.ead_backend.service.EmployeeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@Service +public class EmployeeServiceImpl implements EmployeeService { + + @Autowired + private EmployeeRepository employeeRepository; + + @Override + @Transactional + public Employee createEmployee(User user, Role role, LocalDate joinedDate) { + if (role != Role.ADMIN && role != Role.EMPLOYEE) { + throw new IllegalArgumentException("Invalid role for employee"); + } + Employee employee = new Employee(user, role, joinedDate); + return employeeRepository.save(employee); + } + + @Override + public Employee findByUserId(Long userId) { + return employeeRepository.findByUserId(userId).orElse(null); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/NotificationServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/NotificationServiceImpl.java new file mode 100644 index 0000000..72bc691 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/NotificationServiceImpl.java @@ -0,0 +1,68 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.dto.NotificationDTO; +import com.example.ead_backend.mapper.NotificationMapper; +import com.example.ead_backend.model.entity.Notification; +import com.example.ead_backend.model.enums.NotificationType; +import com.example.ead_backend.repository.NotificationRepository; +import com.example.ead_backend.service.NotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Implementation of NotificationService for managing notifications. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class NotificationServiceImpl implements NotificationService { + + private final NotificationRepository notificationRepository; + private final NotificationMapper notificationMapper; + + @Override + @Transactional + public NotificationDTO createNotification(Long userId, NotificationType type, String message) { + log.info("Creating notification for user {} of type {}", userId, type); + + Notification notification = Notification.builder() + .userId(userId) + .type(type) + .message(message) + .isRead(false) + .build(); + + Notification saved = notificationRepository.save(notification); + log.debug("Notification created with ID: {}", saved.getId()); + + return notificationMapper.toDto(saved); + } + + @Override + @Transactional(readOnly = true) + public List getNotificationsForUser(Long userId) { + log.debug("Fetching notifications for user {}", userId); + List notifications = notificationRepository.findByUserIdOrderByCreatedAtDesc(userId); + + return notifications.stream() + .map(notificationMapper::toDto) + .collect(Collectors.toList()); + } + + @Override + @Transactional + public void markAsRead(Long notificationId) { + log.debug("Marking notification {} as read", notificationId); + + notificationRepository.findById(notificationId).ifPresent(notification -> { + notification.setIsRead(true); + notificationRepository.save(notification); + log.info("Notification {} marked as read", notificationId); + }); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/PasswordResetService.java b/src/main/java/com/example/ead_backend/service/impl/PasswordResetService.java new file mode 100644 index 0000000..d1324de --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/PasswordResetService.java @@ -0,0 +1,121 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.model.entity.OTP; +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.repository.OTPRepository; +import com.example.ead_backend.repository.UserRepo; +import com.example.ead_backend.service.EmailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.Random; + +@Service +public class PasswordResetService { + + @Autowired + private OTPRepository otpRepository; + + @Autowired + private UserRepo userRepo; + + @Autowired + private EmailService emailService; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Transactional + public void generateAndSendOTP(String email) { + // Check if user exists + User user = userRepo.findByEmail(email) + .orElseThrow(() -> new RuntimeException("No user found with this email address")); + + // Generate 6-digit OTP + String otp = String.format("%06d", new Random().nextInt(999999)); + + // Delete any existing OTP for this email + otpRepository.deleteByEmail(email); + + // Create new OTP entity + OTP otpEntity = new OTP(); + otpEntity.setEmail(email); + otpEntity.setOtp(otp); + otpEntity.setExpiryTime(LocalDateTime.now().plusMinutes(10)); // Valid for 10 minutes + otpRepository.save(otpEntity); + + // Send OTP via email + emailService.sendOTP(email, otp); + } + + public boolean verifyOTP(String email, String otp) { + Optional otpEntity = otpRepository.findByEmailAndOtp(email, otp); + + if (otpEntity.isPresent()) { + OTP otpRecord = otpEntity.get(); + // Check if OTP is still valid (not expired) + if (otpRecord.getExpiryTime().isAfter(LocalDateTime.now())) { + return true; + } else { + // OTP expired, delete it + otpRepository.deleteByEmail(email); + throw new RuntimeException("OTP has expired. Please request a new one."); + } + } + return false; + } + + @Transactional + public void resetPassword(String email, String otp, String newPassword) { + // Verify OTP + if (!verifyOTP(email, otp)) { + throw new RuntimeException("Invalid OTP"); + } + + // Find user + User user = userRepo.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + + // Update password + user.setPassword(passwordEncoder.encode(newPassword)); + userRepo.save(user); + + // Delete used OTP + otpRepository.deleteByEmail(email); + } + + @Transactional + public void deleteExpiredOTPs() { + // This method can be called periodically to clean up expired OTPs + otpRepository.findAll().forEach(otp -> { + if (otp.getExpiryTime().isBefore(LocalDateTime.now())) { + otpRepository.delete(otp); + } + }); + } + + @Transactional + public void changePassword(String email, String oldPassword, String newPassword) { + // Find user + User user = userRepo.findByEmail(email) + .orElseThrow(() -> new RuntimeException("User not found")); + + // Verify old password + if (!passwordEncoder.matches(oldPassword, user.getPassword())) { + throw new RuntimeException("Old password is incorrect"); + } + + // Check if new password is different from old password + if (passwordEncoder.matches(newPassword, user.getPassword())) { + throw new RuntimeException("New password must be different from old password"); + } + + // Update password + user.setPassword(passwordEncoder.encode(newPassword)); + userRepo.save(user); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/ProgressServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/ProgressServiceImpl.java new file mode 100644 index 0000000..6caa1e3 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/ProgressServiceImpl.java @@ -0,0 +1,115 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.dto.ProgressUpdateRequest; +import com.example.ead_backend.mapper.ProgressMapper; +import com.example.ead_backend.model.entity.Notification; +import com.example.ead_backend.model.entity.ProgressUpdate; +import com.example.ead_backend.model.enums.NotificationType; +import com.example.ead_backend.repository.NotificationRepository; +import com.example.ead_backend.repository.ProgressUpdateRepository; +import com.example.ead_backend.service.EmailService; +import com.example.ead_backend.service.ProgressCalculationService; +import com.example.ead_backend.service.ProgressService; +import com.example.ead_backend.service.WebSocketNotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Implementation of ProgressService for managing progress updates. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class ProgressServiceImpl implements ProgressService { + + private final ProgressUpdateRepository progressUpdateRepository; + private final NotificationRepository notificationRepository; + private final ProgressMapper progressMapper; + private final ProgressCalculationService progressCalculationService; + private final WebSocketNotificationService webSocketNotificationService; + private final EmailService emailService; + + @Override + @Transactional + public ProgressResponse createOrUpdateProgress(Long appointmentId, ProgressUpdateRequest request, Long updatedBy) { + log.info("Creating/updating progress for appointment {} by user {}", appointmentId, updatedBy); + + // Convert DTO to entity + ProgressUpdate progressUpdate = progressMapper.toEntity(request); + progressUpdate.setAppointmentId(appointmentId); + progressUpdate.setUpdatedBy(updatedBy); + + // Save progress update + ProgressUpdate saved = progressUpdateRepository.save(progressUpdate); + log.debug("Progress update saved with ID: {}", saved.getId()); + + // Calculate overall progress percentage + int overallProgress = progressCalculationService.getLatestProgress(appointmentId); + log.debug("Overall progress for appointment {}: {}%", appointmentId, overallProgress); + + // Create notification + String notificationMessage = String.format( + "Progress updated for appointment #%d: %s (%d%%)", + appointmentId, request.getStage(), request.getPercentage()); + + Notification notification = Notification.builder() + .userId(updatedBy) // In real scenario, should notify customer + .type(NotificationType.PROGRESS_UPDATE) + .message(notificationMessage) + .isRead(false) + .build(); + + notificationRepository.save(notification); + log.debug("Notification created for progress update"); + + // Convert to response + ProgressResponse response = progressMapper.toResponse(saved); + + // Broadcast via WebSocket + try { + webSocketNotificationService.broadcastProgressUpdate(appointmentId, response); + } catch (Exception e) { + log.error("Failed to broadcast progress update via WebSocket: {}", e.getMessage()); + } + + // Send email notification (async in real scenario) + try { + emailService.sendProgressUpdateNotification( + "customer@example.com", // Should fetch from appointment/customer + appointmentId, + request.getStage(), + request.getPercentage(), + request.getRemarks()); + } catch (Exception e) { + log.error("Failed to send email notification: {}", e.getMessage()); + } + + log.info("Progress update completed successfully for appointment {}", appointmentId); + return response; + } + + @Override + @Transactional(readOnly = true) + public List getProgressForAppointment(Long appointmentId) { + log.debug("Fetching progress history for appointment {}", appointmentId); + + List updates = progressUpdateRepository.findByAppointmentIdOrderByCreatedAtAsc(appointmentId); + + return updates.stream() + .map(progressMapper::toResponse) + .collect(Collectors.toList()); + } + + @Override + @Transactional(readOnly = true) + public int calculateProgressPercentage(Long appointmentId) { + log.debug("Calculating progress percentage for appointment {}", appointmentId); + return progressCalculationService.getLatestProgress(appointmentId); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/ProjectServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/ProjectServiceImpl.java new file mode 100644 index 0000000..415c7f7 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/ProjectServiceImpl.java @@ -0,0 +1,71 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.service.ProjectService; +import com.example.ead_backend.dto.ProjectDTO; +import com.example.ead_backend.model.entity.Project; +import com.example.ead_backend.repository.ProjectRepository; +import com.example.ead_backend.mapper.ProjectMapper; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ProjectServiceImpl implements ProjectService { + private final ProjectRepository projectRepository; + private final ProjectMapper projectMapper; + + @Override + public ProjectDTO createProject(ProjectDTO dto) { + Project entity = projectMapper.toEntity(dto); + Project saved = projectRepository.save(entity); + return projectMapper.toDTO(saved); + } + + @Override + public ProjectDTO getProjectById(String id) { + Project entity = projectRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Project not found with id " + id)); + return projectMapper.toDTO(entity); + } + + @Override + public List getAllProjects() { + return projectRepository.findAll() + .stream() + .map(projectMapper::toDTO) + .collect(Collectors.toList()); + } + + @Override + public List getProjectsByCustomerId(String customerId) { + return projectRepository.findByCustomerId(customerId) + .stream() + .map(projectMapper::toDTO) + .collect(Collectors.toList()); + } + + @Override + public ProjectDTO updateProject(String id, ProjectDTO dto) { + Project existing = projectRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Project not found with id " + id)); + + existing.setName(dto.getName()); + existing.setDescription(dto.getDescription()); + existing.setCustomerId(dto.getCustomerId()); + existing.setStartDate(dto.getStartDate()); + existing.setEndDate(dto.getEndDate()); + existing.setStatus(dto.getStatus()); + + Project updated = projectRepository.save(existing); + return projectMapper.toDTO(updated); + } + + @Override + public void deleteProject(String id) { + projectRepository.deleteById(id); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/TaskServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/TaskServiceImpl.java new file mode 100644 index 0000000..782152a --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/TaskServiceImpl.java @@ -0,0 +1,7 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.service.TaskService; + +public class TaskServiceImpl implements TaskService { + +} diff --git a/src/main/java/com/example/ead_backend/service/impl/TimeLogServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/TimeLogServiceImpl.java new file mode 100644 index 0000000..044a374 --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/TimeLogServiceImpl.java @@ -0,0 +1,7 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.service.TimeLogService; + +public class TimeLogServiceImpl implements TimeLogService { + +} diff --git a/src/main/java/com/example/ead_backend/service/impl/UserService.java b/src/main/java/com/example/ead_backend/service/impl/UserService.java new file mode 100644 index 0000000..931633f --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/UserService.java @@ -0,0 +1,63 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.model.entity.User; +import com.example.ead_backend.repository.UserRepo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.regex.Pattern; + +@Service +public class UserService implements UserDetailsService { + + @Autowired + private UserRepo userRepo; + + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$"); + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepo.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email)); + } + + public User save(User user) { + return userRepo.save(user); + } + + public User createUser(String firstName, String lastName, String password, String email) { + // Validate email format + if (!isValidEmail(email)) { + throw new IllegalArgumentException("Invalid email format"); + } + + // Validate password length + if (password.length() < 6) { + throw new IllegalArgumentException("Password must be at least 6 characters long"); + } + + // Check if user already exists by email + if (existsByEmail(email)) { + throw new IllegalArgumentException("Email already exists"); + } + + User user = new User(firstName, lastName, password, email); + return save(user); + } + + public boolean existsByEmail(String email) { + return userRepo.existsByEmail(email); + } + + public User findByEmail(String email) { + return userRepo.findByEmail(email).orElse(null); + } + + private boolean isValidEmail(String email) { + return EMAIL_PATTERN.matcher(email).matches(); + } +} diff --git a/src/main/java/com/example/ead_backend/service/impl/VehicleServiceImpl.java b/src/main/java/com/example/ead_backend/service/impl/VehicleServiceImpl.java new file mode 100644 index 0000000..b14e0da --- /dev/null +++ b/src/main/java/com/example/ead_backend/service/impl/VehicleServiceImpl.java @@ -0,0 +1,170 @@ +package com.example.ead_backend.service.impl; + +import com.example.ead_backend.dto.VehicleDTO; +import com.example.ead_backend.mapper.VehicleMapper; +import com.example.ead_backend.model.entity.Customer; +import com.example.ead_backend.model.entity.Vehicle; +import com.example.ead_backend.repository.CustomerRepository; +import com.example.ead_backend.repository.VehicleRepository; +import com.example.ead_backend.service.CloudinaryService; +import com.example.ead_backend.service.VehicleService; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor // Lombok: creates a constructor for final fields (auto-injects them) +public class VehicleServiceImpl implements VehicleService { + + private final VehicleRepository vehicleRepository; + private final CustomerRepository customerRepository; + private final VehicleMapper vehicleMapper; + private final CloudinaryService cloudinaryService; + + @Override + public VehicleDTO createVehicle(VehicleDTO vehicleDTO) { + // Find the customer + Customer customer = customerRepository.findById(vehicleDTO.getCustomerId()) + .orElseThrow(() -> new RuntimeException("Customer not found with id " + vehicleDTO.getCustomerId())); + + // Create vehicle entity + Vehicle vehicle = vehicleMapper.toEntity(vehicleDTO); + vehicle.setCustomer(customer); + + // Save and return + Vehicle saved = vehicleRepository.save(vehicle); + return vehicleMapper.toDTO(saved); + } + + @Override + public VehicleDTO createVehicleWithImage(VehicleDTO vehicleDTO, MultipartFile image) throws IOException { + // Find the customer + Customer customer = customerRepository.findById(vehicleDTO.getCustomerId()) + .orElseThrow(() -> new RuntimeException("Customer not found with id " + vehicleDTO.getCustomerId())); + + // Upload image to Cloudinary + Map uploadResult = cloudinaryService.uploadImage(image); + String imageUrl = (String) uploadResult.get("secure_url"); + String publicId = (String) uploadResult.get("public_id"); + + // Create vehicle entity + Vehicle vehicle = vehicleMapper.toEntity(vehicleDTO); + vehicle.setCustomer(customer); + vehicle.setImageUrl(imageUrl); + vehicle.setImagePublicId(publicId); + + // Save and return + Vehicle saved = vehicleRepository.save(vehicle); + return vehicleMapper.toDTO(saved); + } + + @Override + public VehicleDTO getVehicleById(Long id) { + Vehicle vehicle = vehicleRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Vehicle not found with id " + id)); + return vehicleMapper.toDTO(vehicle); + } + + @Override + public List getAllVehicles() { + return vehicleRepository.findAll() + .stream() + .map(vehicleMapper::toDTO) + .collect(Collectors.toList()); + } + + @Override + public List getVehiclesByCustomerId(Long customerId) { + // Verify customer exists + customerRepository.findById(customerId) + .orElseThrow(() -> new RuntimeException("Customer not found with id " + customerId)); + + return vehicleRepository.findByCustomerId(customerId) + .stream() + .map(vehicleMapper::toDTO) + .collect(Collectors.toList()); + } + + @Override + public VehicleDTO updateVehicle(Long id, VehicleDTO vehicleDTO) { + Vehicle existing = vehicleRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Vehicle not found with id " + id)); + + // Update fields + existing.setModel(vehicleDTO.getModel()); + existing.setColor(vehicleDTO.getColor()); + existing.setVin(vehicleDTO.getVin()); + existing.setLicensePlate(vehicleDTO.getLicensePlate()); + existing.setYear(vehicleDTO.getYear()); + existing.setRegistrationDate(vehicleDTO.getRegistrationDate()); + + // If customer is being changed + if (vehicleDTO.getCustomerId() != null && !vehicleDTO.getCustomerId().equals(existing.getCustomer().getId())) { + Customer newCustomer = customerRepository.findById(vehicleDTO.getCustomerId()) + .orElseThrow(() -> new RuntimeException("Customer not found with id " + vehicleDTO.getCustomerId())); + existing.setCustomer(newCustomer); + } + + Vehicle updated = vehicleRepository.save(existing); + return vehicleMapper.toDTO(updated); + } + + @Override + public VehicleDTO updateVehicleWithImage(Long id, VehicleDTO vehicleDTO, MultipartFile image) throws IOException { + Vehicle existing = vehicleRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Vehicle not found with id " + id)); + + // Update image if provided + if (image != null && !image.isEmpty()) { + // Upload new image and delete old one + Map uploadResult = cloudinaryService.updateImage(image, existing.getImagePublicId()); + String imageUrl = (String) uploadResult.get("secure_url"); + String publicId = (String) uploadResult.get("public_id"); + + existing.setImageUrl(imageUrl); + existing.setImagePublicId(publicId); + } + + // Update fields + existing.setModel(vehicleDTO.getModel()); + existing.setColor(vehicleDTO.getColor()); + existing.setVin(vehicleDTO.getVin()); + existing.setLicensePlate(vehicleDTO.getLicensePlate()); + existing.setYear(vehicleDTO.getYear()); + existing.setRegistrationDate(vehicleDTO.getRegistrationDate()); + + // If customer is being changed + if (vehicleDTO.getCustomerId() != null && !vehicleDTO.getCustomerId().equals(existing.getCustomer().getId())) { + Customer newCustomer = customerRepository.findById(vehicleDTO.getCustomerId()) + .orElseThrow(() -> new RuntimeException("Customer not found with id " + vehicleDTO.getCustomerId())); + existing.setCustomer(newCustomer); + } + + Vehicle updated = vehicleRepository.save(existing); + return vehicleMapper.toDTO(updated); + } + + @Override + public void deleteVehicle(Long id) { + Vehicle vehicle = vehicleRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Vehicle not found with id " + id)); + + // Delete image from Cloudinary if exists + if (vehicle.getImagePublicId() != null && !vehicle.getImagePublicId().isEmpty()) { + try { + cloudinaryService.deleteImage(vehicle.getImagePublicId()); + } catch (IOException e) { + // Log error but continue with vehicle deletion + // You might want to handle this differently based on your requirements + } + } + + vehicleRepository.deleteById(id); + } +} diff --git a/src/main/java/com/example/ead_backend/service/notification/NotificationService.java b/src/main/java/com/example/ead_backend/service/notification/NotificationService.java deleted file mode 100644 index a7b8ef2..0000000 --- a/src/main/java/com/example/ead_backend/service/notification/NotificationService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.notification; - -public class NotificationService { - -} diff --git a/src/main/java/com/example/ead_backend/service/notification/NotificationServiceImpl.java b/src/main/java/com/example/ead_backend/service/notification/NotificationServiceImpl.java deleted file mode 100644 index 3a5310c..0000000 --- a/src/main/java/com/example/ead_backend/service/notification/NotificationServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.notification; - -public class NotificationServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/progress/ProgressService.java b/src/main/java/com/example/ead_backend/service/progress/ProgressService.java deleted file mode 100644 index 3d8a080..0000000 --- a/src/main/java/com/example/ead_backend/service/progress/ProgressService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.progress; - -public class ProgressService { - -} diff --git a/src/main/java/com/example/ead_backend/service/progress/ProgressServiceImpl.java b/src/main/java/com/example/ead_backend/service/progress/ProgressServiceImpl.java deleted file mode 100644 index 29a352e..0000000 --- a/src/main/java/com/example/ead_backend/service/progress/ProgressServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.progress; - -public class ProgressServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceCategoryService.java b/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceCategoryService.java deleted file mode 100644 index 9628ea6..0000000 --- a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceCategoryService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.servicemanagement; - -public class ServiceCategoryService { - -} diff --git a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceCategoryServiceImpl.java b/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceCategoryServiceImpl.java deleted file mode 100644 index 5bc081d..0000000 --- a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceCategoryServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.servicemanagement; - -public class ServiceCategoryServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceManagementService.java b/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceManagementService.java deleted file mode 100644 index ef88d34..0000000 --- a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceManagementService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.servicemanagement; - -public class ServiceManagementService { - -} diff --git a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceManagementServiceImpl.java b/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceManagementServiceImpl.java deleted file mode 100644 index 0c48faa..0000000 --- a/src/main/java/com/example/ead_backend/service/servicemanagement/ServiceManagementServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.servicemanagement; - -public class ServiceManagementServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/timelog/TimeCalculationService.java b/src/main/java/com/example/ead_backend/service/timelog/TimeCalculationService.java deleted file mode 100644 index 30a7e11..0000000 --- a/src/main/java/com/example/ead_backend/service/timelog/TimeCalculationService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.timelog; - -public class TimeCalculationService { - -} diff --git a/src/main/java/com/example/ead_backend/service/timelog/TimeLogService.java b/src/main/java/com/example/ead_backend/service/timelog/TimeLogService.java deleted file mode 100644 index 9236adb..0000000 --- a/src/main/java/com/example/ead_backend/service/timelog/TimeLogService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.timelog; - -public class TimeLogService { - -} diff --git a/src/main/java/com/example/ead_backend/service/timelog/TimeLogServiceImpl.java b/src/main/java/com/example/ead_backend/service/timelog/TimeLogServiceImpl.java deleted file mode 100644 index 44a3666..0000000 --- a/src/main/java/com/example/ead_backend/service/timelog/TimeLogServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.timelog; - -public class TimeLogServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/user/ProfileService.java b/src/main/java/com/example/ead_backend/service/user/ProfileService.java deleted file mode 100644 index 4f0e207..0000000 --- a/src/main/java/com/example/ead_backend/service/user/ProfileService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.user; - -public class ProfileService { - -} diff --git a/src/main/java/com/example/ead_backend/service/user/ProfileServiceImpl.java b/src/main/java/com/example/ead_backend/service/user/ProfileServiceImpl.java deleted file mode 100644 index b3b0a24..0000000 --- a/src/main/java/com/example/ead_backend/service/user/ProfileServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.user; - -public class ProfileServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/user/UserService.java b/src/main/java/com/example/ead_backend/service/user/UserService.java deleted file mode 100644 index 7c9182d..0000000 --- a/src/main/java/com/example/ead_backend/service/user/UserService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.user; - -public class UserService { - -} diff --git a/src/main/java/com/example/ead_backend/service/user/UserServiceImpl.java b/src/main/java/com/example/ead_backend/service/user/UserServiceImpl.java deleted file mode 100644 index 865490b..0000000 --- a/src/main/java/com/example/ead_backend/service/user/UserServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.user; - -public class UserServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/service/vehicle/VehicleService.java b/src/main/java/com/example/ead_backend/service/vehicle/VehicleService.java deleted file mode 100644 index 8eac7cc..0000000 --- a/src/main/java/com/example/ead_backend/service/vehicle/VehicleService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.vehicle; - -public class VehicleService { - -} diff --git a/src/main/java/com/example/ead_backend/service/vehicle/VehicleServiceImpl.java b/src/main/java/com/example/ead_backend/service/vehicle/VehicleServiceImpl.java deleted file mode 100644 index 0c0a683..0000000 --- a/src/main/java/com/example/ead_backend/service/vehicle/VehicleServiceImpl.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.ead_backend.service.vehicle; - -public class VehicleServiceImpl { - -} diff --git a/src/main/java/com/example/ead_backend/util/JwtUtil.java b/src/main/java/com/example/ead_backend/util/JwtUtil.java new file mode 100644 index 0000000..ee1796b --- /dev/null +++ b/src/main/java/com/example/ead_backend/util/JwtUtil.java @@ -0,0 +1,70 @@ +package com.example.ead_backend.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + private static final String SECRET = "mySecretKeymySecretKeymySecretKeymySecretKey"; + private static final int JWT_EXPIRATION = 86400000; // 24 hours + + private Key getSigningKey() { + return Keys.hmacShaKeyFor(SECRET.getBytes()); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return createToken(claims, userDetails.getUsername()); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } +} diff --git a/src/main/java/com/example/ead_backend/websocket/ProgressWebSocketController.java b/src/main/java/com/example/ead_backend/websocket/ProgressWebSocketController.java new file mode 100644 index 0000000..347a4ac --- /dev/null +++ b/src/main/java/com/example/ead_backend/websocket/ProgressWebSocketController.java @@ -0,0 +1,71 @@ +package com.example.ead_backend.websocket; + +import com.example.ead_backend.model.message.NotificationMessage; +import com.example.ead_backend.model.message.ProgressUpdateMessage; +import com.example.ead_backend.model.message.StatusChangeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.stereotype.Controller; + +/** + * WebSocket controller for handling STOMP messages related to progress updates. + */ +@Controller +@RequiredArgsConstructor +@Slf4j +public class ProgressWebSocketController { + + /** + * Handle progress update messages from clients. + * + * @param appointmentId the appointment ID + * @param message the progress update message + * @return the message to broadcast to subscribers + */ + @MessageMapping("/progress/{appointmentId}") + @SendTo("/topic/progress.{appointmentId}") + public ProgressUpdateMessage handleProgressUpdate( + @DestinationVariable Long appointmentId, + ProgressUpdateMessage message) { + + log.info("Received progress update message for appointment {}", appointmentId); + return message; + } + + /** + * Handle status change messages from clients. + * + * @param appointmentId the appointment ID + * @param message the status change message + * @return the message to broadcast to subscribers + */ + @MessageMapping("/status/{appointmentId}") + @SendTo("/topic/status.{appointmentId}") + public StatusChangeMessage handleStatusChange( + @DestinationVariable Long appointmentId, + StatusChangeMessage message) { + + log.info("Received status change message for appointment {}", appointmentId); + return message; + } + + /** + * Handle notification messages for specific users. + * + * @param userId the user ID + * @param message the notification message + * @return the message to broadcast to the user + */ + @MessageMapping("/notifications/{userId}") + @SendTo("/topic/notifications.{userId}") + public NotificationMessage handleNotification( + @DestinationVariable Long userId, + NotificationMessage message) { + + log.info("Received notification message for user {}", userId); + return message; + } +} diff --git a/src/main/java/com/example/ead_backend/websocket/WebSocketConfig.java b/src/main/java/com/example/ead_backend/websocket/WebSocketConfig.java new file mode 100644 index 0000000..ca4ea0e --- /dev/null +++ b/src/main/java/com/example/ead_backend/websocket/WebSocketConfig.java @@ -0,0 +1,43 @@ +package com.example.ead_backend.websocket; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +/** + * WebSocket configuration for real-time notifications. + * Configures STOMP protocol over WebSocket with SockJS fallback. + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + /** + * Configure message broker for pub/sub messaging. + * + * @param config the message broker registry + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // Enable simple broker for topic subscriptions + config.enableSimpleBroker("/topic"); + + // Set application destination prefix for client messages + config.setApplicationDestinationPrefixes("/app"); + } + + /** + * Register STOMP endpoints with SockJS fallback. + * + * @param registry the STOMP endpoint registry + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // Register endpoint for WebSocket connections + registry.addEndpoint("/ws/progress") + .setAllowedOriginPatterns("*") + .withSockJS(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 02d8a75..2b68ffb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,29 @@ -spring.application.name=ead-backend +spring.application.name=auto-mobile + +# PostgreSQL Configuration +spring.datasource.url=jdbc:postgresql://localhost:5432/auto-mobile +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.driver-class-name=org.postgresql.Driver + +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.security.user.name=user +spring.security.user.password=1234 + +# Email Configuration for OTP +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=behaniw2002@gmail.com +spring.mail.password=efyb foni oxka npia +spring.mail.protocol=smtp +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.starttls.required=true +spring.mail.properties.mail.debug=true + +# Cloudinary Configuration +cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME:your-cloud-name} +cloudinary.api-key=${CLOUDINARY_API_KEY:your-api-key} +cloudinary.api-secret=${CLOUDINARY_API_SECRET:your-api-secret} diff --git a/src/test/java/com/example/ead_backend/controller/ProgressUpdateControllerTest.java b/src/test/java/com/example/ead_backend/controller/ProgressUpdateControllerTest.java new file mode 100644 index 0000000..6cb14a3 --- /dev/null +++ b/src/test/java/com/example/ead_backend/controller/ProgressUpdateControllerTest.java @@ -0,0 +1,242 @@ +package com.example.ead_backend.controller; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.dto.ProgressUpdateRequest; +import com.example.ead_backend.service.ProgressService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Controller tests for ProgressUpdateController. + */ +@WebMvcTest(controllers = {ProgressUpdateController.class, ProgressViewController.class}) +class ProgressUpdateControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ProgressService progressService; + + private ProgressUpdateRequest testRequest; + private ProgressResponse testResponse; + + @BeforeEach + void setUp() { + testRequest = ProgressUpdateRequest.builder() + .stage("Inspection") + .percentage(50) + .remarks("Initial inspection completed") + .build(); + + testResponse = ProgressResponse.builder() + .id(1L) + .appointmentId(100L) + .stage("Inspection") + .percentage(50) + .remarks("Initial inspection completed") + .updatedBy(10L) + .updatedAt(Timestamp.from(Instant.now())) + .build(); + } + + @Test + @WithMockUser + void testUpdateProgress_Success() throws Exception { + // Arrange + when(progressService.createOrUpdateProgress(anyLong(), any(ProgressUpdateRequest.class), anyLong())) + .thenReturn(testResponse); + + // Act & Assert + mockMvc.perform(put("/api/employee/progress/100") + .with(csrf()) + .header("X-User-Id", "10") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(testRequest))) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.appointmentId", is(100))) + .andExpect(jsonPath("$.stage", is("Inspection"))) + .andExpect(jsonPath("$.percentage", is(50))) + .andExpect(jsonPath("$.remarks", is("Initial inspection completed"))); + } + + @Test + @WithMockUser + void testUpdateProgress_InvalidRequest_MissingStage() throws Exception { + // Arrange + ProgressUpdateRequest invalidRequest = ProgressUpdateRequest.builder() + .percentage(50) + .remarks("Missing stage") + .build(); + + // Act & Assert + mockMvc.perform(put("/api/employee/progress/100") + .with(csrf()) + .header("X-User-Id", "10") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void testUpdateProgress_InvalidPercentage_LessThanZero() throws Exception { + // Arrange + ProgressUpdateRequest invalidRequest = ProgressUpdateRequest.builder() + .stage("Testing") + .percentage(-10) + .remarks("Invalid percentage") + .build(); + + // Act & Assert + mockMvc.perform(put("/api/employee/progress/100") + .with(csrf()) + .header("X-User-Id", "10") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void testUpdateProgress_InvalidPercentage_GreaterThan100() throws Exception { + // Arrange + ProgressUpdateRequest invalidRequest = ProgressUpdateRequest.builder() + .stage("Testing") + .percentage(150) + .remarks("Invalid percentage") + .build(); + + // Act & Assert + mockMvc.perform(put("/api/employee/progress/100") + .with(csrf()) + .header("X-User-Id", "10") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequest))) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void testUpdateStatus_Success() throws Exception { + // Arrange + when(progressService.createOrUpdateProgress(anyLong(), any(ProgressUpdateRequest.class), anyLong())) + .thenReturn(testResponse); + + // Act & Assert + mockMvc.perform(post("/api/employee/progress/100/status") + .with(csrf()) + .header("X-User-Id", "10") + .param("status", "In Progress")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().string("Status updated successfully")); + } + + @Test + @WithMockUser + void testGetProgressHistory_Success() throws Exception { + // Arrange + ProgressResponse response1 = ProgressResponse.builder() + .id(1L) + .appointmentId(100L) + .stage("Inspection") + .percentage(25) + .build(); + + ProgressResponse response2 = ProgressResponse.builder() + .id(2L) + .appointmentId(100L) + .stage("Repair") + .percentage(75) + .build(); + + List responses = Arrays.asList(response1, response2); + + when(progressService.getProgressForAppointment(100L)).thenReturn(responses); + + // Act & Assert + mockMvc.perform(get("/api/customer/progress/100") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].stage", is("Inspection"))) + .andExpect(jsonPath("$[0].percentage", is(25))) + .andExpect(jsonPath("$[1].stage", is("Repair"))) + .andExpect(jsonPath("$[1].percentage", is(75))); + } + + @Test + @WithMockUser + void testGetLatestProgress_Success() throws Exception { + // Arrange + List responses = Arrays.asList(testResponse); + when(progressService.getProgressForAppointment(100L)).thenReturn(responses); + + // Act & Assert + mockMvc.perform(get("/api/customer/progress/100/latest") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.stage", is("Inspection"))) + .andExpect(jsonPath("$.percentage", is(50))); + } + + @Test + @WithMockUser + void testGetLatestProgress_NotFound() throws Exception { + // Arrange + when(progressService.getProgressForAppointment(100L)).thenReturn(Arrays.asList()); + + // Act & Assert + mockMvc.perform(get("/api/customer/progress/100/latest") + .with(csrf())) + .andDo(print()) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser + void testGetProgressPercentage_Success() throws Exception { + // Arrange + when(progressService.calculateProgressPercentage(100L)).thenReturn(65); + + // Act & Assert + mockMvc.perform(get("/api/customer/progress/100/percentage") + .with(csrf())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().string("65")); + } +} diff --git a/src/test/java/com/example/ead_backend/service/ProgressServiceTest.java b/src/test/java/com/example/ead_backend/service/ProgressServiceTest.java new file mode 100644 index 0000000..b6c1cb6 --- /dev/null +++ b/src/test/java/com/example/ead_backend/service/ProgressServiceTest.java @@ -0,0 +1,239 @@ +package com.example.ead_backend.service; + +import com.example.ead_backend.dto.ProgressResponse; +import com.example.ead_backend.dto.ProgressUpdateRequest; +import com.example.ead_backend.mapper.ProgressMapper; +import com.example.ead_backend.model.entity.Notification; +import com.example.ead_backend.model.entity.ProgressUpdate; +import com.example.ead_backend.model.enums.NotificationType; +import com.example.ead_backend.repository.NotificationRepository; +import com.example.ead_backend.repository.ProgressUpdateRepository; +import com.example.ead_backend.service.impl.ProgressServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +/** + * Unit tests for ProgressService implementation. + */ +@ExtendWith(MockitoExtension.class) +class ProgressServiceTest { + + @Mock + private ProgressUpdateRepository progressUpdateRepository; + + @Mock + private NotificationRepository notificationRepository; + + @Mock + private ProgressMapper progressMapper; + + @Mock + private ProgressCalculationService progressCalculationService; + + @Mock + private WebSocketNotificationService webSocketNotificationService; + + @Mock + private EmailService emailService; + + @InjectMocks + private ProgressServiceImpl progressService; + + private ProgressUpdateRequest testRequest; + private ProgressUpdate testProgressUpdate; + private ProgressResponse testProgressResponse; + + @BeforeEach + void setUp() { + testRequest = ProgressUpdateRequest.builder() + .stage("Inspection") + .percentage(50) + .remarks("Initial inspection completed") + .build(); + + testProgressUpdate = ProgressUpdate.builder() + .id(1L) + .appointmentId(100L) + .stage("Inspection") + .percentage(50) + .remarks("Initial inspection completed") + .updatedBy(10L) + .createdAt(Timestamp.from(Instant.now())) + .updatedAt(Timestamp.from(Instant.now())) + .build(); + + testProgressResponse = ProgressResponse.builder() + .id(1L) + .appointmentId(100L) + .stage("Inspection") + .percentage(50) + .remarks("Initial inspection completed") + .updatedBy(10L) + .updatedAt(Timestamp.from(Instant.now())) + .build(); + } + + @Test + void testCreateOrUpdateProgress_Success() { + // Arrange + Long appointmentId = 100L; + Long updatedBy = 10L; + + when(progressMapper.toEntity(testRequest)).thenReturn(testProgressUpdate); + when(progressUpdateRepository.save(any(ProgressUpdate.class))).thenReturn(testProgressUpdate); + when(progressCalculationService.getLatestProgress(appointmentId)).thenReturn(50); + when(progressMapper.toResponse(testProgressUpdate)).thenReturn(testProgressResponse); + when(notificationRepository.save(any(Notification.class))).thenReturn(new Notification()); + + // Act + ProgressResponse result = progressService.createOrUpdateProgress(appointmentId, testRequest, updatedBy); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getAppointmentId()).isEqualTo(appointmentId); + assertThat(result.getStage()).isEqualTo("Inspection"); + assertThat(result.getPercentage()).isEqualTo(50); + + // Verify interactions + verify(progressUpdateRepository).save(any(ProgressUpdate.class)); + verify(notificationRepository).save(any(Notification.class)); + verify(webSocketNotificationService).broadcastProgressUpdate(anyLong(), any(ProgressResponse.class)); + verify(emailService).sendProgressUpdateNotification(anyString(), anyLong(), anyString(), anyInt(), anyString()); + } + + @Test + void testCreateOrUpdateProgress_CreatesNotification() { + // Arrange + Long appointmentId = 100L; + Long updatedBy = 10L; + + when(progressMapper.toEntity(testRequest)).thenReturn(testProgressUpdate); + when(progressUpdateRepository.save(any(ProgressUpdate.class))).thenReturn(testProgressUpdate); + when(progressCalculationService.getLatestProgress(appointmentId)).thenReturn(50); + when(progressMapper.toResponse(testProgressUpdate)).thenReturn(testProgressResponse); + + ArgumentCaptor notificationCaptor = ArgumentCaptor.forClass(Notification.class); + when(notificationRepository.save(notificationCaptor.capture())).thenReturn(new Notification()); + + // Act + progressService.createOrUpdateProgress(appointmentId, testRequest, updatedBy); + + // Assert + Notification savedNotification = notificationCaptor.getValue(); + assertThat(savedNotification).isNotNull(); + assertThat(savedNotification.getType()).isEqualTo(NotificationType.PROGRESS_UPDATE); + assertThat(savedNotification.getIsRead()).isFalse(); + assertThat(savedNotification.getMessage()).contains("Progress updated"); + } + + @Test + void testGetProgressForAppointment_ReturnsMultipleUpdates() { + // Arrange + Long appointmentId = 100L; + + ProgressUpdate update1 = ProgressUpdate.builder() + .id(1L) + .appointmentId(appointmentId) + .stage("Inspection") + .percentage(25) + .build(); + + ProgressUpdate update2 = ProgressUpdate.builder() + .id(2L) + .appointmentId(appointmentId) + .stage("Repair") + .percentage(75) + .build(); + + List updates = Arrays.asList(update1, update2); + + when(progressUpdateRepository.findByAppointmentIdOrderByCreatedAtAsc(appointmentId)).thenReturn(updates); + when(progressMapper.toResponse(update1)).thenReturn(ProgressResponse.builder().id(1L).stage("Inspection").percentage(25).build()); + when(progressMapper.toResponse(update2)).thenReturn(ProgressResponse.builder().id(2L).stage("Repair").percentage(75).build()); + + // Act + List results = progressService.getProgressForAppointment(appointmentId); + + // Assert + assertThat(results).hasSize(2); + assertThat(results.get(0).getStage()).isEqualTo("Inspection"); + assertThat(results.get(1).getStage()).isEqualTo("Repair"); + } + + @Test + void testCalculateProgressPercentage_DelegatesToCalculationService() { + // Arrange + Long appointmentId = 100L; + when(progressCalculationService.getLatestProgress(appointmentId)).thenReturn(65); + + // Act + int result = progressService.calculateProgressPercentage(appointmentId); + + // Assert + assertThat(result).isEqualTo(65); + verify(progressCalculationService).getLatestProgress(appointmentId); + } + + @Test + void testCreateOrUpdateProgress_HandlesWebSocketFailure() { + // Arrange + Long appointmentId = 100L; + Long updatedBy = 10L; + + when(progressMapper.toEntity(testRequest)).thenReturn(testProgressUpdate); + when(progressUpdateRepository.save(any(ProgressUpdate.class))).thenReturn(testProgressUpdate); + when(progressCalculationService.getLatestProgress(appointmentId)).thenReturn(50); + when(progressMapper.toResponse(testProgressUpdate)).thenReturn(testProgressResponse); + when(notificationRepository.save(any(Notification.class))).thenReturn(new Notification()); + + // Simulate WebSocket failure + doThrow(new RuntimeException("WebSocket connection failed")) + .when(webSocketNotificationService).broadcastProgressUpdate(anyLong(), any(ProgressResponse.class)); + + // Act & Assert - should not throw exception + ProgressResponse result = progressService.createOrUpdateProgress(appointmentId, testRequest, updatedBy); + + // Verify the operation completes despite WebSocket failure + assertThat(result).isNotNull(); + verify(progressUpdateRepository).save(any(ProgressUpdate.class)); + } + + @Test + void testCreateOrUpdateProgress_HandlesEmailFailure() { + // Arrange + Long appointmentId = 100L; + Long updatedBy = 10L; + + when(progressMapper.toEntity(testRequest)).thenReturn(testProgressUpdate); + when(progressUpdateRepository.save(any(ProgressUpdate.class))).thenReturn(testProgressUpdate); + when(progressCalculationService.getLatestProgress(appointmentId)).thenReturn(50); + when(progressMapper.toResponse(testProgressUpdate)).thenReturn(testProgressResponse); + when(notificationRepository.save(any(Notification.class))).thenReturn(new Notification()); + + // Simulate email failure + doThrow(new RuntimeException("Email server unavailable")) + .when(emailService).sendProgressUpdateNotification(anyString(), anyLong(), anyString(), anyInt(), anyString()); + + // Act & Assert - should not throw exception + ProgressResponse result = progressService.createOrUpdateProgress(appointmentId, testRequest, updatedBy); + + // Verify the operation completes despite email failure + assertThat(result).isNotNull(); + verify(progressUpdateRepository).save(any(ProgressUpdate.class)); + } +}