Skip to content

districts-geolocation

ISSUE #5 : DISTRICTS & GÉOLOCALISATION

🎯 OBJECTIF

Implémenter la carte Fiscométéo avec gestion des districts et géolocalisation interactive.

📖 CONTEXTE DU PROJET

La carte Fiscométéo est la feature phare de SmartAudit. Elle permet de visualiser en temps réel l'état fiscal de chaque district de Côte d'Ivoire avec des données interactives et des statistiques dynamiques.

14 Districts de Côte d'Ivoire

  1. Abidjan (ABJ) - District économique principal
  2. Yamoussoukro (YAM) - Capitale politique
  3. Bouaké (BOU) - Centre du pays
  4. San-Pédro (SPE) - Port maritime
  5. Korhogo (KOR) - Nord du pays
  6. Man (MAN) - Ouest montagneux
  7. Gagnoa (GAG) - Centre-ouest
  8. Divo (DIV) - Sud-ouest
  9. Abengourou (ABE) - Est
  10. Bondoukou (BON) - Nord-est
  11. Odienné (ODI) - Nord-ouest
  12. Daloa (DAL) - Centre-ouest
  13. Séguéla (SEG) - Centre-nord
  14. Toumodi (TOU) - Centre

Fonctionnalités de la Carte

  • Visualisation par district : Couleurs selon niveau de risque
  • Statistiques temps réel : Nombre d'entreprises, alertes, audits
  • Filtres interactifs : Par secteur, taille, statut
  • Zoom et navigation : Interface intuitive
  • Données détaillées : Popup avec informations complètes

🛠️ TÂCHES À IMPLÉMENTER

1. Services de Géolocalisation

// Fichier : service/DistrictService.java
@Service
@Transactional
public class DistrictService {
    
    @Autowired
    private DistrictRepository districtRepository;
    
    @Autowired
    private CompanyRepository companyRepository;
    
    @Autowired
    private AlertRepository alertRepository;
    
    @Autowired
    private AuditRepository auditRepository;
    
    public List<DistrictDTO> getAllDistricts() {
        List<District> districts = districtRepository.findAll();
        return districts.stream()
            .map(this::convertToDTO)
            .collect(Collectors.toList());
    }
    
    public DistrictDetailsDTO getDistrictDetails(Long districtId) {
        District district = districtRepository.findById(districtId)
            .orElseThrow(() -> new DistrictNotFoundException("District non trouvé"));
        
        // Calculer les statistiques
        DistrictStatistics stats = calculateDistrictStatistics(district);
        
        return DistrictDetailsDTO.builder()
            .district(convertToDTO(district))
            .statistics(stats)
            .companies(getDistrictCompanies(district))
            .alerts(getDistrictAlerts(district))
            .audits(getDistrictAudits(district))
            .build();
    }
    
    public List<DistrictMapDTO> getDistrictsForMap() {
        List<District> districts = districtRepository.findAll();
        
        return districts.stream()
            .map(district -> {
                DistrictStatistics stats = calculateDistrictStatistics(district);
                return DistrictMapDTO.builder()
                    .id(district.getId())
                    .code(district.getCode())
                    .name(district.getName())
                    .coordinates(parseCoordinates(district.getCoordinates()))
                    .riskLevel(calculateRiskLevel(stats))
                    .companyCount(stats.getCompanyCount())
                    .alertCount(stats.getAlertCount())
                    .auditCount(stats.getAuditCount())
                    .complianceScore(stats.getAverageComplianceScore())
                    .build();
            })
            .collect(Collectors.toList());
    }
    
    public DistrictStatistics calculateDistrictStatistics(District district) {
        // Compter les entreprises
        long companyCount = companyRepository.countByDistrict(district);
        
        // Compter les alertes actives
        long alertCount = alertRepository.countByCompanyDistrictAndStatus(district, AlertStatus.NEW);
        
        // Compter les audits en cours
        long auditCount = auditRepository.countByCompanyDistrictAndStatus(district, AuditStatus.IN_PROGRESS);
        
        // Calculer score de conformité moyen
        BigDecimal avgCompliance = companyRepository.getAverageComplianceScoreByDistrict(district);
        
        // Calculer montant total des redressements
        BigDecimal totalRedressement = auditRepository.getTotalRedressementByDistrict(district);
        
        return DistrictStatistics.builder()
            .companyCount(companyCount)
            .alertCount(alertCount)
            .auditCount(auditCount)
            .averageComplianceScore(avgCompliance)
            .totalRedressement(totalRedressement)
            .riskLevel(calculateRiskLevel(companyCount, alertCount, avgCompliance))
            .build();
    }
    
    private RiskLevel calculateRiskLevel(DistrictStatistics stats) {
        return calculateRiskLevel(stats.getCompanyCount(), stats.getAlertCount(), stats.getAverageComplianceScore());
    }
    
    private RiskLevel calculateRiskLevel(long companyCount, long alertCount, BigDecimal avgCompliance) {
        // Calculer le niveau de risque basé sur plusieurs facteurs
        double alertRatio = companyCount > 0 ? (double) alertCount / companyCount : 0;
        double complianceScore = avgCompliance != null ? avgCompliance.doubleValue() : 0;
        
        if (alertRatio > 0.3 || complianceScore < 60) {
            return RiskLevel.CRITICAL;
        } else if (alertRatio > 0.2 || complianceScore < 70) {
            return RiskLevel.HIGH;
        } else if (alertRatio > 0.1 || complianceScore < 80) {
            return RiskLevel.MEDIUM;
        } else {
            return RiskLevel.LOW;
        }
    }
    
    private CoordinatesDTO parseCoordinates(String coordinatesJson) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            JsonNode jsonNode = mapper.readTree(coordinatesJson);
            
            return CoordinatesDTO.builder()
                .latitude(jsonNode.get("lat").asDouble())
                .longitude(jsonNode.get("lng").asDouble())
                .bounds(jsonNode.get("bounds"))
                .build();
        } catch (Exception e) {
            log.error("Erreur parsing coordonnées: {}", e.getMessage());
            return null;
        }
    }
}

2. Controllers REST

// Fichier : controller/DistrictController.java
@RestController
@RequestMapping("/api/districts")
@CrossOrigin(origins = "*")
public class DistrictController {
    
    @Autowired
    private DistrictService districtService;
    
    @GetMapping
    public ResponseEntity<List<DistrictDTO>> getAllDistricts() {
        List<DistrictDTO> districts = districtService.getAllDistricts();
        return ResponseEntity.ok(districts);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<DistrictDetailsDTO> getDistrictDetails(@PathVariable Long id) {
        DistrictDetailsDTO details = districtService.getDistrictDetails(id);
        return ResponseEntity.ok(details);
    }
    
    @GetMapping("/map")
    public ResponseEntity<List<DistrictMapDTO>> getDistrictsForMap() {
        List<DistrictMapDTO> districts = districtService.getDistrictsForMap();
        return ResponseEntity.ok(districts);
    }
    
    @GetMapping("/{id}/companies")
    public ResponseEntity<List<CompanyDTO>> getDistrictCompanies(
            @PathVariable Long id,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) Sector sector,
            @RequestParam(required = false) RiskLevel riskLevel) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<Company> companies = districtService.getDistrictCompanies(id, sector, riskLevel, pageable);
        
        List<CompanyDTO> companyDTOs = companies.getContent().stream()
            .map(this::convertToDTO)
            .collect(Collectors.toList());
        
        return ResponseEntity.ok(companyDTOs);
    }
    
    @GetMapping("/{id}/statistics")
    public ResponseEntity<DistrictStatisticsDTO> getDistrictStatistics(@PathVariable Long id) {
        DistrictStatisticsDTO stats = districtService.getDistrictStatistics(id);
        return ResponseEntity.ok(stats);
    }
    
    @GetMapping("/{id}/alerts")
    public ResponseEntity<List<AlertDTO>> getDistrictAlerts(
            @PathVariable Long id,
            @RequestParam(required = false) AlertStatus status,
            @RequestParam(required = false) AlertPriority priority) {
        
        List<AlertDTO> alerts = districtService.getDistrictAlerts(id, status, priority);
        return ResponseEntity.ok(alerts);
    }
}

3. Configuration Leaflet.js

// Fichier : config/MapConfig.java
@Configuration
public class MapConfig {
    
    @Bean
    public MapConfiguration mapConfiguration() {
        return MapConfiguration.builder()
            .defaultZoom(7)
            .defaultCenter(new LatLng(7.5400, -5.5471)) // Centre Côte d'Ivoire
            .minZoom(6)
            .maxZoom(12)
            .tileLayerUrl("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png")
            .attribution("© OpenStreetMap contributors")
            .build();
    }
}

4. DTOs

// Fichier : dto/DistrictDTO.java
public class DistrictDTO {
    private Long id;
    private String code;
    private String name;
    private String description;
    private CoordinatesDTO coordinates;
    private Integer companyCount;
    private Integer agentCount;
}

// Fichier : dto/DistrictMapDTO.java
public class DistrictMapDTO {
    private Long id;
    private String code;
    private String name;
    private CoordinatesDTO coordinates;
    private RiskLevel riskLevel;
    private Long companyCount;
    private Long alertCount;
    private Long auditCount;
    private BigDecimal complianceScore;
}

// Fichier : dto/CoordinatesDTO.java
public class CoordinatesDTO {
    private Double latitude;
    private Double longitude;
    private JsonNode bounds; // Limites du district
}

// Fichier : dto/DistrictStatisticsDTO.java
public class DistrictStatisticsDTO {
    private Long companyCount;
    private Long alertCount;
    private Long auditCount;
    private BigDecimal averageComplianceScore;
    private BigDecimal totalRedressement;
    private RiskLevel riskLevel;
    private Map<Sector, Long> companiesBySector;
    private Map<RiskLevel, Long> companiesByRisk;
    private List<MonthlyTrend> monthlyTrends;
}

🔧 OUTILS & TECHNOLOGIES

Frontend (Angular)

  • Leaflet.js : Carte interactive
  • Angular Material : Interface utilisateur
  • Chart.js : Graphiques statistiques
  • RxJS : Gestion des observables

Backend (Spring Boot)

  • Spring Data JPA : Requêtes géographiques
  • Jackson : Sérialisation JSON
  • OpenStreetMap : Tiles cartographiques

Dépendances Frontend

{
  "dependencies": {
    "leaflet": "^1.9.4",
    "@types/leaflet": "^1.9.8",
    "chart.js": "^4.4.0",
    "@angular/material": "^18.0.0"
  }
}

🎯 CRITÈRES D'ACCEPTATION

Carte Interactive

  • 14 districts : Tous les districts CI affichés
  • Couleurs dynamiques : Selon niveau de risque
  • Zoom/navigation : Interface fluide
  • Popup informations : Détails au clic
  • Responsive : Adaptation mobile/desktop

Statistiques Temps Réel

  • Compteurs dynamiques : Entreprises, alertes, audits
  • Score conformité : Moyenne par district
  • Niveau risque : Calcul automatique
  • Tendances : Évolution mensuelle
  • Filtres : Par secteur, taille, statut

API REST

  • Endpoints géo : Données pour carte
  • Filtres avancés : Par critères multiples
  • Pagination : Pour gros volumes
  • Performance : < 200ms pour données carte

📁 STRUCTURE DES FICHIERS

src/main/java/ci/dgi/smartaudit/
├── service/
│   ├── DistrictService.java
│   └── GeoDataService.java
├── controller/
│   └── DistrictController.java
├── repository/
│   └── DistrictRepository.java
├── dto/
│   ├── DistrictDTO.java
│   ├── DistrictMapDTO.java
│   ├── CoordinatesDTO.java
│   └── DistrictStatisticsDTO.java
└── config/
    └── MapConfig.java

src/main/resources/
├── static/
│   ├── js/
│   │   ├── map.js
│   │   └── statistics.js
│   └── css/
│       └── map.css
└── datasets/
    └── districts_coordinates.json

🧪 TESTS À IMPLÉMENTER

Tests Unitaires

@ExtendWith(MockitoExtension.class)
class DistrictServiceTest {
    
    @Test
    void calculateRiskLevel_WithHighAlertRatio_ShouldReturnCritical() {
        // Test calcul niveau risque
    }
    
    @Test
    void getDistrictsForMap_ShouldReturnAllDistricts() {
        // Test données carte
    }
    
    @Test
    void parseCoordinates_WithValidJSON_ShouldReturnCoordinates() {
        // Test parsing coordonnées
    }
}

Tests d'Intégration

@SpringBootTest
@AutoConfigureTestDatabase
class DistrictControllerIntegrationTest {
    
    @Test
    void getDistrictsForMap_IntegrationTest() {
        // Test endpoint carte
    }
}

🚀 DÉMARRAGE RAPIDE

1. Configuration Carte

# application.yml
smartaudit:
  map:
    default-zoom: 7
    default-center:
      lat: 7.5400
      lng: -5.5471
    tile-layer: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"

2. Données Districts

// datasets/districts_coordinates.json
[
  {
    "code": "ABJ",
    "name": "Abidjan",
    "coordinates": {
      "lat": 5.316667,
      "lng": -4.033333,
      "bounds": [
        [5.1, -4.2],
        [5.5, -3.8]
      ]
    }
  }
]

3. Frontend Angular

// map.component.ts
export class MapComponent implements OnInit {
  map: L.Map;
  
  ngOnInit() {
    this.initMap();
    this.loadDistrictData();
  }
  
  private initMap() {
    this.map = L.map('map').setView([7.5400, -5.5471], 7);
    
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors'
    }).addTo(this.map);
  }
  
  private loadDistrictData() {
    this.districtService.getDistrictsForMap().subscribe(districts => {
      districts.forEach(district => {
        this.addDistrictMarker(district);
      });
    });
  }
}

📊 MÉTRIQUES DE SUCCÈS

  • Temps chargement carte : < 3 secondes
  • Fluidité navigation : 60 FPS
  • Précision géolocalisation : < 100m
  • Temps réponse API : < 200ms

🔗 INTÉGRATIONS

Avec Issue #2

  • Données entreprises : Géolocalisation par district
  • Statistiques : Calculs temps réel

Avec Issue #3

  • Alertes : Visualisation par district
  • Détection : Zones à risque

Avec Issue #4

  • Audits : Répartition géographique
  • Agents : Assignation par district

Avec Issue #6

  • Notifications : Alertes géographiques
  • Rapports : Statistiques par district

⚠️ POINTS D'ATTENTION

  1. Performance : Optimiser requêtes géographiques
  2. Données sensibles : Anonymiser si nécessaire
  3. Connexion : Fonctionnement hors ligne
  4. Mobile : Interface tactile optimisée

ESTIMATION : 1.5 jours
DIFFICULTÉ : Moyenne
PRIORITÉ : Haute (feature phare)