CoolFace
Apppublic

sakthivikram/geo-location

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
IssueService.java569 linesDownload Raw Back to service
1package com.georeport.service;2 3import com.georeport.dto.*;4import com.georeport.entity.*;5import com.georeport.exception.BadRequestException;6import com.georeport.exception.ResourceNotFoundException;7import com.georeport.mapper.IssueMapper;8import com.georeport.repository.*;9import jakarta.persistence.EntityManager;10import jakarta.persistence.PersistenceContext;11import jakarta.persistence.TypedQuery;12import jakarta.persistence.criteria.*;13import org.springframework.beans.factory.annotation.Autowired;14import org.springframework.data.domain.Page;15import org.springframework.data.domain.PageImpl;16import org.springframework.data.domain.Pageable;17import org.springframework.messaging.simp.SimpMessagingTemplate;18import org.springframework.stereotype.Service;19import org.springframework.transaction.annotation.Transactional;20import org.springframework.web.multipart.MultipartFile;21 22import java.io.IOException;23import java.time.LocalDateTime;24import java.util.ArrayList;25import java.util.HashMap;26import java.util.List;27import java.util.Map;28import java.util.stream.Collectors;29 30/**31 * Service for issue management operations.32 * Handles CRUD operations, geo-queries, and status updates.33 */34@Service35public class IssueService {36 37    // Rough conversion: 1 degree latitude โ‰ˆ 111km38    private static final double METERS_PER_DEGREE = 111000.0;39 40    @PersistenceContext41    private EntityManager entityManager;42 43    @Autowired44    private IssueRepository issueRepository;45 46    @Autowired47    private IssueCategoryRepository categoryRepository;48 49    @Autowired50    private IssueImageRepository imageRepository;51 52    @Autowired53    private IssueStatusHistoryRepository statusHistoryRepository;54 55    @Autowired56    private UserRepository userRepository;57 58    @Autowired59    private NotificationRepository notificationRepository;60 61    @Autowired62    private FileStorageService fileStorageService;63 64    @Autowired65    private IssueMapper issueMapper;66 67    @Autowired68    private DepartmentRoutingService departmentRoutingService;69 70    @Autowired71    private SimpMessagingTemplate messagingTemplate;72 73    /**74     * Create a new issue75     */76    @Transactional77    public IssueResponse createIssue(CreateIssueRequest request, User reporter, List<MultipartFile> images) {78        // Get category79        IssueCategory category = categoryRepository.findById(request.getCategoryId())80                .orElseThrow(() -> new ResourceNotFoundException("Category", "id", request.getCategoryId()));81 82        // --- SMART ROUTING: auto-assign department based on category ---83        String assignedDepartment = departmentRoutingService.route(category);84 85        // Create issue with lat/lng86        Issue issue = Issue.builder()87                .title(request.getTitle())88                .description(request.getDescription())89                .latitude(request.getLatitude())90                .longitude(request.getLongitude())91                .address(request.getAddress())92                .ward(request.getWard())93                .landmark(request.getLandmark())94                .category(category)95                .reporter(reporter)96                .contactPhone(request.getContactPhone())97                .contactEmail(request.getContactEmail())98                .status(IssueStatus.SUBMITTED)99                .priority(request.getPriority() != null ? request.getPriority() : IssuePriority.MEDIUM)100                .department(assignedDepartment)101                .build();102 103        issue = issueRepository.save(issue);104 105        // Add status history106        IssueStatusHistory history = IssueStatusHistory.builder()107                .issue(issue)108                .newStatus(IssueStatus.SUBMITTED)109                .changedBy(reporter)110                .changeReason("Issue created")111                .build();112        statusHistoryRepository.save(history);113 114        // Handle image uploads115        if (images != null && !images.isEmpty()) {116            for (int i = 0; i < images.size(); i++) {117                MultipartFile file = images.get(i);118                if (!file.isEmpty()) {119                    try {120                        String filename = fileStorageService.storeFile(file);121                        IssueImage image = IssueImage.builder()122                                .issue(issue)123                                .fileName(filename)124                                .originalName(file.getOriginalFilename())125                                .filePath(fileStorageService.getUploadPath().resolve(filename).toString())126                                .fileSize(file.getSize())127                                .contentType(file.getContentType())128                                .isPrimary(i == 0)129                                .build();130                        imageRepository.save(image);131                        issue.getImages().add(image);132                    } catch (IOException e) {133                        throw new BadRequestException("Failed to upload image: " + e.getMessage());134                    }135                }136            }137        }138 139        // Broadcast new issue to admin dashboard140        broadcastIssueUpdate("NEW_ISSUE", issue);141 142        return issueMapper.toResponse(issue);143    }144 145    /**146     * Get issue by ID147     */148    @Transactional(readOnly = true)149    public IssueResponse getIssueById(Long id) {150        Issue issue = issueRepository.findById(id)151                .orElseThrow(() -> new ResourceNotFoundException("Issue", "id", id));152        return issueMapper.toResponse(issue);153    }154 155    /**156     * Get all issues for a reporter157     */158    @Transactional(readOnly = true)159    public List<IssueResponse> getIssuesByReporter(Long reporterId) {160        return issueRepository.findByReporterIdOrderByCreatedAtDesc(reporterId)161                .stream()162                .map(issueMapper::toResponse)163                .collect(Collectors.toList());164    }165 166    /**167     * Get all issues with optional filters168     */169    @Transactional(readOnly = true)170    public Page<IssueResponse> getAllIssues(IssueStatus status, Long categoryId, String ward,171            LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {172        return issueRepository.findWithFilters(status, categoryId, ward, startDate, endDate, pageable)173                .map(issueMapper::toResponse);174    }175 176    /**177     * Get nearby issues178     */179    @Transactional(readOnly = true)180    public List<IssueResponse> getNearbyIssues(double latitude, double longitude, double radiusMeters) {181        // Convert meters to degrees (approximate)182        double radiusDegrees = radiusMeters / METERS_PER_DEGREE;183 184        return issueRepository.findNearbyIssues(latitude, longitude, radiusDegrees)185                .stream()186                .map(issueMapper::toResponse)187                .collect(Collectors.toList());188    }189 190    /**191     * Get issues within bounding box (for map viewport)192     */193    @Transactional(readOnly = true)194    public List<IssueResponse> getIssuesInBounds(double minLng, double minLat, double maxLng, double maxLat) {195        return issueRepository.findIssuesInBoundingBox(minLng, minLat, maxLng, maxLat)196                .stream()197                .map(issueMapper::toResponse)198                .collect(Collectors.toList());199    }200 201    /**202     * Advanced search with dynamic filtering using Criteria API203     */204    @Transactional(readOnly = true)205    public Page<IssueResponse> searchIssues(IssueSearchRequest request, Pageable pageable) {206        CriteriaBuilder cb = entityManager.getCriteriaBuilder();207        CriteriaQuery<Issue> query = cb.createQuery(Issue.class);208        Root<Issue> root = query.from(Issue.class);209 210        List<Predicate> predicates = new ArrayList<>();211 212        // Keyword search (title or description)213        if (request.getKeyword() != null && !request.getKeyword().trim().isEmpty()) {214            String keyword = "%" + request.getKeyword().toLowerCase() + "%";215            Predicate titleMatch = cb.like(cb.lower(root.get("title")), keyword);216            Predicate descMatch = cb.like(cb.lower(root.get("description")), keyword);217            predicates.add(cb.or(titleMatch, descMatch));218        }219 220        // Filter by statuses221        if (request.getStatuses() != null && !request.getStatuses().isEmpty()) {222            predicates.add(root.get("status").in(request.getStatuses()));223        }224 225        // Filter by category IDs226        if (request.getCategoryIds() != null && !request.getCategoryIds().isEmpty()) {227            predicates.add(root.get("category").get("id").in(request.getCategoryIds()));228        }229 230        // Filter by priorities231        if (request.getPriorities() != null && !request.getPriorities().isEmpty()) {232            predicates.add(root.get("priority").in(request.getPriorities()));233        }234 235        // Filter by ward236        if (request.getWard() != null && !request.getWard().trim().isEmpty()) {237            predicates.add(cb.equal(root.get("ward"), request.getWard()));238        }239 240        // Date range filter241        if (request.getStartDate() != null) {242            predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), request.getStartDate()));243        }244        if (request.getEndDate() != null) {245            predicates.add(cb.lessThanOrEqualTo(root.get("createdAt"), request.getEndDate()));246        }247 248        // Radius/proximity filter249        if (request.getLatitude() != null && request.getLongitude() != null && request.getRadiusMeters() != null) {250            double radiusDegrees = request.getRadiusMeters() / METERS_PER_DEGREE;251            predicates.add(cb.between(root.get("latitude"),252                    request.getLatitude() - radiusDegrees,253                    request.getLatitude() + radiusDegrees));254            predicates.add(cb.between(root.get("longitude"),255                    request.getLongitude() - radiusDegrees,256                    request.getLongitude() + radiusDegrees));257        }258 259        // Apply predicates260        if (!predicates.isEmpty()) {261            query.where(cb.and(predicates.toArray(new Predicate[0])));262        }263 264        // Sorting265        String sortBy = request.getSortBy() != null ? request.getSortBy() : "createdAt";266        if ("desc".equalsIgnoreCase(request.getSortDirection())) {267            query.orderBy(cb.desc(root.get(sortBy)));268        } else {269            query.orderBy(cb.asc(root.get(sortBy)));270        }271 272        // Execute query with pagination273        TypedQuery<Issue> typedQuery = entityManager.createQuery(query);274        typedQuery.setFirstResult((int) pageable.getOffset());275        typedQuery.setMaxResults(pageable.getPageSize());276 277        List<Issue> results = typedQuery.getResultList();278 279        // Get total count for pagination280        CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);281        Root<Issue> countRoot = countQuery.from(Issue.class);282        countQuery.select(cb.count(countRoot));283 284        // Rebuild predicates for count query (same filters)285        List<Predicate> countPredicates = new ArrayList<>();286        if (request.getKeyword() != null && !request.getKeyword().trim().isEmpty()) {287            String keyword = "%" + request.getKeyword().toLowerCase() + "%";288            countPredicates.add(cb.or(289                    cb.like(cb.lower(countRoot.get("title")), keyword),290                    cb.like(cb.lower(countRoot.get("description")), keyword)));291        }292        if (request.getStatuses() != null && !request.getStatuses().isEmpty()) {293            countPredicates.add(countRoot.get("status").in(request.getStatuses()));294        }295        if (request.getCategoryIds() != null && !request.getCategoryIds().isEmpty()) {296            countPredicates.add(countRoot.get("category").get("id").in(request.getCategoryIds()));297        }298        if (request.getPriorities() != null && !request.getPriorities().isEmpty()) {299            countPredicates.add(countRoot.get("priority").in(request.getPriorities()));300        }301        if (request.getWard() != null && !request.getWard().trim().isEmpty()) {302            countPredicates.add(cb.equal(countRoot.get("ward"), request.getWard()));303        }304        if (request.getStartDate() != null) {305            countPredicates.add(cb.greaterThanOrEqualTo(countRoot.get("createdAt"), request.getStartDate()));306        }307        if (request.getEndDate() != null) {308            countPredicates.add(cb.lessThanOrEqualTo(countRoot.get("createdAt"), request.getEndDate()));309        }310        if (request.getLatitude() != null && request.getLongitude() != null && request.getRadiusMeters() != null) {311            double radiusDegrees = request.getRadiusMeters() / METERS_PER_DEGREE;312            countPredicates.add(cb.between(countRoot.get("latitude"),313                    request.getLatitude() - radiusDegrees, request.getLatitude() + radiusDegrees));314            countPredicates.add(cb.between(countRoot.get("longitude"),315                    request.getLongitude() - radiusDegrees, request.getLongitude() + radiusDegrees));316        }317 318        if (!countPredicates.isEmpty()) {319            countQuery.where(cb.and(countPredicates.toArray(new Predicate[0])));320        }321 322        Long total = entityManager.createQuery(countQuery).getSingleResult();323 324        List<IssueResponse> responseList = results.stream()325                .map(issueMapper::toResponse)326                .collect(Collectors.toList());327 328        return new PageImpl<>(responseList, pageable, total);329    }330 331    /**332     * Update issue status (admin action)333     */334    @Transactional335    public IssueResponse updateIssueStatus(Long issueId, UpdateStatusRequest request, User admin) {336        Issue issue = issueRepository.findById(issueId)337                .orElseThrow(() -> new ResourceNotFoundException("Issue", "id", issueId));338 339        IssueStatus oldStatus = issue.getStatus();340        issue.setStatus(request.getStatus());341 342        // Handle status-specific updates343        if (request.getStatus() == IssueStatus.RESOLVED) {344            issue.setResolvedAt(LocalDateTime.now());345            issue.setResolutionNotes(request.getNotes());346        } else if (request.getStatus() == IssueStatus.REJECTED) {347            issue.setRejectionReason(request.getNotes());348        }349 350        // Assign to admin if provided351        if (request.getAssignedToId() != null) {352            User assignee = userRepository.findById(request.getAssignedToId())353                    .orElseThrow(() -> new ResourceNotFoundException("User", "id", request.getAssignedToId()));354            issue.setAssignedTo(assignee);355        }356 357        issue = issueRepository.save(issue);358 359        // Add status history360        IssueStatusHistory history = IssueStatusHistory.builder()361                .issue(issue)362                .oldStatus(oldStatus)363                .newStatus(request.getStatus())364                .changedBy(admin)365                .changeReason(request.getNotes())366                .build();367        statusHistoryRepository.save(history);368 369        // Create notification for reporter370        createNotification(issue.getReporter(), issue,371                "Issue Status Updated",372                String.format("Your issue '%s' status changed to %s", issue.getTitle(),373                        request.getStatus().getDisplayName()),374                "STATUS_UPDATE");375 376        // Broadcast update via WebSocket377        broadcastIssueUpdate("STATUS_UPDATE", issue);378 379        // Send personal notification to reporter380        sendPersonalNotification(issue.getReporter().getId(), issue);381 382        return issueMapper.toResponse(issue);383    }384 385    /**386     * Get status history for an issue387     */388    @Transactional(readOnly = true)389    public List<StatusHistoryResponse> getStatusHistory(Long issueId) {390        return statusHistoryRepository.findByIssueIdOrderByCreatedAtDesc(issueId)391                .stream()392                .map(issueMapper::toStatusHistoryResponse)393                .collect(Collectors.toList());394    }395 396    /**397     * Get dashboard statistics398     */399    @Transactional(readOnly = true)400    public DashboardStats getDashboardStats() {401        long total = issueRepository.count();402        long submitted = issueRepository.countByStatus(IssueStatus.SUBMITTED);403        long inProgress = issueRepository.countByStatus(IssueStatus.IN_PROGRESS);404        long resolved = issueRepository.countByStatus(IssueStatus.RESOLVED);405        long rejected = issueRepository.countByStatus(IssueStatus.REJECTED);406 407        Map<String, Long> byCategory = new HashMap<>();408        issueRepository.getIssueCountByCategory().forEach(row -> byCategory.put((String) row[0], (Long) row[1]));409 410        Map<String, Long> byWard = new HashMap<>();411        issueRepository.findAllDistinctWards().forEach(ward -> byWard.put(ward, issueRepository.countByWard(ward)));412 413        return DashboardStats.builder()414                .totalIssues(total)415                .submittedCount(submitted)416                .inProgressCount(inProgress)417                .resolvedCount(resolved)418                .rejectedCount(rejected)419                .issuesByCategory(byCategory)420                .issuesByWard(byWard)421                .build();422    }423 424    /**425     * Get all categories426     */427    @Transactional(readOnly = true)428    public List<CategoryResponse> getAllCategories() {429        return categoryRepository.findByIsActiveTrueOrderByPriorityAsc()430                .stream()431                .map(issueMapper::toCategoryResponse)432                .collect(Collectors.toList());433    }434 435    /**436     * Get issues filtered by department (for department dashboards).437     * Each department sees ONLY their own complaints.438     */439    @Transactional(readOnly = true)440    public List<IssueResponse> getIssuesByDepartment(String department) {441        return issueRepository.findByDepartmentOrderByCreatedAtDesc(department)442                .stream()443                .map(issueMapper::toResponse)444                .collect(Collectors.toList());445    }446 447    /**448     * Get per-department stats summary.449     */450    @Transactional(readOnly = true)451    public Map<String, Long> getDepartmentStats() {452        Map<String, Long> stats = new HashMap<>();453        issueRepository.getIssueCountByDepartment()454                .forEach(row -> stats.put((String) row[0], (Long) row[1]));455        return stats;456    }457 458    /**459     * Delete issue (Admin or Reporter only)460     */461    @Transactional462    public void deleteIssue(Long issueId, User user) {463        Issue issue = issueRepository.findById(issueId)464                .orElseThrow(() -> new ResourceNotFoundException("Issue", "id", issueId));465 466        // Check permission467        boolean isAdmin = user.getRoles().stream()468                .anyMatch(r -> r.getName() == RoleType.ROLE_ADMIN);469 470        if (!isAdmin && !issue.getReporter().getId().equals(user.getId())) {471            throw new BadRequestException("You do not have permission to delete this issue");472        }473 474        // Delete associated files475        if (issue.getImages() != null) {476            for (IssueImage img : issue.getImages()) {477                fileStorageService.deleteFile(img.getFileName());478            }479        }480 481        issueRepository.delete(issue);482    }483 484    /**485     * Update issue details (Reporter only)486     */487    @Transactional488    public IssueResponse updateIssueDetails(Long issueId, CreateIssueRequest request, User user) {489        Issue issue = issueRepository.findById(issueId)490                .orElseThrow(() -> new ResourceNotFoundException("Issue", "id", issueId));491 492        // Check permission: Only reporter can edit details493        if (!issue.getReporter().getId().equals(user.getId())) {494            throw new BadRequestException("Only the reporter can edit this issue");495        }496 497        // Allow edits only if status is SUBMITTED or IN_PROGRESS498        if (issue.getStatus() != IssueStatus.SUBMITTED && issue.getStatus() != IssueStatus.IN_PROGRESS) {499            throw new BadRequestException("Cannot edit issue in " + issue.getStatus() + " status");500        }501 502        // Update fields503        issue.setTitle(request.getTitle());504        issue.setDescription(request.getDescription());505        issue.setLatitude(request.getLatitude());506        issue.setLongitude(request.getLongitude());507        issue.setAddress(request.getAddress());508        issue.setWard(request.getWard());509        issue.setLandmark(request.getLandmark());510        if (request.getPriority() != null) {511            issue.setPriority(request.getPriority());512        }513 514        // Update category if changed515        if (!issue.getCategory().getId().equals(request.getCategoryId())) {516            IssueCategory newCategory = categoryRepository.findById(request.getCategoryId())517                    .orElseThrow(() -> new ResourceNotFoundException("Category", "id", request.getCategoryId()));518            issue.setCategory(newCategory);519        }520 521        issue = issueRepository.save(issue);522        return issueMapper.toResponse(issue);523    }524 525    /**526     * Create notification for a user527     */528    private void createNotification(User user, Issue issue, String title, String message, String type) {529        Notification notification = Notification.builder()530                .user(user)531                .issue(issue)532                .title(title)533                .message(message)534                .type(type)535                .isRead(false)536                .build();537        notificationRepository.save(notification);538    }539 540    /**541     * Broadcast issue update via WebSocket542     */543    private void broadcastIssueUpdate(String eventType, Issue issue) {544        Map<String, Object> payload = new HashMap<>();545        payload.put("eventType", eventType);546        payload.put("issue", issueMapper.toResponse(issue));547        payload.put("timestamp", System.currentTimeMillis());548 549        messagingTemplate.convertAndSend("/topic/issues", payload);550    }551 552    /**553     * Send personal notification via WebSocket554     */555    private void sendPersonalNotification(Long userId, Issue issue) {556        Map<String, Object> payload = new HashMap<>();557        payload.put("eventType", "NOTIFICATION");558        payload.put("issueId", issue.getId());559        payload.put("title", issue.getTitle());560        payload.put("status", issue.getStatus().name());561        payload.put("timestamp", System.currentTimeMillis());562 563        messagingTemplate.convertAndSendToUser(564                userId.toString(),565                "/queue/notifications",566                payload);567    }568}569